diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 082482fcb74..8086a6b86e5 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -5,12 +5,12 @@ Read before posting: - Checkout How to troubleshoot metric query issues: https://community.grafana.com/t/how-to-troubleshoot-metric-query-issues/50 Please include this information: -- What Grafana version are you using? -- What datasource are you using? -- What OS are you running grafana on? -- What did you do? -- What was the expected result? -- What happened instead? -- If related to metric query / data viz: - - Include raw network request & response: get by opening Chrome Dev Tools (F12, Ctrl+Shift+I on windows, Cmd+Opt+I on Mac), go the network tab. +### What Grafana version are you using? +### What datasource are you using? +### What OS are you running grafana on? +### What did you do? +### What was the expected result? +### What happened instead? +### If related to metric query / data viz: +### Include raw network request & response: get by opening Chrome Dev Tools (F12, Ctrl+Shift+I on windows, Cmd+Opt+I on Mac), go the network tab. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c5b9fe3751..304b1ba6d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,46 @@ # 5.1.0 (unreleased) +* **MSSQL**: New Microsoft SQL Server data source [#10093](https://github.com/grafana/grafana/pull/10093), [#11298](https://github.com/grafana/grafana/pull/11298), thx [@linuxchips](https://github.com/linuxchips) +* **Prometheus**: The heatmap panel now support Prometheus histograms [#10009](https://github.com/grafana/grafana/issues/10009) * **Postgres/MySQL**: Ability to insert 0s or nulls for missing intervals [#9487](https://github.com/grafana/grafana/issues/9487), thanks [@svenklemm](https://github.com/svenklemm) +* **Graph**: Thresholds for Right Y axis [#7107](https://github.com/grafana/grafana/issues/7107), thx [@ilgizar](https://github.com/ilgizar) +* **Graph**: Support multiple series stacking in histogram mode [#8151](https://github.com/grafana/grafana/issues/8151), thx [@mtanda](https://github.com/mtanda) +* **Alerting**: Pausing/un alerts now updates new_state_date [#10942](https://github.com/grafana/grafana/pull/10942) +* **Alerting**: Support Pagerduty notification channel using Pagerduty V2 API [#10531](https://github.com/grafana/grafana/issues/10531), thx [@jbaublitz](https://github.com/jbaublitz) +* **Templating**: Add comma templating format [#10632](https://github.com/grafana/grafana/issues/10632), thx [@mtanda](https://github.com/mtanda) +* **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda) +### Minor +* **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes) +* **Cloudwatch**: Support high resolution metrics [#10925](https://github.com/grafana/grafana/pull/10925), thx [@mtanda](https://github.com/mtanda) +* **Cloudwatch**: Add dimension filtering to CloudWatch `dimension_values()` [#10029](https://github.com/grafana/grafana/issues/10029), thx [@willyhutw](https://github.com/willyhutw) +* **Units**: Second to HH:mm:ss formatter [#11107](https://github.com/grafana/grafana/issues/11107), thx [@gladdiologist](https://github.com/gladdiologist) +* **Singlestat**: Add color to prefix and postfix in singlestat panel [#11143](https://github.com/grafana/grafana/pull/11143), thx [@ApsOps](https://github.com/ApsOps) -# 5.0.1 (unreleased) +# 5.0.4 (unreleased) +* **Dashboard** Fixed bug where collapsed panels could not be directly linked to/renderer [#11114](https://github.com/grafana/grafana/issues/11114) & [#11086](https://github.com/grafana/grafana/issues/11086) + +# 5.0.3 (2018-03-16) +* **Mysql**: Mysql panic occurring occasionally upon Grafana dashboard access (a bigger patch than the one in 5.0.2) [#11155](https://github.com/grafana/grafana/issues/11155) + +# 5.0.2 (2018-03-14) +* **Mysql**: Mysql panic occurring occasionally upon Grafana dashboard access [#11155](https://github.com/grafana/grafana/issues/11155) +* **Dashboards**: Should be possible to browse dashboard using only uid [#11231](https://github.com/grafana/grafana/issues/11231) +* **Alerting**: Fixes bug where alerts from hidden panels where deleted [#11222](https://github.com/grafana/grafana/issues/11222) +* **Import**: Fixes bug where dashboards with alerts couldn't be imported [#11227](https://github.com/grafana/grafana/issues/11227) +* **Teams**: Remove quota restrictions from teams [#11220](https://github.com/grafana/grafana/issues/11220) +* **Render**: Fixes bug with legacy url redirection for panel rendering [#11180](https://github.com/grafana/grafana/issues/11180) + +# 5.0.1 (2018-03-08) * **Postgres**: PostgreSQL error when using ipv6 address as hostname in connection string [#11055](https://github.com/grafana/grafana/issues/11055), thanks [@svenklemm](https://github.com/svenklemm) +* **Dashboards**: Changing templated value from dropdown is causing unsaved changes [#11063](https://github.com/grafana/grafana/issues/11063) +* **Prometheus**: Fixes bundled Prometheus 2.0 dashboard [#11016](https://github.com/grafana/grafana/issues/11016), thx [@roidelapluie](https://github.com/roidelapluie) +* **Sidemenu**: Profile menu "invisible" when gravatar is disabled [#11097](https://github.com/grafana/grafana/issues/11097) +* **Dashboard**: Fixes a bug with resizeable handles for panels [#11103](https://github.com/grafana/grafana/issues/11103) +* **Alerting**: Telegram inline image mode fails when caption too long [#10975](https://github.com/grafana/grafana/issues/10975) +* **Alerting**: Fixes silent failing validation [#11145](https://github.com/grafana/grafana/pull/11145) +* **OAuth**: Only use jwt token if it contains an email address [#11127](https://github.com/grafana/grafana/pull/11127) # 5.0.0-stable (2018-03-01) diff --git a/Gopkg.lock b/Gopkg.lock index 8d82b29d622..d447223795e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -103,6 +103,14 @@ revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" +[[projects]] + name = "github.com/denisenkom/go-mssqldb" + packages = [ + ".", + "internal/cp" + ] + revision = "270bc3860bb94dd3a3ffd047377d746c5e276726" + [[projects]] name = "github.com/fatih/color" packages = ["."] @@ -145,7 +153,6 @@ packages = [ ".", "memcache", - "mysql", "postgres", "redis" ] @@ -171,12 +178,14 @@ [[projects]] name = "github.com/go-xorm/core" packages = ["."] - revision = "e8409d73255791843585964791443dbad877058c" + revision = "da1adaf7a28ca792961721a34e6e04945200c890" + version = "v0.5.7" [[projects]] name = "github.com/go-xorm/xorm" packages = ["."] - revision = "6687a2b4e824f4d87f2d65060ec5cb0d896dff1e" + revision = "1933dd69e294c0a26c0266637067f24dbb25770c" + version = "v0.6.4" [[projects]] branch = "master" @@ -462,7 +471,10 @@ [[projects]] branch = "master" name = "golang.org/x/crypto" - packages = ["pbkdf2"] + packages = [ + "md4", + "pbkdf2" + ] revision = "3d37316aaa6bd9929127ac9a527abf408178ea7b" [[projects]] @@ -631,6 +643,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "4de68f1342ba98a637ec8ca7496aeeae2021bf9e4c7c80db7924e14709151a62" + inputs-digest = "8a9e651fb8ea49dfd3c6ddc99bd3242b39e453ea9edd11321da79bd2c865e9d1" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 22c56d29c71..350da50fc4b 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -85,13 +85,11 @@ ignored = [ [[constraint]] name = "github.com/go-xorm/core" - revision = "e8409d73255791843585964791443dbad877058c" - #version = "0.5.7" //keeping this since we would rather depend on version then commit + version = "0.5.7" [[constraint]] name = "github.com/go-xorm/xorm" - revision = "6687a2b4e824f4d87f2d65060ec5cb0d896dff1e" - #version = "0.6.4" //keeping this since we would rather depend on version then commit + version = "0.6.4" [[constraint]] name = "github.com/gorilla/websocket" @@ -197,3 +195,7 @@ ignored = [ [[constraint]] branch = "master" name = "github.com/teris-io/shortid" + +[[constraint]] + name = "github.com/denisenkom/go-mssqldb" + revision = "270bc3860bb94dd3a3ffd047377d746c5e276726" diff --git a/Makefile b/Makefile index d89718d200c..6f7beb837d8 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,14 @@ deps: deps-js build-go: go run build.go build +build-server: + go run build.go build-server + +build-cli: + go run build.go build-cli + build-js: - npm run build + yarn run build build: build-go build-js @@ -20,7 +26,7 @@ test-go: go test -v ./pkg/... test-js: - npm test + yarn test test: test-go test-js diff --git a/README.md b/README.md index 81fb1f8d42b..9a05633c391 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,6 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB. ![](http://docs.grafana.org/assets/img/features/dashboard_ex1.png) -## Grafana v5 Alpha Preview -Grafana master is now v5.0 alpha. This is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=BC_YRNpqj5k) of Grafana v5. - ## Installation Head to [docs.grafana.org](http://docs.grafana.org/installation/) and [download](https://grafana.com/get) the latest release. @@ -27,13 +24,13 @@ the latest master builds [here](https://grafana.com/grafana/download) ### Dependencies -- Go 1.9 +- Go 1.10 - NodeJS LTS ### Building the backend ```bash go get github.com/grafana/grafana -cd ~/go/src/github.com/grafana/grafana +cd $GOPATH/src/github.com/grafana/grafana go run build.go setup go run build.go build ``` diff --git a/ROADMAP.md b/ROADMAP.md index c8dc3186c73..e7bed99489e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,19 +6,21 @@ But it will give you an idea of our current vision and plan. ### Short term (1-2 months) - v5.1 - - Crossplatform builds & build speed improvements + - Build speed improvements & integration test execution + - Kubernetes friendly docker container - Enterprise LDAP - - New template interpolation syntax - Provisioning workflow - - First login registration view - - IFQL Initial support + - MSSQL datasource ### Mid term (2-4 months) - v5.2 - Azure monitor backend rewrite - Elasticsearch alerting + - First login registration view - Backend plugins? (alert notifiers, auth) + - Crossplatform builds + - IFQL Initial support ### Long term (4 - 8 months) diff --git a/build.go b/build.go index d55244246ff..c38c452f61f 100644 --- a/build.go +++ b/build.go @@ -79,10 +79,18 @@ func main() { case "setup": setup() + case "build-srv": + clean() + build("grafana-server", "./pkg/cmd/grafana-server", []string{}) + case "build-cli": clean() build("grafana-cli", "./pkg/cmd/grafana-cli", []string{}) + case "build-server": + clean() + build("grafana-server", "./pkg/cmd/grafana-server", []string{}) + case "build": clean() for _, binary := range binaries { diff --git a/circle.yml b/circle.yml index bf013e3f5b1..cfa8b762e49 100644 --- a/circle.yml +++ b/circle.yml @@ -1,57 +1,135 @@ -machine: - node: - version: 6.11.4 - python: - version: 2.7.3 - services: - - docker - environment: - GOPATH: "/home/ubuntu/.go_workspace" - ORG_PATH: "github.com/grafana" - REPO_PATH: "${ORG_PATH}/grafana" - GODIST: "go1.9.3.linux-amd64.tar.gz" - post: - - mkdir -p ~/download - - mkdir -p ~/docker - - test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST - - sudo rm -rf /usr/local/go - - sudo tar -C /usr/local -xzf download/$GODIST +version: 2 -dependencies: - cache_directories: - - "~/docker" - - "~/download" - override: - - rm -rf ${GOPATH}/src/${REPO_PATH} - - mkdir -p ${GOPATH}/src/${ORG_PATH} - - cp -r ~/grafana ${GOPATH}/src/${ORG_PATH} - pre: - - pip install awscli - - sudo apt-get update; sudo apt-get install rpm; sudo apt-get install expect - - ./scripts/build/build_container.sh +jobs: + test-frontend: + docker: + - image: circleci/node:6.11.4 + steps: + - checkout + - run: + name: install yarn + command: 'sudo npm install -g yarn --quiet' + - restore_cache: + key: dependency-cache-{{ checksum "yarn.lock" }} + # Could we skip this step if the cache has been restored? `[ -d node_modules ] || yarn install ...` should be able to apply to build step as well + - run: + name: yarn install + command: 'yarn install --pure-lockfile --no-progress' + - save_cache: + key: dependency-cache-{{ checksum "yarn.lock" }} + paths: + - node_modules + - run: + name: frontend tests + command: './scripts/circle-test-frontend.sh' -test: - override: - - bash scripts/circle-test-frontend.sh - - bash scripts/circle-test-backend.sh + test-backend: + docker: + - image: circleci/golang:1.10 + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: + name: build backend and run go tests + command: './scripts/circle-test-backend.sh' -deployment: - gh_branch: - branch: master - commands: - - ./scripts/build/deploy.sh - - ./scripts/build/sign_packages.sh - - go run build.go sha-dist - - aws s3 sync ./dist s3://$BUCKET_NAME/master - - ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master - - ./scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} - - go run ./scripts/build/publish.go -apiKey ${GRAFANA_COM_API_KEY} - gh_tag: - tag: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ - commands: - - ./scripts/build/deploy.sh - - ./scripts/build/sign_packages.sh - - go run build.go sha-dist - - aws s3 sync ./dist s3://$BUCKET_NAME/release - - ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release - - ./scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} ${CIRCLE_TAG} + build: + docker: + - image: grafana/build-container:v0.1 + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: + name: build and package grafana + command: './scripts/build/build.sh' + - run: + name: sign packages + command: './scripts/build/sign_packages.sh' + - run: + name: sha-sum packages + command: 'go run build.go sha-dist' + - run: + name: Build Grafana.com publisher + command: 'go build -o scripts/publish scripts/build/publish.go' + - persist_to_workspace: + root: . + paths: + - dist/grafana* + - scripts/*.sh + - scripts/publish + + deploy-master: + docker: + - image: circleci/python:2.7-stretch + 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/master' + - run: + name: Trigger Windows build + command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master' + - run: + name: Trigger Docker build + command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}' + - run: + name: Publish to Grafana.com + command: './scripts/publish -apiKey ${GRAFANA_COM_API_KEY}' + + deploy-release: + docker: + - image: circleci/python:2.7-stretch + steps: + - attach_workspace: + at: dist + - 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' + - run: + name: Trigger Docker build + command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} ${CIRCLE_TAG}' + +workflows: + version: 2 + test-and-build: + jobs: + - build: + filters: + tags: + only: /.*/ + - test-frontend: + filters: + tags: + only: /.*/ + - test-backend: + filters: + tags: + only: /.*/ + - deploy-master: + requires: + - test-backend + - test-frontend + - build + filters: + branches: + only: master + - deploy-release: + requires: + - test-backend + - test-frontend + - build + filters: + branches: + ignore: /.*/ + tags: + only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ diff --git a/conf/defaults.ini b/conf/defaults.ini index 4a2240f1924..11d173d955d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -82,6 +82,9 @@ max_idle_conn = 2 # Max conn setting default is 0 (mean not set) max_open_conn = +# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) +conn_max_lifetime = 14400 + # Set to true to log the sql calls and execution times. log_queries = @@ -125,6 +128,9 @@ cookie_secure = false session_life_time = 86400 gc_interval_time = 86400 +# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) +conn_max_lifetime = 14400 + #################################### Data proxy ########################### [dataproxy] diff --git a/conf/sample.ini b/conf/sample.ini index 3e45ac44d61..1af5bbdb62b 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -4,10 +4,10 @@ # change # possible values : production, development -; app_mode = production +;app_mode = production # instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty -; instance_name = ${HOSTNAME} +;instance_name = ${HOSTNAME} #################################### Paths #################################### [paths] @@ -21,7 +21,7 @@ ;plugins = /var/lib/grafana/plugins # folder that contains provisioning config files that grafana will apply on startup and while running. -; provisioning = conf/provisioning +;provisioning = conf/provisioning #################################### Server #################################### [server] @@ -90,6 +90,9 @@ # Max conn setting default is 0 (mean not set) ;max_open_conn = +# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) +;conn_max_lifetime = 14400 + # Set to true to log the sql calls and execution times. log_queries = @@ -121,7 +124,6 @@ log_queries = # This enables data proxy logging, default is false ;logging = false - #################################### Analytics #################################### [analytics] # Server reporting, sends usage counters to stats.grafana.org every 24 hours. @@ -323,7 +325,6 @@ log_queries = # optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug ;filters = - # For "console" mode only [log.console] ;level = @@ -369,7 +370,6 @@ log_queries = # Syslog tag. By default, the process' argv[0] is used. ;tag = - #################################### Alerting ############################ [alerting] # Disable alerting engine & UI features diff --git a/docker/blocks/elastic/docker-compose.yaml b/docker/blocks/elastic/docker-compose.yaml index 193b8f252f6..2eba60f38be 100644 --- a/docker/blocks/elastic/docker-compose.yaml +++ b/docker/blocks/elastic/docker-compose.yaml @@ -6,3 +6,10 @@ - "9300:9300" volumes: - ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml + + fake-elastic-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: elasticsearch + FD_PORT: 9200 diff --git a/docker/blocks/elastic5/docker-compose.yaml b/docker/blocks/elastic5/docker-compose.yaml index 5b12be9ada4..7148aa18c42 100644 --- a/docker/blocks/elastic5/docker-compose.yaml +++ b/docker/blocks/elastic5/docker-compose.yaml @@ -6,3 +6,10 @@ ports: - "10200:9200" - "10300:9300" + + fake-elastic5-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: elasticsearch + FD_PORT: 10200 diff --git a/docker/blocks/graphite1/big-dashboard.json b/docker/blocks/graphite1/big-dashboard.json new file mode 100644 index 00000000000..f825fb331c1 --- /dev/null +++ b/docker/blocks/graphite1/big-dashboard.json @@ -0,0 +1,1161 @@ +{ + "__inputs": [ + { + "name": "DS_GRAPHITE", + "label": "Graphite", + "description": "", + "type": "datasource", + "pluginId": "graphite", + "pluginName": "Graphite" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "5.0.0" + }, + { + "type": "datasource", + "id": "graphite", + "name": "Graphite", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "singlestat", + "name": "Singlestat", + "version": "5.0.0" + } + ], + "annotations": { + "enable": false, + "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": 1, + "id": null, + "links": [], + "panels": [ + { + "aliasColors": { + "cpu": "#E24D42", + "memory": "#1f78c1", + "statsd.fakesite.counters.session_start.desktop.count": "#6ED0E0" + }, + "annotate": { + "enable": false + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_GRAPHITE}", + "editable": true, + "fill": 3, + "grid": { + "max": null, + "min": 0 + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 4, + "interactive": true, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": true, + "show": true, + "total": false, + "values": false + }, + "legend_counts": true, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "options": false, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [ + { + "alias": "cpu", + "fill": 0, + "lines": true, + "yaxis": 2, + "zindex": 2 + }, + { + "alias": "memory", + "pointradius": 2, + "points": true + } + ], + "spaceLength": 10, + "spyable": true, + "stack": false, + "steppedLine": false, + "targets": [ + { + "hide": false, + "refId": "A", + "target": "alias(movingAverage(scaleToSeconds(apps.fakesite.web_server_01.counters.request_status.code_302.count, 10), 20), 'cpu')" + }, + { + "refId": "B", + "target": "alias(statsd.fakesite.counters.session_start.desktop.count, 'memory')" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "timezone": "browser", + "title": "Memory / CPU", + "tooltip": { + "msResolution": false, + "query_as_alias": true, + "shared": false, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "percent", + "logBase": 1, + "max": null, + "min": 0, + "show": true + } + ], + "zerofill": true + }, + { + "aliasColors": { + "logins": "#5195ce", + "logins (-1 day)": "#447EBC", + "logins (-1 hour)": "#705da0" + }, + "annotate": { + "enable": false + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_GRAPHITE}", + "editable": true, + "fill": 1, + "grid": { + "max": null, + "min": 0 + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 0 + }, + "id": 3, + "interactive": true, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "legend_counts": true, + "lines": true, + "linewidth": 1, + "nullPointMode": "connected", + "options": false, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [], + "spaceLength": 10, + "spyable": true, + "stack": true, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "alias(movingAverage(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count, 1), 2), 'logins')" + }, + { + "refId": "B", + "target": "alias(movingAverage(timeShift(scaleToSeconds(apps.fakesite.web_server_01.counters.requests.count, 1), '1h'), 2), 'logins (-1 hour)')" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": "1h", + "timezone": "browser", + "title": "logins", + "tooltip": { + "msResolution": false, + "query_as_alias": true, + "shared": false, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "zerofill": true + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "#629e51", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_GRAPHITE}", + "editable": true, + "error": false, + "format": "bytes", + "gauge": { + "maxValue": 300, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 16, + "y": 0 + }, + "id": 22, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "refId": "A", + "target": "scale(apps.backend.backend_01.counters.requests.count, 0.4)" + } + ], + "thresholds": "200,270", + "title": "Memory", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "${DS_GRAPHITE}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 0 + }, + "id": 16, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "refId": "A", + "target": "apps.backend.backend_02.counters.requests.count" + } + ], + "thresholds": "100,270", + "title": "Sign ups", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "${DS_GRAPHITE}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 3 + }, + "id": 17, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "refId": "A", + "target": "apps.backend.backend_04.counters.requests.count" + } + ], + "thresholds": "100,270", + "title": "Sign outs", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "${DS_GRAPHITE}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 6 + }, + "id": 15, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "refId": "A", + "target": "scale(apps.backend.backend_01.counters.requests.count, 0.7)" + } + ], + "thresholds": "100,270", + "title": "Logins", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "aliasColors": { + "web_server_01": "#badff4", + "web_server_02": "#5195ce", + "web_server_03": "#1f78c1", + "web_server_04": "#0a437c" + }, + "annotate": { + "enable": false + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_GRAPHITE}", + "editable": true, + "fill": 6, + "grid": { + "max": null, + "min": 0 + }, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 7 + }, + "id": 2, + "interactive": true, + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "legend_counts": true, + "lines": true, + "linewidth": 1, + "nullPointMode": "connected", + "options": false, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [], + "spaceLength": 10, + "spyable": true, + "stack": true, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(movingAverage(scaleToSeconds(apps.fakesite.*.counters.requests.count, 1), 2), 2)" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "timezone": "browser", + "title": "server requests", + "tooltip": { + "msResolution": false, + "query_as_alias": true, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "zerofill": true + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "#629e51", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_GRAPHITE}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 300, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 16, + "y": 7 + }, + "id": 21, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "refId": "A", + "target": "scale(apps.backend.backend_01.counters.requests.count, 0.8)" + } + ], + "thresholds": "200,270", + "title": "Logouts", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "${DS_GRAPHITE}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 9 + }, + "id": 18, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "refId": "A", + "target": "scale(apps.backend.backend_03.counters.requests.count, 0.3)" + } + ], + "thresholds": "100,270", + "title": "Support calls", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "#629e51", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_GRAPHITE}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 300, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 16, + "y": 12 + }, + "id": 26, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "refId": "A", + "target": "scale(apps.backend.backend_01.counters.requests.count, 0.2)" + } + ], + "thresholds": "200,270", + "title": "Google hits", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "#629e51", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_GRAPHITE}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 300, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 20, + "y": 12 + }, + "id": 24, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "refId": "A", + "target": "scale(apps.backend.backend_01.counters.requests.count, 0.2)" + } + ], + "thresholds": "200,270", + "title": "Google hits", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "aliasColors": { + "upper_25": "#F9E2D2", + "upper_50": "#F2C96D", + "upper_75": "#EAB839" + }, + "annotate": { + "enable": false + }, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_GRAPHITE}", + "editable": true, + "fill": 1, + "grid": { + "max": null, + "min": 0 + }, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 5, + "interactive": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "legend_counts": true, + "lines": false, + "linewidth": 2, + "nullPointMode": "connected", + "options": false, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [], + "spaceLength": 10, + "spyable": true, + "stack": true, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(summarize(statsd.fakesite.timers.ads_timer.*, '4min', 'avg'), 4)" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "timezone": "browser", + "title": "client side full page load", + "tooltip": { + "msResolution": false, + "query_as_alias": true, + "shared": false, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "zerofill": true + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [ + "demo" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "collapse": false, + "enable": true, + "notice": false, + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "2h", + " 6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "type": "timepicker" + }, + "timezone": "browser", + "title": "Big Dashboard", + "uid": "000000003", + "version": 1 +} \ No newline at end of file diff --git a/docker/blocks/graphite11/big-dashboard.json b/docker/blocks/graphite11/big-dashboard.json new file mode 100644 index 00000000000..dd22e287f23 --- /dev/null +++ b/docker/blocks/graphite11/big-dashboard.json @@ -0,0 +1,1179 @@ +{ + "__inputs": [ + { + "name": "DS_GRAPHITE_1.1+", + "label": "Graphite 1.1+", + "description": "", + "type": "datasource", + "pluginId": "graphite", + "pluginName": "Graphite" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "5.0.0" + }, + { + "type": "datasource", + "id": "graphite", + "name": "Graphite", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "singlestat", + "name": "Singlestat", + "version": "5.0.0" + } + ], + "annotations": { + "enable": false, + "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": 1, + "id": null, + "links": [], + "panels": [ + { + "aliasColors": { + "cpu": "#E24D42", + "memory": "#1f78c1", + "statsd.fakesite.counters.session_start.desktop.count": "#6ED0E0" + }, + "annotate": { + "enable": false + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "fill": 3, + "grid": { + "max": null, + "min": 0 + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 4, + "interactive": true, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": true, + "show": true, + "total": false, + "values": false + }, + "legend_counts": true, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "options": false, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [ + { + "alias": "cpu", + "fill": 0, + "lines": true, + "yaxis": 2, + "zindex": 2 + }, + { + "alias": "memory", + "pointradius": 2, + "points": true + } + ], + "spaceLength": 10, + "spyable": true, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refCount": 0, + "refId": "C", + "target": "alias(movingAverage(scaleToSeconds(seriesByTag('name=request_status.count', 'status=code_302', 'app=fakesite', 'server=web_server_01'), 10), 20, 0), 'cpu')" + }, + { + "refCount": 0, + "refId": "A", + "target": "alias(seriesByTag('name=statsd.counters.session_start', 'app=fakesite', 'device=desktop'), 'memory')" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "timezone": "browser", + "title": "Memory / CPU", + "tooltip": { + "msResolution": false, + "query_as_alias": true, + "shared": false, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "percent", + "logBase": 1, + "max": null, + "min": 0, + "show": true + } + ], + "zerofill": true + }, + { + "aliasColors": { + "logins": "#5195ce", + "logins (-1 day)": "#447EBC", + "logins (-1 hour)": "#705da0" + }, + "annotate": { + "enable": false + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "fill": 1, + "grid": { + "max": null, + "min": 0 + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 0 + }, + "id": 3, + "interactive": true, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "legend_counts": true, + "lines": true, + "linewidth": 1, + "nullPointMode": "connected", + "options": false, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [], + "spaceLength": 10, + "spyable": true, + "stack": true, + "steppedLine": false, + "targets": [ + { + "refCount": 0, + "refId": "C", + "target": "alias(movingAverage(scaleToSeconds(seriesByTag('name=requests.count', 'app=fakesite', 'server=web_server_01'), 1), 2, 0), 'logins')" + }, + { + "refCount": 0, + "refId": "A", + "target": "alias(movingAverage(timeShift(scaleToSeconds(seriesByTag('name=requests.count', 'app=fakesite', 'server=web_server_01'), 1), '1h'), 2, 0), 'logins (-1 hour)')" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": "1h", + "timezone": "browser", + "title": "logins", + "tooltip": { + "msResolution": false, + "query_as_alias": true, + "shared": false, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "zerofill": true + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "#629e51", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "error": false, + "format": "bytes", + "gauge": { + "maxValue": 300, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 16, + "y": 0 + }, + "id": 22, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "refCount": 0, + "refId": "B", + "target": "scale(seriesByTag('app=backend', 'server=backend_01', 'name=requests.count'), 0.4)", + "textEditor": false + } + ], + "thresholds": "200,270", + "title": "Memory", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 0 + }, + "id": 16, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "refCount": 0, + "refId": "B", + "target": "seriesByTag('app=backend', 'server=backend_02', 'name=requests.count')" + } + ], + "thresholds": "100,270", + "title": "Sign ups", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 3 + }, + "id": 17, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "refCount": 0, + "refId": "B", + "target": "seriesByTag('name=requests.count', 'app=backend', 'server=backend_04')", + "textEditor": false + } + ], + "thresholds": "100,270", + "title": "Sign outs", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 6 + }, + "id": 15, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "refCount": 0, + "refId": "B", + "target": "scale(seriesByTag('name=requests.count', 'app=backend', 'server=backend_01'), 0.7)", + "textEditor": false + } + ], + "thresholds": "100,270", + "title": "Logins", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "aliasColors": { + "web_server_01": "#badff4", + "web_server_02": "#5195ce", + "web_server_03": "#1f78c1", + "web_server_04": "#0a437c" + }, + "annotate": { + "enable": false + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "fill": 6, + "grid": { + "max": null, + "min": 0 + }, + "gridPos": { + "h": 11, + "w": 16, + "x": 0, + "y": 7 + }, + "id": 2, + "interactive": true, + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "legend_counts": true, + "lines": true, + "linewidth": 1, + "nullPointMode": "connected", + "options": false, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [], + "spaceLength": 10, + "spyable": true, + "stack": true, + "steppedLine": false, + "targets": [ + { + "refCount": 0, + "refId": "B", + "target": "aliasByTags(movingAverage(scaleToSeconds(seriesByTag('name=requests.count', 'app=fakesite', 'target='), 1), 2, 0), 'server')" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "timezone": "browser", + "title": "server requests", + "tooltip": { + "msResolution": false, + "query_as_alias": true, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "zerofill": true + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "#629e51", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 300, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 16, + "y": 7 + }, + "id": 21, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "hide": false, + "refCount": 0, + "refId": "B", + "target": "scale(seriesByTag('name=requests.count', 'app=backend', 'server=backend_01'), 0.8)" + } + ], + "thresholds": "200,270", + "title": "Logouts", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 9 + }, + "id": 18, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "refCount": 0, + "refId": "B", + "target": "scale(seriesByTag('name=requests.count', 'app=backend', 'server=backend_03'), 0.3)", + "textEditor": false + } + ], + "thresholds": "100,270", + "title": "Support calls", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "#629e51", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 300, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 16, + "y": 12 + }, + "id": 26, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "refCount": 0, + "refId": "B", + "target": "scale(seriesByTag('name=requests.count', 'app=backend', 'server=backend_01'), 0.2)" + } + ], + "thresholds": "200,270", + "title": "Google hits", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "#629e51", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 300, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 20, + "y": 12 + }, + "id": 24, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "refCount": 0, + "refId": "B", + "target": "scale(seriesByTag('name=requests.count', 'app=backend', 'server=backend_01'), 0.2)" + } + ], + "thresholds": "200,270", + "title": "Google hits", + "type": "singlestat", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "aliasColors": { + "upper_25": "#F9E2D2", + "upper_50": "#F2C96D", + "upper_75": "#EAB839" + }, + "annotate": { + "enable": false + }, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_GRAPHITE_1.1+}", + "editable": true, + "fill": 1, + "grid": { + "max": null, + "min": 0 + }, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 5, + "interactive": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "legend_counts": true, + "lines": false, + "linewidth": 2, + "nullPointMode": "connected", + "options": false, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [], + "spaceLength": 10, + "spyable": true, + "stack": true, + "steppedLine": false, + "targets": [ + { + "refCount": 0, + "refId": "B", + "target": "aliasByTags(summarize(seriesByTag('name=statsd.timers.ads_timer', 'app=fakesite', 'percentile='), '4min', 'average'), 'target')" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "timezone": "browser", + "title": "client side full page load", + "tooltip": { + "msResolution": false, + "query_as_alias": true, + "shared": false, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "zerofill": true + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [ + "demo" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "collapse": false, + "enable": true, + "notice": false, + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "2h", + " 6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "type": "timepicker" + }, + "timezone": "browser", + "title": "Big Dashboard", + "uid": "yNSrbLRmz", + "version": 9 +} \ No newline at end of file diff --git a/docker/blocks/graphite11/docker-compose.yaml b/docker/blocks/graphite11/docker-compose.yaml new file mode 100644 index 00000000000..4b0d837a619 --- /dev/null +++ b/docker/blocks/graphite11/docker-compose.yaml @@ -0,0 +1,18 @@ + graphite11: + image: graphiteapp/graphite-statsd + ports: + - "8180:80" + - "2103-2104:2003-2004" + - "2123-2124:2023-2024" + - "8225:8125/udp" + - "8226:8126" + + fake-graphite11-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: graphite + FD_PORT: 2103 + FD_GRAPHITE_VERSION: 1.1 + depends_on: + - graphite11 \ No newline at end of file diff --git a/docker/blocks/mssql/build/Dockerfile b/docker/blocks/mssql/build/Dockerfile new file mode 100644 index 00000000000..ac19463538c --- /dev/null +++ b/docker/blocks/mssql/build/Dockerfile @@ -0,0 +1,5 @@ +FROM microsoft/mssql-server-linux:2017-CU4 +WORKDIR /usr/setup +COPY . /usr/setup +RUN chmod +x /usr/setup/setup.sh +CMD /bin/bash ./entrypoint.sh \ No newline at end of file diff --git a/docker/blocks/mssql/build/entrypoint.sh b/docker/blocks/mssql/build/entrypoint.sh new file mode 100644 index 00000000000..de5aef5124a --- /dev/null +++ b/docker/blocks/mssql/build/entrypoint.sh @@ -0,0 +1,2 @@ +#start SQL Server and run setup script +/usr/setup/setup.sh & /opt/mssql/bin/sqlservr \ No newline at end of file diff --git a/docker/blocks/mssql/build/setup.sh b/docker/blocks/mssql/build/setup.sh new file mode 100755 index 00000000000..e1d509b042e --- /dev/null +++ b/docker/blocks/mssql/build/setup.sh @@ -0,0 +1,12 @@ +#/bin/bash + +#wait for the SQL Server to come up +sleep 15s + +cat /usr/setup/setup.sql.template | awk '{ + gsub(/%%DB%%/,"'$MSSQL_DATABASE'"); + gsub(/%%USER%%/,"'$MSSQL_USER'"); + gsub(/%%PWD%%/,"'$MSSQL_PASSWORD'") +}1' > /usr/setup/setup.sql + +/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P $MSSQL_SA_PASSWORD -d master -i /usr/setup/setup.sql \ No newline at end of file diff --git a/docker/blocks/mssql/build/setup.sql.template b/docker/blocks/mssql/build/setup.sql.template new file mode 100644 index 00000000000..1746a18d241 --- /dev/null +++ b/docker/blocks/mssql/build/setup.sql.template @@ -0,0 +1,14 @@ +CREATE LOGIN %%USER%% WITH PASSWORD = '%%PWD%%' +GO + +CREATE DATABASE %%DB%%; +GO + +USE %%DB%%; +GO + +CREATE USER %%USER%% FOR LOGIN %%USER%%; +GO + +EXEC sp_addrolemember 'db_owner', '%%USER%%'; +GO diff --git a/docker/blocks/mssql/dashboard.json b/docker/blocks/mssql/dashboard.json new file mode 100644 index 00000000000..ce9aa141a75 --- /dev/null +++ b/docker/blocks/mssql/dashboard.json @@ -0,0 +1,539 @@ +{ + "__inputs": [ + { + "name": "DS_MSSQL", + "label": "MSSQL", + "description": "", + "type": "datasource", + "pluginId": "mssql", + "pluginName": "MSSQL" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "5.0.0" + }, + { + "type": "datasource", + "id": "mssql", + "name": "MSSQL", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "5.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "A dashboard visualizing data generated from grafana/fake-data-gen", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "iteration": 1520976748896, + "links": [], + "panels": [ + { + "aliasColors": { + "total avg": "#6ed0e0" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL}", + "fill": 2, + "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": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "total avg", + "fill": 0, + "pointradius": 3, + "points": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time,\n avg(value) as value,\n hostname as metric\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'logins.count' AND\n hostname IN($host)\nGROUP BY $__timeGroup(createdAt,'$summarize'), hostname\nORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time,\n min(value) as value,\n 'total avg' as metric\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'logins.count'\nGROUP BY $__timeGroup(createdAt,'$summarize')\nORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Average logins / $summarize", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL}", + "fill": 2, + "gridPos": { + "h": 18, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 8, + "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": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time,\n avg(value) as value,\n 'started' as metric\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'payment.started'\nGROUP BY $__timeGroup(createdAt,'$summarize')\nORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time,\n avg(value) as value,\n 'ended' as \"metric\"\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'payment.ended'\nGROUP BY $__timeGroup(createdAt,'$summarize')\nORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Average payments started/ended / $summarize", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 6, + "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": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT\n $__timeGroup(createdAt,'$summarize') as time,\n max(value) as value,\n hostname as metric\nFROM \n grafana_metric\nWHERE\n $__timeFilter(createdAt) AND\n measurement = 'cpu' AND\n hostname IN($host)\nGROUP BY $__timeGroup(createdAt,'$summarize'), hostname\nORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Max CPU / $summarize", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "columns": [], + "datasource": "${DS_MSSQL}", + "fontSize": "100%", + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 4, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT createdAt as Time, source, datacenter, hostname, value FROM grafana_metric WHERE hostname in($host)", + "refId": "A" + } + ], + "title": "Values", + "transform": "table", + "type": "table" + } + ], + "schemaVersion": 16, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": {}, + "datasource": "${DS_MSSQL}", + "hide": 0, + "includeAll": false, + "label": "Datacenter", + "multi": false, + "name": "datacenter", + "options": [], + "query": "SELECT DISTINCT datacenter FROM grafana_metric", + "refresh": 1, + "regex": "", + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": {}, + "datasource": "${DS_MSSQL}", + "hide": 0, + "includeAll": true, + "label": "Hostname", + "multi": true, + "name": "host", + "options": [], + "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'", + "refresh": 1, + "regex": "", + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "1m", + "value": "1m" + }, + "hide": 0, + "label": "Summarize", + "name": "summarize", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "10s", + "value": "10s" + }, + { + "selected": false, + "text": "30s", + "value": "30s" + }, + { + "selected": true, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": false, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "type": "interval" + } + ] + }, + "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": "Grafana Fake Data Gen - MSSQL", + "uid": "86Js1xRmk", + "version": 11 +} \ No newline at end of file diff --git a/docker/blocks/mssql/docker-compose.yaml b/docker/blocks/mssql/docker-compose.yaml new file mode 100644 index 00000000000..538908fec72 --- /dev/null +++ b/docker/blocks/mssql/docker-compose.yaml @@ -0,0 +1,19 @@ + mssql: + build: + context: blocks/mssql/build + environment: + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: Password! + MSSQL_PID: Express + MSSQL_DATABASE: grafana + MSSQL_USER: grafana + MSSQL_PASSWORD: Password! + ports: + - "1433:1433" + + fake-mssql-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: mssql + FD_PORT: 1433 \ No newline at end of file diff --git a/docker/blocks/mssql_tests/dashboard.json b/docker/blocks/mssql_tests/dashboard.json new file mode 100644 index 00000000000..323a61bb49a --- /dev/null +++ b/docker/blocks/mssql_tests/dashboard.json @@ -0,0 +1,2242 @@ +{ + "__inputs": [ + { + "name": "DS_MSSQL_TEST", + "label": "MSSQL Test", + "description": "", + "type": "datasource", + "pluginId": "mssql", + "pluginName": "Microsoft SQL Server" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "5.0.0" + }, + { + "type": "datasource", + "id": "mssql", + "name": "Microsoft SQL Server", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "5.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + }, + { + "datasource": "${DS_MSSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "#6ed0e0", + "limit": 100, + "name": "Deploys", + "rawQuery": "SELECT\n time_sec as time,\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_MSSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "rgba(255, 96, 96, 1)", + "limit": 100, + "name": "Tickets", + "rawQuery": "SELECT\n time_sec as time,\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_MSSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "#7eb26d", + "limit": 100, + "name": "Metric Values", + "rawQuery": "SELECT \n time, \n measurement as text, \n '' as tags\nFROM\n metric_values \nORDER BY 1", + "showIn": 0, + "tags": [], + "type": "tags" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "iteration": 1521481503341, + "links": [], + "panels": [ + { + "columns": [], + "datasource": "${DS_MSSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT * from mssql_types", + "refId": "A" + } + ], + "title": "Data types", + "transform": "table", + "type": "table" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 4 + }, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m without fill", + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 4 + }, + "id": 9, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(NULL) and null as zero", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 4 + }, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(10.0)", + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 13 + }, + "id": 16, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize without fill", + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 13 + }, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(NULL)", + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 13 + }, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(100.0)", + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 27, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value one' as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value two' as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column using timeGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 22 + }, + "id": 5, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nGROUP BY \n $__timeGroup(time, '$summarize')\nORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column using timeGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 30 + }, + "id": 4, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column", + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 30 + }, + "id": 28, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column", + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 38 + }, + "id": 19, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - stacked", + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 38 + }, + "id": 18, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - stacked", + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 46 + }, + "id": 17, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - stacked percent", + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 46 + }, + "id": 20, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - stacked percent", + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 54 + }, + "id": 29, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "DECLARE \n @from int = $__unixEpochFrom(),\n @to int = $__unixEpochTo()\n \nEXEC dbo.sp_test_epoch @from, @to", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Stored procedure support using epoch", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 54 + }, + "id": 30, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "DECLARE \n @from datetime = $__timeFrom(),\n @to datetime = $__timeTo()\n \nEXEC dbo.sp_test_datetime @from, @to", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Stored procedure support using datetime", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 62 + }, + "id": 14, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 62 + }, + "id": 15, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 70 + }, + "id": 25, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 50, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 70 + }, + "id": 22, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 100, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 78 + }, + "id": 21, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 78 + }, + "id": 26, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "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 + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 86 + }, + "id": 23, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 86 + }, + "id": 24, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "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 + } + ] + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "10m", + "value": "10m" + }, + "hide": 0, + "label": "Interval", + "name": "summarize", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "10s", + "value": "10s" + }, + { + "selected": false, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + }, + { + "selected": true, + "text": "10m", + "value": "10m" + } + ], + "query": "1s,10s,30s,1m,5m,10m", + "refresh": 2, + "type": "interval" + } + ] + }, + "time": { + "from": "2018-03-15T12:30:00.000Z", + "to": "2018-03-15T13:55:00.000Z" + }, + "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": "Microsoft SQL Server Data Source Test", + "uid": "GlAqcPgmz", + "version": 37 +} \ No newline at end of file diff --git a/docker/blocks/mssql_tests/docker-compose.yaml b/docker/blocks/mssql_tests/docker-compose.yaml new file mode 100644 index 00000000000..5da6aad82af --- /dev/null +++ b/docker/blocks/mssql_tests/docker-compose.yaml @@ -0,0 +1,12 @@ + mssqltests: + build: + context: blocks/mssql/build + environment: + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: Password! + MSSQL_PID: Express + MSSQL_DATABASE: grafanatest + MSSQL_USER: grafana + MSSQL_PASSWORD: Password! + ports: + - "1433:1433" \ No newline at end of file diff --git a/docker/blocks/prometheus/docker-compose.yaml b/docker/blocks/prometheus/docker-compose.yaml index ccb1238a179..3c304cc74ad 100644 --- a/docker/blocks/prometheus/docker-compose.yaml +++ b/docker/blocks/prometheus/docker-compose.yaml @@ -23,3 +23,9 @@ network_mode: host ports: - "9093:9093" + + prometheus-random-data: + build: blocks/prometheus_random_data + network_mode: host + ports: + - "8081:8080" diff --git a/docker/blocks/prometheus/prometheus.yml b/docker/blocks/prometheus/prometheus.yml index ae40dfdf067..0ff8c3e7100 100644 --- a/docker/blocks/prometheus/prometheus.yml +++ b/docker/blocks/prometheus/prometheus.yml @@ -25,11 +25,15 @@ scrape_configs: - job_name: 'node_exporter' static_configs: - targets: ['127.0.0.1:9100'] - + - job_name: 'fake-data-gen' static_configs: - targets: ['127.0.0.1:9091'] - + - job_name: 'grafana' static_configs: - targets: ['127.0.0.1:3000'] + + - job_name: 'prometheus-random-data' + static_configs: + - targets: ['127.0.0.1:8081'] diff --git a/docker/blocks/prometheus2/Dockerfile b/docker/blocks/prometheus2/Dockerfile index d4a9eb2d75d..03edf4c9ee2 100644 --- a/docker/blocks/prometheus2/Dockerfile +++ b/docker/blocks/prometheus2/Dockerfile @@ -1,3 +1,3 @@ -FROM prom/prometheus:v2.0.0 +FROM prom/prometheus:v2.2.0 ADD prometheus.yml /etc/prometheus/ ADD alert.rules /etc/prometheus/ diff --git a/docker/blocks/prometheus2/docker-compose.yaml b/docker/blocks/prometheus2/docker-compose.yaml index 7b133ed9000..589df868084 100644 --- a/docker/blocks/prometheus2/docker-compose.yaml +++ b/docker/blocks/prometheus2/docker-compose.yaml @@ -23,3 +23,9 @@ network_mode: host ports: - "9093:9093" + + prometheus-random-data: + build: blocks/prometheus_random_data + network_mode: host + ports: + - "8081:8080" diff --git a/docker/blocks/prometheus2/prometheus.yml b/docker/blocks/prometheus2/prometheus.yml index 83dda78bb3c..42592543d87 100644 --- a/docker/blocks/prometheus2/prometheus.yml +++ b/docker/blocks/prometheus2/prometheus.yml @@ -25,11 +25,15 @@ scrape_configs: - job_name: 'node_exporter' static_configs: - targets: ['127.0.0.1:9100'] - + - job_name: 'fake-data-gen' static_configs: - targets: ['127.0.0.1:9091'] - + - job_name: 'grafana' static_configs: - targets: ['127.0.0.1:3000'] + + - job_name: 'prometheus-random-data' + static_configs: + - targets: ['127.0.0.1:8081'] diff --git a/docker/blocks/prometheus_random_data/Dockerfile b/docker/blocks/prometheus_random_data/Dockerfile new file mode 100644 index 00000000000..3aad497c94d --- /dev/null +++ b/docker/blocks/prometheus_random_data/Dockerfile @@ -0,0 +1,18 @@ +# This Dockerfile builds an image for a client_golang example. + +# Builder image, where we build the example. +FROM golang:1.9.0 AS builder +# Download prometheus/client_golang/examples/random first +RUN go get github.com/prometheus/client_golang/examples/random +WORKDIR /go/src/github.com/prometheus/client_golang +WORKDIR /go/src/github.com/prometheus/client_golang/prometheus +RUN go get -d +WORKDIR /go/src/github.com/prometheus/client_golang/examples/random +RUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w' + +# Final image. +FROM scratch +LABEL maintainer "The Prometheus Authors " +COPY --from=builder /go/src/github.com/prometheus/client_golang/examples/random . +EXPOSE 8080 +ENTRYPOINT ["/random"] diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 140053ec541..a8cb9fe3023 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -133,18 +133,24 @@ datasources: editable: false ``` +#### Extra info per datasource + +| Datasource | Misc | +| ---- | ---- | +| Elasticserach | Elasticsearch uses the `database` property to configure the index for a datasource | + #### 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. -| Name | Type | Datasource |Description | -| ----| ---- | ---- | --- | +| Name | Type | Datasource | Description | +| ---- | ---- | ---- | ---- | | tlsAuth | boolean | *All* | Enable TLS authentication using client cert configured in secure json data | | tlsAuthWithCACert | boolean | *All* | Enable TLS authtication using CA cert | | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | | graphiteVersion | string | Graphite | Graphite version | | timeInterval | string | Elastic, Influxdb & Prometheus | Lowest interval/step value that should be used for this data source | -| esVersion | string | Elastic | Elasticsearch version | +| esVersion | string | Elastic | Elasticsearch version as an number (2/5/56) | | timeField | string | Elastic | Which field that should be used as timestamp | | interval | string | Elastic | Index date time format | | authType | string | Cloudwatch | Auth provider. keys/credentials/arn | diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 453d169457b..fe57fd0fa8f 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -58,6 +58,8 @@ Recipient | allows you to override the Slack recipient. Mention | make it possible to include a mention in the Slack notification sent by Grafana. Ex @here or @channel Token | If provided, Grafana will upload the generated image via Slack's file.upload API method, not the external image destination. +If you are using the token for a slack bot, then you have to invite the bot to the channel you want to send notifications and add the channel to the recipient field. + ### PagerDuty To set up PagerDuty, all you have to do is to provide an API key. diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index bd5b95da856..9bbbd70641d 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -59,7 +59,7 @@ avg() OF query(A, 5m, now) IS BELOW 14 ``` - `avg()` Controls how the values for **each** series should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function. -- `query(A, 5m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `5m, now` means 5 minutes from now to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes from now to 2 minutes from now. This is useful if you want to ignore the last 2 minutes of data. +- `query(A, 5m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `5m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data. - `IS BELOW 14` Defines the type of threshold and the threshold value. You can click on `IS BELOW` to change the type of threshold. The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially. diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index e955dbb9569..f7f8138b5e9 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -87,7 +87,7 @@ Name | Description *namespaces()* | Returns a list of namespaces CloudWatch support. *metrics(namespace, [region])* | Returns a list of metrics in the namespace. (specify region or use "default" for custom metrics) *dimension_keys(namespace)* | Returns a list of dimension keys in the namespace. -*dimension_values(region, namespace, metric, dimension_key)* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`. +*dimension_values(region, namespace, metric, dimension_key, [filters])* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric`, `dimension_key` or you can use dimension `filters` to get more specific result as well. *ebs_volume_ids(region, instance_id)* | Returns a list of volume ids matching the specified `region`, `instance_id`. *ec2_instance_attribute(region, attribute_name, filters)* | Returns a list of attributes matching the specified `region`, `attribute_name`, `filters`. @@ -104,6 +104,7 @@ Query | Service *dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)* | RedShift *dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)* | RDS *dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)* | S3 +*dimension_values(us-east-1,CWAgent,disk_used_percent,device,{"InstanceId":"$instance_id"})* | CloudWatch Agent ## ec2_instance_attribute examples diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index 15247ba5ebd..c9bb16441ca 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -93,7 +93,7 @@ queries via the Dashboard menu / Annotations view. Prometheus supports two ways to query annotations. - A regular metric query -- A Prometheus query for pending and firing alerts (for details see [Inspecting alerts during runtime](https://prometheus.io/docs/alerting/rules/#inspecting-alerts-during-runtime)) +- A Prometheus query for pending and firing alerts (for details see [Inspecting alerts during runtime](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/#inspecting-alerts-during-runtime)) The step option is useful to limit the number of events returned from your query. diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index a6ed0c81f4b..ea1bd7f2ef7 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -43,11 +43,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "title": "Production Overview", "tags": [ "templated" ], "timezone": "browser", - "rows": [ - { - } - ], - "schemaVersion": 6, + "schemaVersion": 16, "version": 0 }, "folderId": 0, @@ -140,11 +136,7 @@ Content-Type: application/json "title": "Production Overview", "tags": [ "templated" ], "timezone": "browser", - "rows": [ - { - } - ], - "schemaVersion": 6, + "schemaVersion": 16, "version": 0 }, "meta": { @@ -219,14 +211,9 @@ Content-Type: application/json "editable":false, "hideControls":true, "nav":[ - { - "enable":false, - "type":"timepicker" - } - ], - "rows": [ { - + "enable":false, + "type":"timepicker" } ], "style":"dark", @@ -322,11 +309,7 @@ Content-Type: application/json "title": "Production Overview", "tags": [ "templated" ], "timezone": "browser", - "rows": [ - { - } - ], - "schemaVersion": 6, + "schemaVersion": 16, "version": 0 }, "meta": { diff --git a/docs/sources/http_api/index.md b/docs/sources/http_api/index.md index 81b3c907c7a..2a74917a9fd 100644 --- a/docs/sources/http_api/index.md +++ b/docs/sources/http_api/index.md @@ -31,6 +31,7 @@ dashboards, creating users and updating data sources. * [Annotations API]({{< relref "http_api/annotations.md" >}}) * [Alerting API]({{< relref "http_api/alerting.md" >}}) * [User API]({{< relref "http_api/user.md" >}}) +* [Team API]({{< relref "http_api/team.md" >}}) * [Admin API]({{< relref "http_api/admin.md" >}}) * [Preferences API]({{< relref "http_api/preferences.md" >}}) * [Other API]({{< relref "http_api/other.md" >}}) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 66072a98f84..6169280b798 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -234,7 +234,12 @@ The maximum number of connections in the idle connection pool. ### max_open_conn The maximum number of open connections to the database. +### conn_max_lifetime + +Sets the maximum amount of time a connection may be reused. The default is 14400 (which means 14400 seconds or 4 hours). For MySQL, this setting should be shorter than the [`wait_timeout`](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_wait_timeout) variable. + ### log_queries + Set to `true` to log the sql calls and execution times.
diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 515a598d3cd..d4d3b05343a 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -15,7 +15,7 @@ weight = 1 Description | Download ------------ | ------------- -Stable for Debian-based Linux | [grafana_5.0.0_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0_amd64.deb) +Stable for Debian-based Linux | [grafana_5.0.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.3_amd64.deb) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -24,9 +24,9 @@ installation. ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.3_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.0.0_amd64.deb +sudo dpkg -i grafana_5.0.3_amd64.deb ``` ## APT Repository diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index 2b2fb22e9fa..3ca5ba06638 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -83,7 +83,7 @@ $ docker run \ -d \ -p 3000:3000 \ --name grafana \ - grafana/grafana:4.5.2 + grafana/grafana:5.0.2 ``` ## Configuring AWS Credentials for CloudWatch Support diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 4ed59093903..b0405eb6533 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -15,7 +15,7 @@ weight = 2 Description | Download ------------ | ------------- -Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.0 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-1.x86_64.rpm) +Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3-1.x86_64.rpm) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing @@ -26,7 +26,7 @@ installation. You can install Grafana using Yum directly. ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-1.x86_64.rpm +$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3-1.x86_64.rpm ``` Or install manually using `rpm`. @@ -34,15 +34,15 @@ Or install manually using `rpm`. #### On CentOS / Fedora / Redhat: ```bash -$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-1.x86_64.rpm +$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3-1.x86_64.rpm $ sudo yum install initscripts fontconfig -$ sudo rpm -Uvh grafana-5.0.0-1.x86_64.rpm +$ sudo rpm -Uvh grafana-5.0.3-1.x86_64.rpm ``` #### On OpenSuse: ```bash -$ sudo rpm -i --nodeps grafana-5.0.0-1.x86_64.rpm +$ sudo rpm -i --nodeps grafana-5.0.3-1.x86_64.rpm ``` ## Install via YUM Repository diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index d4c233df996..2dac13a6322 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -13,7 +13,7 @@ weight = 3 Description | Download ------------ | ------------- -Latest stable package for Windows | [grafana-5.0.0.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0.windows-x64.zip) +Latest stable package for Windows | [grafana-5.0.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3.windows-x64.zip) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index 3a15b4ed7d1..f9e16e26610 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -1,6 +1,6 @@ +++ title = "Variables" -keywords = ["grafana", "templating", "documentation", "guide"] +keywords = ["grafana", "templating", "documentation", "guide", "template", "variable"] type = "docs" [menu.docs] name = "Variables" @@ -80,6 +80,73 @@ Option | Description *Regex* | Regex to filter or capture specific parts of the names return by your data source query. Optional. *Sort* | Define sort order for options in dropdown. **Disabled** means that the order of options returned by your data source query will be used. +#### Using regex to filter/modify values in the Variable dropdown + +Using the Regex Query Option, you filter the list of options returned by the Variable query or modify the options returned. + +Examples of filtering on the following list of options: + +```text +backend_01 +backend_02 +backend_03 +backend_04 +``` + +##### Filter so that only the options that end with `01` or `02` are returned: + +Regex: + +```regex +/.*[01|02]/ +``` + +Result: + +```text +backend_01 +backend_02 +``` + +##### Filter and modify the options using a regex capture group to return part of the text: + +Regex: + +```regex +/.*(01|02)/ +``` + +Result: + +```text +01 +02 +``` + +#### Filter and modify - Prometheus Example + +List of options: + +```text +up{instance="demo.robustperception.io:9090",job="prometheus"} 1 1521630638000 +up{instance="demo.robustperception.io:9093",job="alertmanager"} 1 1521630638000 +up{instance="demo.robustperception.io:9100",job="node"} 1 1521630638000 +``` + +Regex: + +```regex +/.*instance="([^"]*).*/ +``` + +Result: + +```text +demo.robustperception.io:9090 +demo.robustperception.io:9093 +demo.robustperception.io:9100 +``` + ### Query expressions The query expressions are different for each data source. diff --git a/docs/sources/tutorials/ha_setup.md b/docs/sources/tutorials/ha_setup.md index 9dd8aac4618..9ae2989f6e6 100644 --- a/docs/sources/tutorials/ha_setup.md +++ b/docs/sources/tutorials/ha_setup.md @@ -9,30 +9,38 @@ weight = 10 # How to setup Grafana for high availability -> Alerting does not support high availability yet. - Setting up Grafana for high availability is fairly simple. It comes down to two things: - * Use a shared database for multiple grafana instances. - * Consider how user sessions are stored. + 1. Use a shared database for storing dashboard, users, and other persistent data + 2. Decide how to store session data. + +
+ +
## Configure multiple servers to use the same database -First you need to do is to setup mysql or postgres on another server and configure Grafana to use that database. +First, you need to do is to setup MySQL or Postgres on another server and configure Grafana to use that database. You can find the configuration for doing that in the [[database]]({{< relref "configuration.md" >}}#database) section in the grafana config. -Grafana will now persist all long term data in the database. -It also worth considering how to setup the database for high availability but thats outside the scope of this guide. +Grafana will now persist all long term data in the database. How to configure the database for high availability is out of scope for this guide. We recommend finding an expert on for the database your using. ## User sessions -The second thing to consider is how to deal with user sessions and how to balance the load between servers. -By default Grafana stores user sessions on disk which works fine if you use `sticky sessions` in your load balancer. -Grafana also supports storing the session data in the database, redis or memcache which makes it possible to use round robin in your load balancer. -If you use mysql/postgres for session storage you first need a table to store the session data in. More details about that in [[sessions]]({{< relref "configuration.md" >}}#session) +The second thing to consider is how to deal with user sessions and how to configure your load balancer infront of Grafana. +Grafana support two says of storing session data locally on disk or in a database/cache-server. +If you want to store sessions on disk you can use `sticky sessions` in your load balanacer. If you prefer to store session data in a database/cache-server +you can use any stateless routing strategy in your load balancer (ex round robin or least connections). -For Grafana itself it doesn't really matter if you store your sessions on disk or database/redis/memcache. -But we suggest that you store the session in redis/memcache since it makes it easier to add/remote instances from the group. +### Sticky sessions +Using sticky sessions, all traffic for one user will always be sent to the same server. Which means that session related data can be +stored on disk rather than on a shared database. This is the default behavior for Grafana and if only want multiple servers for fail over this is a good solution since it requires the least amount of work. + +### Stateless sessions +You can also choose to store session data in a Redis/Memcache/Postgres/MySQL which means that the load balancer can send a user to any Grafana server without having to log in on each server. This requires a little bit more work from the operator but enables you to remove/add grafana servers without impacting the user experience. +If you use MySQL/Postgres for session storage, you first need a table to store the session data in. More details about that in [[sessions]]({{< relref "configuration.md" >}}#session) + +For Grafana itself it doesn't really matter if you store the session data on disk or database/redis/memcache. But we recommend using a database/redis/memcache since it makes it easier manage the grafana servers. ## Alerting -Currently alerting supports a limited form of high availability. Since v4.2.0 of Grafana, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but no duplicate alert notifications are sent due to the deduping logic. Proper load balancing of alerts will be introduced in the future. +Currently alerting supports a limited form of high availability. Since v4.2.0, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support distributing the alert rule execution between servers. That might be added in the future but right now prefer to keep it simple. diff --git a/emails/templates/layouts/default.html b/emails/templates/layouts/default.html index 07eb32874c7..ca54acdd206 100644 --- a/emails/templates/layouts/default.html +++ b/emails/templates/layouts/default.html @@ -143,7 +143,7 @@ td[class="stack-column-center"] {

Sent by Grafana v[[.BuildVersion]] -
© 2016 Grafana and raintank +
© 2018 Grafana Labs

diff --git a/package.json b/package.json index df4359ef5c1..6dcfc16b82b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "5.0.1-pre1", + "version": "5.1.0-pre1", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" @@ -118,7 +118,7 @@ "prettier --write", "git add" ], - "*.go": [ + "*pkg/**/*.go": [ "gofmt -w -s", "git add" ] diff --git a/packaging/publish/publish_both.sh b/packaging/publish/publish_both.sh index 9736cbddd6c..cbe3918bf38 100755 --- a/packaging/publish/publish_both.sh +++ b/packaging/publish/publish_both.sh @@ -1,5 +1,5 @@ #! /usr/bin/env bash -version=4.6.3 +version=5.0.2 wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${version}_amd64.deb diff --git a/pkg/api/admin.go b/pkg/api/admin.go index d7f5a240416..52d271ce69b 100644 --- a/pkg/api/admin.go +++ b/pkg/api/admin.go @@ -1,15 +1,15 @@ package api import ( + "regexp" "strings" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) -func AdminGetSettings(c *middleware.Context) { +func AdminGetSettings(c *m.ReqContext) { settings := make(map[string]interface{}) for _, section := range setting.Cfg.Sections() { @@ -22,6 +22,14 @@ func AdminGetSettings(c *middleware.Context) { if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config")) { value = "************" } + if strings.Contains(keyName, "url") { + var rgx = regexp.MustCompile(`.*:\/\/([^:]*):([^@]*)@.*?$`) + var subs = rgx.FindAllSubmatch([]byte(value), -1) + if subs != nil && len(subs[0]) == 3 { + value = strings.Replace(value, string(subs[0][1]), "******", 1) + value = strings.Replace(value, string(subs[0][2]), "******", 1) + } + } jsonSec[keyName] = value } @@ -30,7 +38,7 @@ func AdminGetSettings(c *middleware.Context) { c.JSON(200, settings) } -func AdminGetStats(c *middleware.Context) { +func AdminGetStats(c *m.ReqContext) { statsQuery := m.GetAdminStatsQuery{} diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 1868c589673..dc3d390dda9 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -4,12 +4,11 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" ) -func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { +func AdminCreateUser(c *m.ReqContext, form dtos.AdminCreateUserForm) { cmd := m.CreateUserCommand{ Login: form.Login, Email: form.Email, @@ -47,15 +46,15 @@ func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { c.JSON(200, result) } -func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPasswordForm) { - userId := c.ParamsInt64(":id") +func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordForm) { + userID := c.ParamsInt64(":id") if len(form.Password) < 4 { c.JsonApiErr(400, "New password too short", nil) return } - userQuery := m.GetUserByIdQuery{Id: userId} + userQuery := m.GetUserByIdQuery{Id: userID} if err := bus.Dispatch(&userQuery); err != nil { c.JsonApiErr(500, "Could not read user from database", err) @@ -65,7 +64,7 @@ func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPas passwordHashed := util.EncodePassword(form.Password, userQuery.Result.Salt) cmd := m.ChangeUserPasswordCommand{ - UserId: userId, + UserId: userID, NewPassword: passwordHashed, } @@ -77,11 +76,11 @@ func AdminUpdateUserPassword(c *middleware.Context, form dtos.AdminUpdateUserPas c.JsonOK("User password updated") } -func AdminUpdateUserPermissions(c *middleware.Context, form dtos.AdminUpdateUserPermissionsForm) { - userId := c.ParamsInt64(":id") +func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) { + userID := c.ParamsInt64(":id") cmd := m.UpdateUserPermissionsCommand{ - UserId: userId, + UserId: userID, IsGrafanaAdmin: form.IsGrafanaAdmin, } @@ -93,10 +92,10 @@ func AdminUpdateUserPermissions(c *middleware.Context, form dtos.AdminUpdateUser c.JsonOK("User permissions updated") } -func AdminDeleteUser(c *middleware.Context) { - userId := c.ParamsInt64(":id") +func AdminDeleteUser(c *m.ReqContext) { + userID := c.ParamsInt64(":id") - cmd := m.DeleteUserCommand{UserId: userId} + cmd := m.DeleteUserCommand{UserId: userID} if err := bus.Dispatch(&cmd); err != nil { c.JsonApiErr(500, "Failed to delete user", err) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 08edf54748b..803823f6a94 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -5,15 +5,14 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/guardian" ) -func ValidateOrgAlert(c *middleware.Context) { +func ValidateOrgAlert(c *m.ReqContext) { id := c.ParamsInt64(":alertId") - query := models.GetAlertByIdQuery{Id: id} + query := m.GetAlertByIdQuery{Id: id} if err := bus.Dispatch(&query); err != nil { c.JsonApiErr(404, "Alert not found", nil) @@ -26,14 +25,14 @@ func ValidateOrgAlert(c *middleware.Context) { } } -func GetAlertStatesForDashboard(c *middleware.Context) Response { - dashboardId := c.QueryInt64("dashboardId") +func GetAlertStatesForDashboard(c *m.ReqContext) Response { + dashboardID := c.QueryInt64("dashboardId") - if dashboardId == 0 { + if dashboardID == 0 { return ApiError(400, "Missing query parameter dashboardId", nil) } - query := models.GetAlertStatesForDashboardQuery{ + query := m.GetAlertStatesForDashboardQuery{ OrgId: c.OrgId, DashboardId: c.QueryInt64("dashboardId"), } @@ -46,8 +45,8 @@ func GetAlertStatesForDashboard(c *middleware.Context) Response { } // GET /api/alerts -func GetAlerts(c *middleware.Context) Response { - query := models.GetAlertsQuery{ +func GetAlerts(c *m.ReqContext) Response { + query := m.GetAlertsQuery{ OrgId: c.OrgId, DashboardId: c.QueryInt64("dashboardId"), PanelId: c.QueryInt64("panelId"), @@ -65,14 +64,14 @@ func GetAlerts(c *middleware.Context) Response { } for _, alert := range query.Result { - alert.Url = models.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug) + alert.Url = m.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug) } return Json(200, query.Result) } // POST /api/alerts/test -func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response { +func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response { if _, idErr := dto.Dashboard.Get("id").Int64(); idErr != nil { return ApiError(400, "The dashboard needs to be saved at least once before you can test an alert rule", nil) } @@ -114,9 +113,9 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response { } // GET /api/alerts/:id -func GetAlert(c *middleware.Context) Response { +func GetAlert(c *m.ReqContext) Response { id := c.ParamsInt64(":alertId") - query := models.GetAlertByIdQuery{Id: id} + query := m.GetAlertByIdQuery{Id: id} if err := bus.Dispatch(&query); err != nil { return ApiError(500, "List alerts failed", err) @@ -125,12 +124,12 @@ func GetAlert(c *middleware.Context) Response { return Json(200, &query.Result) } -func GetAlertNotifiers(c *middleware.Context) Response { +func GetAlertNotifiers(c *m.ReqContext) Response { return Json(200, alerting.GetNotifiers()) } -func GetAlertNotifications(c *middleware.Context) Response { - query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId} +func GetAlertNotifications(c *m.ReqContext) Response { + query := &m.GetAllAlertNotificationsQuery{OrgId: c.OrgId} if err := bus.Dispatch(query); err != nil { return ApiError(500, "Failed to get alert notifications", err) @@ -152,8 +151,8 @@ func GetAlertNotifications(c *middleware.Context) Response { return Json(200, result) } -func GetAlertNotificationById(c *middleware.Context) Response { - query := &models.GetAlertNotificationsQuery{ +func GetAlertNotificationByID(c *m.ReqContext) Response { + query := &m.GetAlertNotificationsQuery{ OrgId: c.OrgId, Id: c.ParamsInt64("notificationId"), } @@ -165,7 +164,7 @@ func GetAlertNotificationById(c *middleware.Context) Response { return Json(200, query.Result) } -func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotificationCommand) Response { +func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response { cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { @@ -175,7 +174,7 @@ func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotifi return Json(200, cmd.Result) } -func UpdateAlertNotification(c *middleware.Context, cmd models.UpdateAlertNotificationCommand) Response { +func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response { cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { @@ -185,8 +184,8 @@ func UpdateAlertNotification(c *middleware.Context, cmd models.UpdateAlertNotifi return Json(200, cmd.Result) } -func DeleteAlertNotification(c *middleware.Context) Response { - cmd := models.DeleteAlertNotificationCommand{ +func DeleteAlertNotification(c *m.ReqContext) Response { + cmd := m.DeleteAlertNotificationCommand{ OrgId: c.OrgId, Id: c.ParamsInt64("notificationId"), } @@ -199,7 +198,7 @@ func DeleteAlertNotification(c *middleware.Context) Response { } //POST /api/alert-notifications/test -func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) Response { +func NotificationTest(c *m.ReqContext, dto dtos.NotificationTestCommand) Response { cmd := &alerting.NotificationTestCommand{ Name: dto.Name, Type: dto.Type, @@ -207,7 +206,7 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R } if err := bus.Dispatch(cmd); err != nil { - if err == models.ErrSmtpNotEnabled { + if err == m.ErrSmtpNotEnabled { return ApiError(412, err.Error(), err) } return ApiError(500, "Failed to send alert notifications", err) @@ -217,10 +216,10 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R } //POST /api/alerts/:alertId/pause -func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response { +func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response { alertId := c.ParamsInt64("alertId") - query := models.GetAlertByIdQuery{Id: alertId} + query := m.GetAlertByIdQuery{Id: alertId} if err := bus.Dispatch(&query); err != nil { return ApiError(500, "Get Alert failed", err) @@ -235,7 +234,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response { return ApiError(403, "Access denied to this dashboard and alert", nil) } - cmd := models.PauseAlertCommand{ + cmd := m.PauseAlertCommand{ OrgId: c.OrgId, AlertIds: []int64{alertId}, Paused: dto.Paused, @@ -245,10 +244,10 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response { return ApiError(500, "", err) } - var response models.AlertStateType = models.AlertStatePending + var response m.AlertStateType = m.AlertStatePending pausedState := "un-paused" if cmd.Paused { - response = models.AlertStatePaused + response = m.AlertStatePaused pausedState = "paused" } @@ -262,8 +261,8 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response { } //POST /api/admin/pause-all-alerts -func PauseAllAlerts(c *middleware.Context, dto dtos.PauseAllAlertsCommand) Response { - updateCmd := models.PauseAllAlertCommand{ +func PauseAllAlerts(c *m.ReqContext, dto dtos.PauseAllAlertsCommand) Response { + updateCmd := m.PauseAllAlertCommand{ Paused: dto.Paused, } @@ -271,10 +270,10 @@ func PauseAllAlerts(c *middleware.Context, dto dtos.PauseAllAlertsCommand) Respo return ApiError(500, "Failed to pause alerts", err) } - var response models.AlertStateType = models.AlertStatePending + var response m.AlertStateType = m.AlertStatePending pausedState := "un paused" if updateCmd.Paused { - response = models.AlertStatePaused + response = m.AlertStatePaused pausedState = "paused" } diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index 6b030053e22..9302ef7beca 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" @@ -81,7 +80,7 @@ func postAlertScenario(desc string, url string, routePattern string, role m.Role defer bus.ClearBusHandlers() sc := setupScenarioContext(url) - sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.defaultHandler = wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.UserId = TestUserID sc.context.OrgId = TestOrgID diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index cb1a15e69eb..b4e328793cc 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -6,14 +6,13 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/util" ) -func GetAnnotations(c *middleware.Context) Response { +func GetAnnotations(c *m.ReqContext) Response { query := &annotations.ItemQuery{ From: c.QueryInt64("from") / 1000, @@ -52,8 +51,8 @@ func (e *CreateAnnotationError) Error() string { return e.message } -func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response { - if canSave, err := canSaveByDashboardId(c, cmd.DashboardId); err != nil || !canSave { +func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response { + if canSave, err := canSaveByDashboardID(c, cmd.DashboardId); err != nil || !canSave { return dashboardGuardianResponse(err) } @@ -125,7 +124,7 @@ func formatGraphiteAnnotation(what string, data string) string { return text } -func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotationsCmd) Response { +func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd) Response { repo := annotations.GetRepository() if cmd.What == "" { @@ -179,19 +178,19 @@ func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotati }) } -func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Response { - annotationId := c.ParamsInt64(":annotationId") +func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response { + annotationID := c.ParamsInt64(":annotationId") repo := annotations.GetRepository() - if resp := canSave(c, repo, annotationId); resp != nil { + if resp := canSave(c, repo, annotationID); resp != nil { return resp } item := annotations.Item{ OrgId: c.OrgId, UserId: c.UserId, - Id: annotationId, + Id: annotationID, Epoch: cmd.Time / 1000, Text: cmd.Text, Tags: cmd.Tags, @@ -218,7 +217,7 @@ func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Resp return ApiSuccess("Annotation updated") } -func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response { +func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response { repo := annotations.GetRepository() err := repo.Delete(&annotations.DeleteParams{ @@ -234,7 +233,7 @@ func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Res return ApiSuccess("Annotations deleted") } -func DeleteAnnotationById(c *middleware.Context) Response { +func DeleteAnnotationById(c *m.ReqContext) Response { repo := annotations.GetRepository() annotationId := c.ParamsInt64(":annotationId") @@ -253,16 +252,16 @@ func DeleteAnnotationById(c *middleware.Context) Response { return ApiSuccess("Annotation deleted") } -func DeleteAnnotationRegion(c *middleware.Context) Response { +func DeleteAnnotationRegion(c *m.ReqContext) Response { repo := annotations.GetRepository() - regionId := c.ParamsInt64(":regionId") + regionID := c.ParamsInt64(":regionId") - if resp := canSave(c, repo, regionId); resp != nil { + if resp := canSave(c, repo, regionID); resp != nil { return resp } err := repo.Delete(&annotations.DeleteParams{ - RegionId: regionId, + RegionId: regionID, }) if err != nil { @@ -272,13 +271,13 @@ func DeleteAnnotationRegion(c *middleware.Context) Response { return ApiSuccess("Annotation region deleted") } -func canSaveByDashboardId(c *middleware.Context, dashboardId int64) (bool, error) { - if dashboardId == 0 && !c.SignedInUser.HasRole(m.ROLE_EDITOR) { +func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) { + if dashboardID == 0 && !c.SignedInUser.HasRole(m.ROLE_EDITOR) { return false, nil } - if dashboardId > 0 { - guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser) + if dashboardID > 0 { + guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser) if canEdit, err := guardian.CanEdit(); err != nil || !canEdit { return false, err } @@ -287,32 +286,32 @@ func canSaveByDashboardId(c *middleware.Context, dashboardId int64) (bool, error return true, nil } -func canSave(c *middleware.Context, repo annotations.Repository, annotationId int64) Response { +func canSave(c *m.ReqContext, repo annotations.Repository, annotationId int64) Response { items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationId, OrgId: c.OrgId}) if err != nil || len(items) == 0 { return ApiError(500, "Could not find annotation to update", err) } - dashboardId := items[0].DashboardId + dashboardID := items[0].DashboardId - if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave { + if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave { return dashboardGuardianResponse(err) } return nil } -func canSaveByRegionId(c *middleware.Context, repo annotations.Repository, regionId int64) Response { - items, err := repo.Find(&annotations.ItemQuery{RegionId: regionId, OrgId: c.OrgId}) +func canSaveByRegionID(c *m.ReqContext, repo annotations.Repository, regionID int64) Response { + items, err := repo.Find(&annotations.ItemQuery{RegionId: regionID, OrgId: c.OrgId}) if err != nil || len(items) == 0 { return ApiError(500, "Could not find annotation to update", err) } - dashboardId := items[0].DashboardId + dashboardID := items[0].DashboardId - if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave { + if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave { return dashboardGuardianResponse(err) } diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 480962d8826..7c298550673 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" @@ -200,7 +199,7 @@ func postAnnotationScenario(desc string, url string, routePattern string, role m defer bus.ClearBusHandlers() sc := setupScenarioContext(url) - sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.defaultHandler = wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.UserId = TestUserID sc.context.OrgId = TestOrgID @@ -223,7 +222,7 @@ func putAnnotationScenario(desc string, url string, routePattern string, role m. defer bus.ClearBusHandlers() sc := setupScenarioContext(url) - sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.defaultHandler = wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.UserId = TestUserID sc.context.OrgId = TestOrgID diff --git a/pkg/api/api.go b/pkg/api/api.go index 1b7e30e34ad..22d5d773d2a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -15,7 +15,7 @@ func (hs *HttpServer) registerRoutes() { reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}) reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN) reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN) - redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardUrl() + redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardSoloUrl := middleware.RedirectFromLegacyDashboardSoloUrl() quota := middleware.Quota bind := binding.Bind @@ -66,6 +66,7 @@ func (hs *HttpServer) registerRoutes() { 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) @@ -109,7 +110,7 @@ func (hs *HttpServer) registerRoutes() { r.Get("/api/snapshots-delete/:key", reqEditorRole, wrap(DeleteDashboardSnapshot)) // api renew session based on remember cookie - r.Get("/api/login/ping", quota("session"), LoginApiPing) + r.Get("/api/login/ping", quota("session"), LoginAPIPing) // authed api r.Group("/api", func(apiRoute RouteRegister) { @@ -138,7 +139,7 @@ func (hs *HttpServer) registerRoutes() { apiRoute.Group("/users", func(usersRoute RouteRegister) { usersRoute.Get("/", wrap(SearchUsers)) usersRoute.Get("/search", wrap(SearchUsersWithPaging)) - usersRoute.Get("/:id", wrap(GetUserById)) + usersRoute.Get("/:id", wrap(GetUserByID)) usersRoute.Get("/:id/orgs", wrap(GetUserOrgList)) // query parameters /users/lookup?loginOrEmail=admin@example.com usersRoute.Get("/lookup", wrap(GetUserByLoginOrEmail)) @@ -148,13 +149,13 @@ func (hs *HttpServer) registerRoutes() { // team (admin permission required) apiRoute.Group("/teams", func(teamsRoute RouteRegister) { - teamsRoute.Get("/:teamId", wrap(GetTeamById)) + teamsRoute.Get("/:teamId", wrap(GetTeamByID)) teamsRoute.Get("/search", wrap(SearchTeams)) - teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam)) + teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam)) teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam)) - teamsRoute.Delete("/:teamId", wrap(DeleteTeamById)) + teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID)) teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers)) - teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) + teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember)) }, reqOrgAdmin) @@ -191,10 +192,10 @@ func (hs *HttpServer) registerRoutes() { // orgs (admin routes) apiRoute.Group("/orgs/:orgId", func(orgsRoute RouteRegister) { - orgsRoute.Get("/", wrap(GetOrgById)) + orgsRoute.Get("/", wrap(GetOrgByID)) orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrg)) orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddress)) - orgsRoute.Delete("/", wrap(DeleteOrgById)) + orgsRoute.Delete("/", wrap(DeleteOrgByID)) orgsRoute.Get("/users", wrap(GetOrgUsers)) orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser)) orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser)) @@ -210,9 +211,9 @@ func (hs *HttpServer) registerRoutes() { // auth api keys apiRoute.Group("/auth/keys", func(keysRoute RouteRegister) { - keysRoute.Get("/", wrap(GetApiKeys)) - keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey)) - keysRoute.Delete("/:id", wrap(DeleteApiKey)) + keysRoute.Get("/", wrap(GetAPIKeys)) + keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddAPIKey)) + keysRoute.Delete("/:id", wrap(DeleteAPIKey)) }, reqOrgAdmin) // Preferences @@ -225,16 +226,16 @@ 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)) + apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingByID)) apiRoute.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown)) apiRoute.Group("/plugins", func(pluginRoute RouteRegister) { @@ -249,11 +250,11 @@ func (hs *HttpServer) registerRoutes() { // Folders apiRoute.Group("/folders", func(folderRoute RouteRegister) { folderRoute.Get("/", wrap(GetFolders)) - folderRoute.Get("/id/:id", wrap(GetFolderById)) + folderRoute.Get("/id/:id", wrap(GetFolderByID)) folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder)) folderRoute.Group("/:uid", func(folderUidRoute RouteRegister) { - folderUidRoute.Get("/", wrap(GetFolderByUid)) + folderUidRoute.Get("/", wrap(GetFolderByUID)) folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder)) folderUidRoute.Delete("/", wrap(DeleteFolder)) @@ -267,7 +268,7 @@ func (hs *HttpServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) { dashboardRoute.Get("/uid/:uid", wrap(GetDashboard)) - dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUid)) + dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUID)) dashboardRoute.Get("/db/:slug", wrap(GetDashboard)) dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard)) @@ -313,7 +314,7 @@ func (hs *HttpServer) registerRoutes() { // metrics apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics)) apiRoute.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios)) - apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSqlTestData)) + apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSQLTestData)) apiRoute.Get("/tsdb/testdata/random-walk", wrap(GetTestDataRandomWalk)) apiRoute.Group("/alerts", func(alertsRoute RouteRegister) { @@ -331,7 +332,7 @@ func (hs *HttpServer) registerRoutes() { alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest)) alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification)) alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification)) - alertNotifications.Get("/:notificationId", wrap(GetAlertNotificationById)) + alertNotifications.Get("/:notificationId", wrap(GetAlertNotificationByID)) alertNotifications.Delete("/:notificationId", wrap(DeleteAlertNotification)) }, reqEditorRole) diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index b2097104aba..7195c7453f6 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -4,11 +4,10 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/apikeygen" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" ) -func GetApiKeys(c *middleware.Context) Response { +func GetAPIKeys(c *m.ReqContext) Response { query := m.GetApiKeysQuery{OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { @@ -27,7 +26,7 @@ func GetApiKeys(c *middleware.Context) Response { return Json(200, result) } -func DeleteApiKey(c *middleware.Context) Response { +func DeleteAPIKey(c *m.ReqContext) Response { id := c.ParamsInt64(":id") cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId} @@ -40,7 +39,7 @@ func DeleteApiKey(c *middleware.Context) Response { return ApiSuccess("API key deleted") } -func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) Response { +func AddAPIKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response { if !cmd.Role.IsValid() { return ApiError(400, "Invalid role specified", nil) } diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index 0440c880979..0b7dcd32ce3 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.go @@ -55,11 +55,11 @@ func InitAppPluginRoutes(r *macaron.Macaron) { } } -func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler { - return func(c *middleware.Context) { +func AppPluginRoute(route *plugins.AppPluginRoute, appID string) macaron.Handler { + return func(c *m.ReqContext) { path := c.Params("*") - proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId) + proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID) proxy.Transport = pluginProxyTransport proxy.ServeHTTP(c.Resp, c.Req.Request) } diff --git a/pkg/api/common.go b/pkg/api/common.go index bd1c8be477d..370f78f8b1d 100644 --- a/pkg/api/common.go +++ b/pkg/api/common.go @@ -4,7 +4,7 @@ import ( "encoding/json" "net/http" - "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "gopkg.in/macaron.v1" ) @@ -19,7 +19,7 @@ var ( ) type Response interface { - WriteTo(ctx *middleware.Context) + WriteTo(ctx *m.ReqContext) } type NormalResponse struct { @@ -32,7 +32,7 @@ type NormalResponse struct { func wrap(action interface{}) macaron.Handler { - return func(c *middleware.Context) { + return func(c *m.ReqContext) { var res Response val, err := c.Invoke(action) if err == nil && val != nil && len(val) > 0 { @@ -45,7 +45,7 @@ func wrap(action interface{}) macaron.Handler { } } -func (r *NormalResponse) WriteTo(ctx *middleware.Context) { +func (r *NormalResponse) WriteTo(ctx *m.ReqContext) { if r.err != nil { ctx.Logger.Error(r.errMessage, "error", r.err) } diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 33fc1688603..a4a547d8bbf 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -8,22 +8,22 @@ import ( "github.com/go-macaron/session" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" - macaron "gopkg.in/macaron.v1" + m "github.com/grafana/grafana/pkg/models" + "gopkg.in/macaron.v1" . "github.com/smartystreets/goconvey/convey" ) func loggedInUserScenario(desc string, url string, fn scenarioFunc) { - loggedInUserScenarioWithRole(desc, "GET", url, url, models.ROLE_EDITOR, fn) + loggedInUserScenarioWithRole(desc, "GET", url, url, m.ROLE_EDITOR, fn) } -func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) { +func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role m.RoleType, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() sc := setupScenarioContext(url) - sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.defaultHandler = wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.UserId = TestUserID sc.context.OrgId = TestOrgID @@ -71,7 +71,7 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map type scenarioContext struct { m *macaron.Macaron - context *middleware.Context + context *m.ReqContext resp *httptest.ResponseRecorder handlerFunc handlerFunc defaultHandler macaron.Handler @@ -84,7 +84,7 @@ func (sc *scenarioContext) exec() { } type scenarioFunc func(c *scenarioContext) -type handlerFunc func(c *middleware.Context) Response +type handlerFunc func(c *m.ReqContext) Response func setupScenarioContext(url string) *scenarioContext { sc := &scenarioContext{ @@ -99,7 +99,7 @@ func setupScenarioContext(url string) *scenarioContext { })) sc.m.Use(middleware.GetContextHandler()) - sc.m.Use(middleware.Sessioner(&session.Options{})) + sc.m.Use(middleware.Sessioner(&session.Options{}, 0)) return sc } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 53174075c25..25c89238933 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -14,20 +14,20 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) -func isDashboardStarredByUser(c *middleware.Context, dashId int64) (bool, error) { +func isDashboardStarredByUser(c *m.ReqContext, dashID int64) (bool, error) { if !c.IsSignedIn { return false, nil } - query := m.IsStarredByUserQuery{UserId: c.UserId, DashboardId: dashId} + query := m.IsStarredByUserQuery{UserId: c.UserId, DashboardId: dashID} if err := bus.Dispatch(&query); err != nil { return false, err } @@ -43,7 +43,7 @@ func dashboardGuardianResponse(err error) Response { return ApiError(403, "Access denied to this dashboard", nil) } -func GetDashboard(c *middleware.Context) Response { +func GetDashboard(c *m.ReqContext) Response { dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid")) if rsp != nil { return rsp @@ -114,24 +114,22 @@ func GetDashboard(c *middleware.Context) Response { return Json(200, dto) } -func getUserLogin(userId int64) string { - query := m.GetUserByIdQuery{Id: userId} +func getUserLogin(userID int64) string { + query := m.GetUserByIdQuery{Id: userID} err := bus.Dispatch(&query) if err != nil { return "Anonymous" - } else { - user := query.Result - return user.Login } + return query.Result.Login } -func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dashboard, Response) { +func getDashboardHelper(orgID int64, slug string, id int64, uid string) (*m.Dashboard, Response) { var query m.GetDashboardQuery if len(uid) > 0 { - query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgId} + query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgID} } else { - query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId} + query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgID} } if err := bus.Dispatch(&query); err != nil { @@ -141,7 +139,7 @@ func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dash return query.Result, nil } -func DeleteDashboard(c *middleware.Context) Response { +func DeleteDashboard(c *m.ReqContext) Response { query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")} if err := bus.Dispatch(&query); err != nil { @@ -173,7 +171,7 @@ func DeleteDashboard(c *middleware.Context) Response { }) } -func DeleteDashboardByUid(c *middleware.Context) Response { +func DeleteDashboardByUID(c *m.ReqContext) Response { dash, rsp := getDashboardHelper(c.OrgId, "", 0, c.Params(":uid")) if rsp != nil { return rsp @@ -195,14 +193,14 @@ func DeleteDashboardByUid(c *middleware.Context) Response { }) } -func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { +func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response { cmd.OrgId = c.OrgId cmd.UserId = c.UserId dash := cmd.GetDashboardModel() if dash.Id == 0 && dash.Uid == "" { - limitReached, err := middleware.QuotaReached(c, "dashboard") + limitReached, err := quota.QuotaReached(c, "dashboard") if err != nil { return ApiError(500, "failed to get quota", err) } @@ -278,7 +276,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { }) } -func GetHomeDashboard(c *middleware.Context) Response { +func GetHomeDashboard(c *m.ReqContext) Response { prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId} if err := bus.Dispatch(&prefsQuery); err != nil { return ApiError(500, "Failed to get preferences", err) @@ -291,9 +289,8 @@ func GetHomeDashboard(c *middleware.Context) Response { url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug) dashRedirect := dtos.DashboardRedirect{RedirectUri: url} return Json(200, &dashRedirect) - } else { - log.Warn("Failed to get slug from database, %s", err.Error()) } + log.Warn("Failed to get slug from database, %s", err.Error()) } filePath := path.Join(setting.StaticRootPath, "dashboards/home.json") @@ -338,23 +335,23 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) { } // GetDashboardVersions returns all dashboard versions as JSON -func GetDashboardVersions(c *middleware.Context) Response { - dashId := c.ParamsInt64(":dashboardId") +func GetDashboardVersions(c *m.ReqContext) Response { + dashID := c.ParamsInt64(":dashboardId") - guardian := guardian.New(dashId, c.OrgId, c.SignedInUser) + guardian := guardian.New(dashID, c.OrgId, c.SignedInUser) if canSave, err := guardian.CanSave(); err != nil || !canSave { return dashboardGuardianResponse(err) } query := m.GetDashboardVersionsQuery{ OrgId: c.OrgId, - DashboardId: dashId, + DashboardId: dashID, Limit: c.QueryInt("limit"), Start: c.QueryInt("start"), } if err := bus.Dispatch(&query); err != nil { - return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashId), err) + return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashID), err) } for _, version := range query.Result { @@ -377,22 +374,22 @@ func GetDashboardVersions(c *middleware.Context) Response { } // GetDashboardVersion returns the dashboard version with the given ID. -func GetDashboardVersion(c *middleware.Context) Response { - dashId := c.ParamsInt64(":dashboardId") +func GetDashboardVersion(c *m.ReqContext) Response { + dashID := c.ParamsInt64(":dashboardId") - guardian := guardian.New(dashId, c.OrgId, c.SignedInUser) + guardian := guardian.New(dashID, c.OrgId, c.SignedInUser) if canSave, err := guardian.CanSave(); err != nil || !canSave { return dashboardGuardianResponse(err) } query := m.GetDashboardVersionQuery{ OrgId: c.OrgId, - DashboardId: dashId, + DashboardId: dashID, Version: c.ParamsInt(":id"), } if err := bus.Dispatch(&query); err != nil { - return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashId), err) + return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err) } creator := "Anonymous" @@ -409,7 +406,7 @@ func GetDashboardVersion(c *middleware.Context) Response { } // POST /api/dashboards/calculate-diff performs diffs on two dashboards -func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiffOptions) Response { +func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOptions) Response { guardianBase := guardian.New(apiOptions.Base.DashboardId, c.OrgId, c.SignedInUser) if canSave, err := guardianBase.CanSave(); err != nil || !canSave { @@ -454,7 +451,7 @@ func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiff } // RestoreDashboardVersion restores a dashboard to the given version. -func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response { +func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response { dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "") if rsp != nil { return rsp @@ -484,7 +481,7 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard return PostDashboard(c, saveCmd) } -func GetDashboardTags(c *middleware.Context) { +func GetDashboardTags(c *m.ReqContext) { query := m.GetDashboardTagsQuery{OrgId: c.OrgId} err := bus.Dispatch(&query) if err != nil { diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 419825644c8..c852033829a 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -5,20 +5,19 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/guardian" ) -func GetDashboardPermissionList(c *middleware.Context) Response { - dashId := c.ParamsInt64(":dashboardId") +func GetDashboardPermissionList(c *m.ReqContext) Response { + dashID := c.ParamsInt64(":dashboardId") - _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") + _, rsp := getDashboardHelper(c.OrgId, "", dashID, "") if rsp != nil { return rsp } - g := guardian.New(dashId, c.OrgId, c.SignedInUser) + g := guardian.New(dashID, c.OrgId, c.SignedInUser) if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { return dashboardGuardianResponse(err) @@ -38,26 +37,26 @@ func GetDashboardPermissionList(c *middleware.Context) Response { return Json(200, acl) } -func UpdateDashboardPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response { - dashId := c.ParamsInt64(":dashboardId") +func UpdateDashboardPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response { + dashID := c.ParamsInt64(":dashboardId") - _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") + _, rsp := getDashboardHelper(c.OrgId, "", dashID, "") if rsp != nil { return rsp } - g := guardian.New(dashId, c.OrgId, c.SignedInUser) + g := guardian.New(dashID, c.OrgId, c.SignedInUser) if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { return dashboardGuardianResponse(err) } cmd := m.UpdateDashboardAclCommand{} - cmd.DashboardId = dashId + cmd.DashboardId = dashID for _, item := range apiCmd.Items { cmd.Items = append(cmd.Items, &m.DashboardAcl{ OrgId: c.OrgId, - DashboardId: dashId, + DashboardId: dashID, UserId: item.UserId, TeamId: item.TeamId, Role: item.Role, diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 03231338268..bdf80ef5241 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/guardian" @@ -195,7 +194,7 @@ func updateDashboardPermissionScenario(desc string, url string, routePattern str sc := setupScenarioContext(url) - sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.defaultHandler = wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.OrgId = TestOrgID sc.context.UserId = TestUserID diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index c10302faf32..b74dfe5fd34 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -6,14 +6,13 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) -func GetSharingOptions(c *middleware.Context) { +func GetSharingOptions(c *m.ReqContext) { c.JSON(200, util.DynMap{ "externalSnapshotURL": setting.ExternalSnapshotUrl, "externalSnapshotName": setting.ExternalSnapshotName, @@ -21,7 +20,7 @@ func GetSharingOptions(c *middleware.Context) { }) } -func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) { +func CreateDashboardSnapshot(c *m.ReqContext, cmd m.CreateDashboardSnapshotCommand) { if cmd.Name == "" { cmd.Name = "Unnamed snapshot" } @@ -58,7 +57,7 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho } // GET /api/snapshots/:key -func GetDashboardSnapshot(c *middleware.Context) { +func GetDashboardSnapshot(c *m.ReqContext) { key := c.Params(":key") query := &m.GetDashboardSnapshotQuery{Key: key} @@ -93,7 +92,7 @@ func GetDashboardSnapshot(c *middleware.Context) { } // GET /api/snapshots-delete/:key -func DeleteDashboardSnapshot(c *middleware.Context) Response { +func DeleteDashboardSnapshot(c *m.ReqContext) Response { key := c.Params(":key") query := &m.GetDashboardSnapshotQuery{DeleteKey: key} @@ -107,9 +106,9 @@ func DeleteDashboardSnapshot(c *middleware.Context) Response { return ApiError(404, "Failed to get dashboard snapshot", nil) } dashboard := query.Result.Dashboard - dashboardId := dashboard.Get("id").MustInt64() + dashboardID := dashboard.Get("id").MustInt64() - guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser) + guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser) canEdit, err := guardian.CanEdit() if err != nil { return ApiError(500, "Error while checking permissions for snapshot", err) @@ -129,7 +128,7 @@ func DeleteDashboardSnapshot(c *middleware.Context) Response { } // GET /api/dashboard/snapshots -func SearchDashboardSnapshots(c *middleware.Context) Response { +func SearchDashboardSnapshots(c *m.ReqContext) Response { query := c.Query("query") limit := c.QueryInt("limit") diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 3857fa0b9e1..d940c5406be 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/setting" @@ -106,7 +105,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -166,7 +165,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 200) Convey("Should lookup dashboard by uid", func() { @@ -272,7 +271,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -330,7 +329,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -399,7 +398,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 200) Convey("Should lookup dashboard by uid", func() { @@ -469,7 +468,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -528,7 +527,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 200) Convey("Should lookup dashboard by uid", func() { @@ -595,7 +594,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -838,12 +837,12 @@ func CallDeleteDashboard(sc *scenarioContext) { sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } -func CallDeleteDashboardByUid(sc *scenarioContext) { +func CallDeleteDashboardByUID(sc *scenarioContext) { bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error { return nil }) - sc.handlerFunc = DeleteDashboardByUid + sc.handlerFunc = DeleteDashboardByUID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } @@ -862,7 +861,7 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d defer bus.ClearBusHandlers() sc := setupScenarioContext(url) - sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.defaultHandler = wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId} @@ -887,7 +886,7 @@ func postDiffScenario(desc string, url string, routePattern string, cmd dtos.Cal defer bus.ClearBusHandlers() sc := setupScenarioContext(url) - sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.defaultHandler = wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.SignedInUser = &m.SignedInUser{ OrgId: TestOrgID, diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 8a712f99804..2e97e5d2d6f 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -7,26 +7,25 @@ import ( "github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ) const HeaderNameNoBackendCache = "X-Grafana-NoCache" -func (hs *HttpServer) getDatasourceById(id int64, orgId int64, nocache bool) (*m.DataSource, error) { +func (hs *HttpServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m.DataSource, error) { cacheKey := fmt.Sprintf("ds-%d", id) if !nocache { if cached, found := hs.cache.Get(cacheKey); found { ds := cached.(*m.DataSource) - if ds.OrgId == orgId { + if ds.OrgId == orgID { return ds, nil } } } - query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgId} + query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgID} if err := bus.Dispatch(&query); err != nil { return nil, err } @@ -35,12 +34,12 @@ func (hs *HttpServer) getDatasourceById(id int64, orgId int64, nocache bool) (*m return query.Result, nil } -func (hs *HttpServer) ProxyDataSourceRequest(c *middleware.Context) { +func (hs *HttpServer) ProxyDataSourceRequest(c *m.ReqContext) { c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer) nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true" - ds, err := hs.getDatasourceById(c.ParamsInt64(":id"), c.OrgId, nocache) + ds, err := hs.getDatasourceByID(c.ParamsInt64(":id"), c.OrgId, nocache) if err != nil { c.JsonApiErr(500, "Unable to load datasource meta data", err) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index b5c5f9cb834..4b5aa56d6e7 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -5,13 +5,12 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/util" ) -func GetDataSources(c *middleware.Context) Response { +func GetDataSources(c *m.ReqContext) Response { query := m.GetDataSourcesQuery{OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { @@ -50,7 +49,7 @@ func GetDataSources(c *middleware.Context) Response { return Json(200, &result) } -func GetDataSourceById(c *middleware.Context) Response { +func GetDataSourceByID(c *m.ReqContext) Response { query := m.GetDataSourceByIdQuery{ Id: c.ParamsInt64(":id"), OrgId: c.OrgId, @@ -69,14 +68,14 @@ func GetDataSourceById(c *middleware.Context) Response { return Json(200, &dtos) } -func DeleteDataSourceById(c *middleware.Context) Response { +func DeleteDataSourceByID(c *m.ReqContext) Response { id := c.ParamsInt64(":id") if id <= 0 { return ApiError(400, "Missing valid datasource id", nil) } - ds, err := getRawDataSourceById(id, c.OrgId) + ds, err := getRawDataSourceByID(id, c.OrgId) if err != nil { return ApiError(400, "Failed to delete datasource", nil) } @@ -95,7 +94,7 @@ func DeleteDataSourceById(c *middleware.Context) Response { return ApiSuccess("Data source deleted") } -func DeleteDataSourceByName(c *middleware.Context) Response { +func DeleteDataSourceByName(c *m.ReqContext) Response { name := c.Params(":name") if name == "" { @@ -120,7 +119,7 @@ func DeleteDataSourceByName(c *middleware.Context) Response { return ApiSuccess("Data source deleted") } -func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) Response { +func AddDataSource(c *m.ReqContext, cmd m.AddDataSourceCommand) Response { cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { @@ -140,11 +139,11 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) Response { }) } -func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Response { +func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response { cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":id") - err := fillWithSecureJsonData(&cmd) + err := fillWithSecureJSONData(&cmd) if err != nil { return ApiError(500, "Failed to update datasource", err) } @@ -153,9 +152,8 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp if err != nil { if err == m.ErrDataSourceUpdatingOldVersion { return ApiError(500, "Failed to update datasource. Reload new version and try again", err) - } else { - return ApiError(500, "Failed to update datasource", err) } + return ApiError(500, "Failed to update datasource", err) } ds := convertModelToDtos(cmd.Result) return Json(200, util.DynMap{ @@ -166,12 +164,12 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp }) } -func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { +func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error { if len(cmd.SecureJsonData) == 0 { return nil } - ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId) + ds, err := getRawDataSourceByID(cmd.Id, cmd.OrgId) if err != nil { return err } @@ -180,8 +178,8 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { return m.ErrDatasourceIsReadOnly } - secureJsonData := ds.SecureJsonData.Decrypt() - for k, v := range secureJsonData { + secureJSONData := ds.SecureJsonData.Decrypt() + for k, v := range secureJSONData { if _, ok := cmd.SecureJsonData[k]; !ok { cmd.SecureJsonData[k] = v @@ -191,10 +189,10 @@ 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, + OrgId: orgID, } if err := bus.Dispatch(&query); err != nil { @@ -205,7 +203,7 @@ func getRawDataSourceById(id int64, orgId int64) (*m.DataSource, error) { } // Get /api/datasources/name/:name -func GetDataSourceByName(c *middleware.Context) Response { +func GetDataSourceByName(c *m.ReqContext) Response { query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { @@ -221,7 +219,7 @@ func GetDataSourceByName(c *middleware.Context) Response { } // Get /api/datasources/id/:name -func GetDataSourceIdByName(c *middleware.Context) Response { +func GetDataSourceIDByName(c *m.ReqContext) Response { query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/api/folder.go b/pkg/api/folder.go index e3c4f127569..e88f7fc2c3b 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -4,14 +4,13 @@ import ( "fmt" "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/util" ) -func GetFolders(c *middleware.Context) Response { +func GetFolders(c *m.ReqContext) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) folders, err := s.GetFolders(c.QueryInt("limit")) @@ -32,7 +31,7 @@ func GetFolders(c *middleware.Context) Response { return Json(200, result) } -func GetFolderByUid(c *middleware.Context) Response { +func GetFolderByUID(c *m.ReqContext) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) folder, err := s.GetFolderByUid(c.Params(":uid")) @@ -44,7 +43,7 @@ func GetFolderByUid(c *middleware.Context) Response { return Json(200, toFolderDto(g, folder)) } -func GetFolderById(c *middleware.Context) Response { +func GetFolderByID(c *m.ReqContext) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) folder, err := s.GetFolderById(c.ParamsInt64(":id")) if err != nil { @@ -55,7 +54,7 @@ func GetFolderById(c *middleware.Context) Response { return Json(200, toFolderDto(g, folder)) } -func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response { +func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) err := s.CreateFolder(&cmd) if err != nil { @@ -66,7 +65,7 @@ func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response { return Json(200, toFolderDto(g, cmd.Result)) } -func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response { +func UpdateFolder(c *m.ReqContext, cmd m.UpdateFolderCommand) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) err := s.UpdateFolder(c.Params(":uid"), &cmd) if err != nil { @@ -77,7 +76,7 @@ func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response { return Json(200, toFolderDto(g, cmd.Result)) } -func DeleteFolder(c *middleware.Context) Response { +func DeleteFolder(c *m.ReqContext) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) f, err := s.DeleteFolder(c.Params(":uid")) if err != nil { diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 7c8aba87337..1b04eb20e53 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -5,13 +5,12 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" ) -func GetFolderPermissionList(c *middleware.Context) Response { +func GetFolderPermissionList(c *m.ReqContext) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) folder, err := s.GetFolderByUid(c.Params(":uid")) @@ -42,7 +41,7 @@ func GetFolderPermissionList(c *middleware.Context) Response { return Json(200, acl) } -func UpdateFolderPermissions(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response { +func UpdateFolderPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) folder, err := s.GetFolderByUid(c.Params(":uid")) diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 552577963b4..00d025fdce2 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" @@ -227,7 +226,7 @@ func updateFolderPermissionScenario(desc string, url string, routePattern string sc := setupScenarioContext(url) - sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.defaultHandler = wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.OrgId = TestOrgID sc.context.UserId = TestUserID diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 0ab3cc7d7c9..e3c63ac0745 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -7,11 +7,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" - m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/smartystreets/goconvey/convey" ) @@ -136,8 +133,8 @@ func TestFoldersApiEndpoint(t *testing.T) { }) } -func callGetFolderByUid(sc *scenarioContext) { - sc.handlerFunc = GetFolderByUid +func callGetFolderByUID(sc *scenarioContext) { + sc.handlerFunc = GetFolderByUID sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } @@ -155,7 +152,7 @@ func createFolderScenario(desc string, url string, routePattern string, mock *fa defer bus.ClearBusHandlers() sc := setupScenarioContext(url) - sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.defaultHandler = wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID} @@ -184,7 +181,7 @@ func updateFolderScenario(desc string, url string, routePattern string, mock *fa defer bus.ClearBusHandlers() sc := setupScenarioContext(url) - sc.defaultHandler = wrap(func(c *middleware.Context) Response { + sc.defaultHandler = wrap(func(c *m.ReqContext) Response { sc.context = c sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID} @@ -205,50 +202,50 @@ func updateFolderScenario(desc string, url string, routePattern string, mock *fa } type fakeFolderService struct { - GetFoldersResult []*models.Folder + GetFoldersResult []*m.Folder GetFoldersError error - GetFolderByUidResult *models.Folder + GetFolderByUidResult *m.Folder GetFolderByUidError error - GetFolderByIdResult *models.Folder + GetFolderByIdResult *m.Folder GetFolderByIdError error - CreateFolderResult *models.Folder + CreateFolderResult *m.Folder CreateFolderError error - UpdateFolderResult *models.Folder + UpdateFolderResult *m.Folder UpdateFolderError error - DeleteFolderResult *models.Folder + DeleteFolderResult *m.Folder DeleteFolderError error DeletedFolderUids []string } -func (s *fakeFolderService) GetFolders(limit int) ([]*models.Folder, error) { +func (s *fakeFolderService) GetFolders(limit int) ([]*m.Folder, error) { return s.GetFoldersResult, s.GetFoldersError } -func (s *fakeFolderService) GetFolderById(id int64) (*models.Folder, error) { +func (s *fakeFolderService) GetFolderById(id int64) (*m.Folder, error) { return s.GetFolderByIdResult, s.GetFolderByIdError } -func (s *fakeFolderService) GetFolderByUid(uid string) (*models.Folder, error) { +func (s *fakeFolderService) GetFolderByUid(uid string) (*m.Folder, error) { return s.GetFolderByUidResult, s.GetFolderByUidError } -func (s *fakeFolderService) CreateFolder(cmd *models.CreateFolderCommand) error { +func (s *fakeFolderService) CreateFolder(cmd *m.CreateFolderCommand) error { cmd.Result = s.CreateFolderResult return s.CreateFolderError } -func (s *fakeFolderService) UpdateFolder(existingUid string, cmd *models.UpdateFolderCommand) error { +func (s *fakeFolderService) UpdateFolder(existingUid string, cmd *m.UpdateFolderCommand) error { cmd.Result = s.UpdateFolderResult return s.UpdateFolderError } -func (s *fakeFolderService) DeleteFolder(uid string) (*models.Folder, error) { +func (s *fakeFolderService) DeleteFolder(uid string) (*m.Folder, error) { s.DeletedFolderUids = append(s.DeletedFolderUids, uid) return s.DeleteFolderResult, s.DeleteFolderError } func mockFolderService(mock *fakeFolderService) { - dashboards.NewFolderService = func(orgId int64, user *models.SignedInUser) dashboards.FolderService { + dashboards.NewFolderService = func(orgId int64, user *m.SignedInUser) dashboards.FolderService { return mock } } diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 591dcc62344..5cd52122c3f 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -5,14 +5,13 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) -func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, error) { +func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { orgDataSources := make([]*m.DataSource, 0) if c.OrgId != 0 { @@ -180,7 +179,7 @@ func getPanelSort(id string) int { return sort } -func GetFrontendSettings(c *middleware.Context) { +func GetFrontendSettings(c *m.ReqContext) { settings, err := getFrontendSettingsMap(c) if err != nil { c.JsonApiErr(400, "Failed to get frontend settings", err) diff --git a/pkg/api/grafana_com_proxy.go b/pkg/api/grafana_com_proxy.go index a2a446b48eb..afd3bb9bf8e 100644 --- a/pkg/api/grafana_com_proxy.go +++ b/pkg/api/grafana_com_proxy.go @@ -7,7 +7,7 @@ import ( "net/url" "time" - "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -41,7 +41,7 @@ func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy { return &httputil.ReverseProxy{Director: director} } -func ProxyGnetRequest(c *middleware.Context) { +func ProxyGnetRequest(c *m.ReqContext) { proxyPath := c.Params("*") proxy := ReverseProxyGnetReq(proxyPath) proxy.Transport = grafanaComProxyTransport diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index b911780913d..772a27bd4cd 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -39,7 +39,7 @@ type HttpServer struct { httpSrv *http.Server } -func NewHttpServer() *HttpServer { +func NewHTTPServer() *HttpServer { return &HttpServer{ log: log.New("http.server"), cache: gocache.New(5*time.Minute, 10*time.Minute), @@ -175,7 +175,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron { m.Use(hs.healthHandler) m.Use(hs.metricsEndpoint) m.Use(middleware.GetContextHandler()) - m.Use(middleware.Sessioner(&setting.SessionOptions)) + m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime)) m.Use(middleware.OrgRedirect()) // needs to be after context handler diff --git a/pkg/api/index.go b/pkg/api/index.go index 5beecefab88..a1d21d1c686 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -6,13 +6,12 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" ) -func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { +func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { settings, err := getFrontendSettingsMap(c) if err != nil { return nil, err @@ -33,13 +32,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { locale = parts[0] } - appUrl := setting.AppUrl - appSubUrl := setting.AppSubUrl + appURL := setting.AppUrl + appSubURL := setting.AppSubUrl // special case when doing localhost call from phantomjs if c.IsRenderCall { - appUrl = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort) - appSubUrl = "" + appURL = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort) + appSubURL = "" settings["appSubUrl"] = "" } @@ -63,8 +62,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { }, Settings: settings, Theme: prefs.Theme, - AppUrl: appUrl, - AppSubUrl: appSubUrl, + AppUrl: appURL, + AppSubUrl: appSubURL, GoogleAnalyticsId: setting.GoogleAnalyticsId, GoogleTagManagerId: setting.GoogleTagManagerId, BuildVersion: setting.BuildVersion, @@ -74,15 +73,15 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { } if setting.DisableGravatar { - data.User.GravatarUrl = setting.AppSubUrl + "/public/img/transparent.png" + data.User.GravatarUrl = setting.AppSubUrl + "/public/img/user_profile.png" } if len(data.User.Name) == 0 { data.User.Name = data.User.Login } - themeUrlParam := c.Query("theme") - if themeUrlParam == "light" { + themeURLParam := c.Query("theme") + if themeURLParam == "light" { data.User.LightTheme = true data.Theme = "light" } @@ -299,25 +298,26 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { return &data, nil } -func Index(c *middleware.Context) { - if data, err := setIndexViewData(c); err != nil { +func Index(c *m.ReqContext) { + data, err := setIndexViewData(c) + if err != nil { c.Handle(500, "Failed to get settings", err) return - } else { - c.HTML(200, "index", data) } + c.HTML(200, "index", data) } -func NotFoundHandler(c *middleware.Context) { +func NotFoundHandler(c *m.ReqContext) { if c.IsApiRequest() { c.JsonApiErr(404, "Not found", nil) return } - if data, err := setIndexViewData(c); err != nil { + data, err := setIndexViewData(c) + if err != nil { c.Handle(500, "Failed to get settings", err) return - } else { - c.HTML(404, "index", data) } + + c.HTML(404, "index", data) } diff --git a/pkg/api/login.go b/pkg/api/login.go index b6855af7baf..dc5ae730721 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -8,16 +8,16 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" ) const ( - VIEW_INDEX = "index" + ViewIndex = "index" ) -func LoginView(c *middleware.Context) { +func LoginView(c *m.ReqContext) { viewData, err := setIndexViewData(c) if err != nil { c.Handle(500, "Failed to get settings", err) @@ -40,7 +40,7 @@ func LoginView(c *middleware.Context) { } if !tryLoginUsingRememberCookie(c) { - c.HTML(200, VIEW_INDEX, viewData) + c.HTML(200, ViewIndex, viewData) return } @@ -53,7 +53,7 @@ func LoginView(c *middleware.Context) { c.Redirect(setting.AppSubUrl + "/") } -func tryLoginUsingRememberCookie(c *middleware.Context) bool { +func tryLoginUsingRememberCookie(c *m.ReqContext) bool { // Check auto-login. uname := c.GetCookie(setting.CookieUserName) if len(uname) == 0 { @@ -87,7 +87,7 @@ func tryLoginUsingRememberCookie(c *middleware.Context) bool { return true } -func LoginApiPing(c *middleware.Context) { +func LoginAPIPing(c *m.ReqContext) { if !tryLoginUsingRememberCookie(c) { c.JsonApiErr(401, "Unauthorized", nil) return @@ -96,7 +96,7 @@ func LoginApiPing(c *middleware.Context) { c.JsonOK("Logged in") } -func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response { +func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response { if setting.DisableLoginForm { return ApiError(401, "Login is disabled", nil) } @@ -133,7 +133,7 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response { return Json(200, result) } -func loginUserWithUser(user *m.User, c *middleware.Context) { +func loginUserWithUser(user *m.User, c *m.ReqContext) { if user == nil { log.Error(3, "User login with nil user") } @@ -146,13 +146,13 @@ func loginUserWithUser(user *m.User, c *middleware.Context) { c.SetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/") } - c.Session.RegenerateId(c) - c.Session.Set(middleware.SESS_KEY_USERID, user.Id) + c.Session.RegenerateId(c.Context) + c.Session.Set(session.SESS_KEY_USERID, user.Id) } -func Logout(c *middleware.Context) { +func Logout(c *m.ReqContext) { c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/") c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/") - c.Session.Destory(c) + c.Session.Destory(c.Context) c.Redirect(setting.AppSubUrl + "/login") } diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 341ff212f10..1dba38e9cbd 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -17,8 +17,9 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/social" ) @@ -38,7 +39,7 @@ func GenStateString() string { return base64.URLEncoding.EncodeToString(rnd) } -func OAuthLogin(ctx *middleware.Context) { +func OAuthLogin(ctx *m.ReqContext) { if setting.OAuthService == nil { ctx.Handle(404, "OAuth not enabled", nil) return @@ -62,7 +63,7 @@ func OAuthLogin(ctx *middleware.Context) { code := ctx.Query("code") if code == "" { state := GenStateString() - ctx.Session.Set(middleware.SESS_KEY_OAUTH_STATE, state) + ctx.Session.Set(session.SESS_KEY_OAUTH_STATE, state) if setting.OAuthService.OAuthInfos[name].HostedDomain == "" { ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline)) } else { @@ -71,7 +72,7 @@ func OAuthLogin(ctx *middleware.Context) { return } - savedState, ok := ctx.Session.Get(middleware.SESS_KEY_OAUTH_STATE).(string) + savedState, ok := ctx.Session.Get(session.SESS_KEY_OAUTH_STATE).(string) if !ok { ctx.Handle(500, "login.OAuthLogin(missing saved state)", nil) return @@ -167,7 +168,7 @@ func OAuthLogin(ctx *middleware.Context) { redirectWithError(ctx, ErrSignUpNotAllowed) return } - limitReached, err := middleware.QuotaReached(ctx, "user") + limitReached, err := quota.QuotaReached(ctx, "user") if err != nil { ctx.Handle(500, "Failed to get user quota", err) return @@ -208,7 +209,7 @@ func OAuthLogin(ctx *middleware.Context) { ctx.Redirect(setting.AppSubUrl + "/") } -func redirectWithError(ctx *middleware.Context, err error, v ...interface{}) { +func redirectWithError(ctx *m.ReqContext, err error, v ...interface{}) { ctx.Logger.Error(err.Error(), v...) ctx.Session.Set("loginError", err.Error()) ctx.Redirect(setting.AppSubUrl + "/login") diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 717565cd4a1..38bb8dc0688 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -6,27 +6,26 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb/testdata" "github.com/grafana/grafana/pkg/util" ) // POST /api/tsdb/query -func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response { +func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response { timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To) if len(reqDto.Queries) == 0 { return ApiError(400, "No queries found in query", nil) } - dsId, err := reqDto.Queries[0].Get("datasourceId").Int64() + dsID, err := reqDto.Queries[0].Get("datasourceId").Int64() if err != nil { return ApiError(400, "Query missing datasourceId", nil) } - dsQuery := models.GetDataSourceByIdQuery{Id: dsId, OrgId: c.OrgId} + dsQuery := m.GetDataSourceByIdQuery{Id: dsID, OrgId: c.OrgId} if err := bus.Dispatch(&dsQuery); err != nil { return ApiError(500, "failed to fetch data source", err) } @@ -61,7 +60,7 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response { } // GET /api/tsdb/testdata/scenarios -func GetTestDataScenarios(c *middleware.Context) Response { +func GetTestDataScenarios(c *m.ReqContext) Response { result := make([]interface{}, 0) for _, scenario := range testdata.ScenarioRegistry { @@ -77,14 +76,14 @@ func GetTestDataScenarios(c *middleware.Context) Response { } // Genereates a index out of range error -func GenerateError(c *middleware.Context) Response { +func GenerateError(c *m.ReqContext) Response { var array []string return Json(200, array[20]) } // GET /api/tsdb/testdata/gensql -func GenerateSqlTestData(c *middleware.Context) Response { - if err := bus.Dispatch(&models.InsertSqlTestDataCommand{}); err != nil { +func GenerateSQLTestData(c *m.ReqContext) Response { + if err := bus.Dispatch(&m.InsertSqlTestDataCommand{}); err != nil { return ApiError(500, "Failed to insert test data", err) } @@ -92,7 +91,7 @@ func GenerateSqlTestData(c *middleware.Context) Response { } // GET /api/tsdb/testdata/random-walk -func GetTestDataRandomWalk(c *middleware.Context) Response { +func GetTestDataRandomWalk(c *m.ReqContext) Response { from := c.Query("from") to := c.Query("to") intervalMs := c.QueryInt64("intervalMs") @@ -100,7 +99,7 @@ func GetTestDataRandomWalk(c *middleware.Context) Response { timeRange := tsdb.NewTimeRange(from, to) request := &tsdb.TsdbQuery{TimeRange: timeRange} - dsInfo := &models.DataSource{Type: "grafana-testdata-datasource"} + dsInfo := &m.DataSource{Type: "grafana-testdata-datasource"} request.Queries = append(request.Queries, &tsdb.Query{ RefId: "A", IntervalMs: intervalMs, diff --git a/pkg/api/org.go b/pkg/api/org.go index bddfebf80ce..7735bd6a7eb 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -4,24 +4,23 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) // GET /api/org -func GetOrgCurrent(c *middleware.Context) Response { +func GetOrgCurrent(c *m.ReqContext) Response { return getOrgHelper(c.OrgId) } // GET /api/orgs/:orgId -func GetOrgById(c *middleware.Context) Response { +func GetOrgByID(c *m.ReqContext) Response { return getOrgHelper(c.ParamsInt64(":orgId")) } // Get /api/orgs/name/:name -func GetOrgByName(c *middleware.Context) Response { +func GetOrgByName(c *m.ReqContext) Response { query := m.GetOrgByNameQuery{Name: c.Params(":name")} if err := bus.Dispatch(&query); err != nil { if err == m.ErrOrgNotFound { @@ -76,7 +75,7 @@ func getOrgHelper(orgId int64) Response { } // POST /api/orgs -func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response { +func CreateOrg(c *m.ReqContext, cmd m.CreateOrgCommand) Response { if !c.IsSignedIn || (!setting.AllowUserOrgCreate && !c.IsGrafanaAdmin) { return ApiError(403, "Access denied", nil) } @@ -98,17 +97,17 @@ func CreateOrg(c *middleware.Context, cmd m.CreateOrgCommand) Response { } // PUT /api/org -func UpdateOrgCurrent(c *middleware.Context, form dtos.UpdateOrgForm) Response { +func UpdateOrgCurrent(c *m.ReqContext, form dtos.UpdateOrgForm) Response { return updateOrgHelper(form, c.OrgId) } // PUT /api/orgs/:orgId -func UpdateOrg(c *middleware.Context, form dtos.UpdateOrgForm) Response { +func UpdateOrg(c *m.ReqContext, form dtos.UpdateOrgForm) Response { return updateOrgHelper(form, c.ParamsInt64(":orgId")) } -func updateOrgHelper(form dtos.UpdateOrgForm, orgId int64) Response { - cmd := m.UpdateOrgCommand{Name: form.Name, OrgId: orgId} +func updateOrgHelper(form dtos.UpdateOrgForm, orgID int64) Response { + cmd := m.UpdateOrgCommand{Name: form.Name, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrOrgNameTaken { return ApiError(400, "Organization name taken", err) @@ -120,18 +119,18 @@ func updateOrgHelper(form dtos.UpdateOrgForm, orgId int64) Response { } // PUT /api/org/address -func UpdateOrgAddressCurrent(c *middleware.Context, form dtos.UpdateOrgAddressForm) Response { +func UpdateOrgAddressCurrent(c *m.ReqContext, form dtos.UpdateOrgAddressForm) Response { return updateOrgAddressHelper(form, c.OrgId) } // PUT /api/orgs/:orgId/address -func UpdateOrgAddress(c *middleware.Context, form dtos.UpdateOrgAddressForm) Response { +func UpdateOrgAddress(c *m.ReqContext, form dtos.UpdateOrgAddressForm) Response { return updateOrgAddressHelper(form, c.ParamsInt64(":orgId")) } -func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Response { +func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgID int64) Response { cmd := m.UpdateOrgAddressCommand{ - OrgId: orgId, + OrgId: orgID, Address: m.Address{ Address1: form.Address1, Address2: form.Address2, @@ -150,7 +149,7 @@ func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Respons } // GET /api/orgs/:orgId -func DeleteOrgById(c *middleware.Context) Response { +func DeleteOrgByID(c *m.ReqContext) Response { if err := bus.Dispatch(&m.DeleteOrgCommand{Id: c.ParamsInt64(":orgId")}); err != nil { if err == m.ErrOrgNotFound { return ApiError(404, "Failed to delete organization. ID not found", nil) @@ -160,7 +159,7 @@ func DeleteOrgById(c *middleware.Context) Response { return ApiSuccess("Organization deleted") } -func SearchOrgs(c *middleware.Context) Response { +func SearchOrgs(c *m.ReqContext) Response { query := m.SearchOrgsQuery{ Query: c.Query("query"), Name: c.Query("name"), diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 57d9913d2eb..0486287a31b 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -7,13 +7,12 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) -func GetPendingOrgInvites(c *middleware.Context) Response { +func GetPendingOrgInvites(c *m.ReqContext) Response { query := m.GetTempUsersQuery{OrgId: c.OrgId, Status: m.TmpUserInvitePending} if err := bus.Dispatch(&query); err != nil { @@ -27,7 +26,7 @@ func GetPendingOrgInvites(c *middleware.Context) Response { return Json(200, query.Result) } -func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response { +func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response { if !inviteDto.Role.IsValid() { return ApiError(400, "Invalid role specified", nil) } @@ -89,7 +88,7 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response return ApiSuccess(fmt.Sprintf("Created invite for %s", inviteDto.LoginOrEmail)) } -func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dtos.AddInviteForm) Response { +func inviteExistingUserToOrg(c *m.ReqContext, user *m.User, inviteDto *dtos.AddInviteForm) Response { // user exists, add org role createOrgUserCmd := m.AddOrgUserCommand{OrgId: c.OrgId, UserId: user.Id, Role: inviteDto.Role} if err := bus.Dispatch(&createOrgUserCmd); err != nil { @@ -97,29 +96,28 @@ func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dto return ApiError(412, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err) } return ApiError(500, "Error while trying to create org user", err) - } else { + } - if inviteDto.SendEmail && util.IsEmail(user.Email) { - emailCmd := m.SendEmailCommand{ - To: []string{user.Email}, - Template: "invited_to_org.html", - Data: map[string]interface{}{ - "Name": user.NameOrFallback(), - "OrgName": c.OrgName, - "InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login), - }, - } - - if err := bus.Dispatch(&emailCmd); err != nil { - return ApiError(500, "Failed to send email invited_to_org", err) - } + if inviteDto.SendEmail && util.IsEmail(user.Email) { + emailCmd := m.SendEmailCommand{ + To: []string{user.Email}, + Template: "invited_to_org.html", + Data: map[string]interface{}{ + "Name": user.NameOrFallback(), + "OrgName": c.OrgName, + "InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login), + }, } - return ApiSuccess(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName)) + if err := bus.Dispatch(&emailCmd); err != nil { + return ApiError(500, "Failed to send email invited_to_org", err) + } } + + return ApiSuccess(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName)) } -func RevokeInvite(c *middleware.Context) Response { +func RevokeInvite(c *m.ReqContext) Response { if ok, rsp := updateTempUserStatus(c.Params(":code"), m.TmpUserRevoked); !ok { return rsp } @@ -127,7 +125,7 @@ func RevokeInvite(c *middleware.Context) Response { return ApiSuccess("Invite revoked") } -func GetInviteInfoByCode(c *middleware.Context) Response { +func GetInviteInfoByCode(c *m.ReqContext) Response { query := m.GetTempUserByCodeQuery{Code: c.Params(":code")} if err := bus.Dispatch(&query); err != nil { @@ -147,7 +145,7 @@ func GetInviteInfoByCode(c *middleware.Context) Response { }) } -func CompleteInvite(c *middleware.Context, completeInvite dtos.CompleteInviteForm) Response { +func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response { query := m.GetTempUserByCodeQuery{Code: completeInvite.InviteCode} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 433b9f2bd66..cd1430d22fb 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -3,18 +3,17 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" ) // POST /api/org/users -func AddOrgUserToCurrentOrg(c *middleware.Context, cmd m.AddOrgUserCommand) Response { +func AddOrgUserToCurrentOrg(c *m.ReqContext, cmd m.AddOrgUserCommand) Response { cmd.OrgId = c.OrgId return addOrgUserHelper(cmd) } // POST /api/orgs/:orgId/users -func AddOrgUser(c *middleware.Context, cmd m.AddOrgUserCommand) Response { +func AddOrgUser(c *m.ReqContext, cmd m.AddOrgUserCommand) Response { cmd.OrgId = c.ParamsInt64(":orgId") return addOrgUserHelper(cmd) } @@ -45,18 +44,18 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response { } // GET /api/org/users -func GetOrgUsersForCurrentOrg(c *middleware.Context) Response { +func GetOrgUsersForCurrentOrg(c *m.ReqContext) Response { return getOrgUsersHelper(c.OrgId, c.Params("query"), c.ParamsInt("limit")) } // GET /api/orgs/:orgId/users -func GetOrgUsers(c *middleware.Context) Response { +func GetOrgUsers(c *m.ReqContext) Response { return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0) } -func getOrgUsersHelper(orgId int64, query string, limit int) Response { +func getOrgUsersHelper(orgID int64, query string, limit int) Response { q := m.GetOrgUsersQuery{ - OrgId: orgId, + OrgId: orgID, Query: query, Limit: limit, } @@ -73,14 +72,14 @@ func getOrgUsersHelper(orgId int64, query string, limit int) Response { } // PATCH /api/org/users/:userId -func UpdateOrgUserForCurrentOrg(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response { +func UpdateOrgUserForCurrentOrg(c *m.ReqContext, cmd m.UpdateOrgUserCommand) Response { cmd.OrgId = c.OrgId cmd.UserId = c.ParamsInt64(":userId") return updateOrgUserHelper(cmd) } // PATCH /api/orgs/:orgId/users/:userId -func UpdateOrgUser(c *middleware.Context, cmd m.UpdateOrgUserCommand) Response { +func UpdateOrgUser(c *m.ReqContext, cmd m.UpdateOrgUserCommand) Response { cmd.OrgId = c.ParamsInt64(":orgId") cmd.UserId = c.ParamsInt64(":userId") return updateOrgUserHelper(cmd) @@ -102,20 +101,20 @@ func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response { } // DELETE /api/org/users/:userId -func RemoveOrgUserForCurrentOrg(c *middleware.Context) Response { - userId := c.ParamsInt64(":userId") - return removeOrgUserHelper(c.OrgId, userId) +func RemoveOrgUserForCurrentOrg(c *m.ReqContext) Response { + userID := c.ParamsInt64(":userId") + return removeOrgUserHelper(c.OrgId, userID) } // DELETE /api/orgs/:orgId/users/:userId -func RemoveOrgUser(c *middleware.Context) Response { - userId := c.ParamsInt64(":userId") - orgId := c.ParamsInt64(":orgId") - return removeOrgUserHelper(orgId, userId) +func RemoveOrgUser(c *m.ReqContext) Response { + userID := c.ParamsInt64(":userId") + orgID := c.ParamsInt64(":orgId") + return removeOrgUserHelper(orgID, userID) } -func removeOrgUserHelper(orgId int64, userId int64) Response { - cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: userId} +func removeOrgUserHelper(orgID int64, userID int64) Response { + cmd := m.RemoveOrgUserCommand{OrgId: orgID, UserId: userID} if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrLastOrgAdmin { diff --git a/pkg/api/password.go b/pkg/api/password.go index e71f1317ee4..31ea5d91b34 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -3,12 +3,11 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" ) -func SendResetPasswordEmail(c *middleware.Context, form dtos.SendResetPasswordEmailForm) Response { +func SendResetPasswordEmail(c *m.ReqContext, form dtos.SendResetPasswordEmailForm) Response { userQuery := m.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail} if err := bus.Dispatch(&userQuery); err != nil { @@ -24,7 +23,7 @@ func SendResetPasswordEmail(c *middleware.Context, form dtos.SendResetPasswordEm return ApiSuccess("Email sent") } -func ResetPassword(c *middleware.Context, form dtos.ResetUserPasswordForm) Response { +func ResetPassword(c *m.ReqContext, form dtos.ResetUserPasswordForm) Response { query := m.ValidateResetPasswordCodeQuery{Code: form.Code} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 040aef0474e..0198850252d 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -3,11 +3,10 @@ package api import ( "github.com/grafana/grafana/pkg/bus" _ "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" ) -func ValidateOrgPlaylist(c *middleware.Context) { +func ValidateOrgPlaylist(c *m.ReqContext) { id := c.ParamsInt64(":id") query := m.GetPlaylistByIdQuery{Id: id} err := bus.Dispatch(&query) @@ -40,7 +39,7 @@ func ValidateOrgPlaylist(c *middleware.Context) { } } -func SearchPlaylists(c *middleware.Context) Response { +func SearchPlaylists(c *m.ReqContext) Response { query := c.Query("query") limit := c.QueryInt("limit") @@ -62,7 +61,7 @@ func SearchPlaylists(c *middleware.Context) Response { return Json(200, searchQuery.Result) } -func GetPlaylist(c *middleware.Context) Response { +func GetPlaylist(c *m.ReqContext) Response { id := c.ParamsInt64(":id") cmd := m.GetPlaylistByIdQuery{Id: id} @@ -115,7 +114,7 @@ func LoadPlaylistItems(id int64) ([]m.PlaylistItem, error) { return *itemQuery.Result, nil } -func GetPlaylistItems(c *middleware.Context) Response { +func GetPlaylistItems(c *m.ReqContext) Response { id := c.ParamsInt64(":id") playlistDTOs, err := LoadPlaylistItemDTOs(id) @@ -127,10 +126,10 @@ func GetPlaylistItems(c *middleware.Context) Response { return Json(200, playlistDTOs) } -func GetPlaylistDashboards(c *middleware.Context) Response { - playlistId := c.ParamsInt64(":id") +func GetPlaylistDashboards(c *m.ReqContext) Response { + playlistID := c.ParamsInt64(":id") - playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistId) + playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistID) if err != nil { return ApiError(500, "Could not load dashboards", err) } @@ -138,7 +137,7 @@ func GetPlaylistDashboards(c *middleware.Context) Response { return Json(200, playlists) } -func DeletePlaylist(c *middleware.Context) Response { +func DeletePlaylist(c *m.ReqContext) Response { id := c.ParamsInt64(":id") cmd := m.DeletePlaylistCommand{Id: id, OrgId: c.OrgId} @@ -149,7 +148,7 @@ func DeletePlaylist(c *middleware.Context) Response { return Json(200, "") } -func CreatePlaylist(c *middleware.Context, cmd m.CreatePlaylistCommand) Response { +func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response { cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { @@ -159,7 +158,7 @@ func CreatePlaylist(c *middleware.Context, cmd m.CreatePlaylistCommand) Response return Json(200, cmd.Result) } -func UpdatePlaylist(c *middleware.Context, cmd m.UpdatePlaylistCommand) Response { +func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response { cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index 1d059e06be5..69a2caef7e0 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -34,29 +34,27 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i return result, nil } -func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice { +func populateDashboardsByTag(orgID int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice { result := make(dtos.PlaylistDashboardsSlice, 0) - if len(dashboardByTag) > 0 { - for _, tag := range dashboardByTag { - searchQuery := search.Query{ - Title: "", - Tags: []string{tag}, - SignedInUser: signedInUser, - Limit: 100, - IsStarred: false, - OrgId: orgId, - } + for _, tag := range dashboardByTag { + searchQuery := search.Query{ + Title: "", + Tags: []string{tag}, + SignedInUser: signedInUser, + Limit: 100, + IsStarred: false, + OrgId: orgID, + } - if err := bus.Dispatch(&searchQuery); err == nil { - for _, item := range searchQuery.Result { - result = append(result, dtos.PlaylistDashboard{ - Id: item.Id, - Title: item.Title, - Uri: item.Uri, - Order: dashboardTagOrder[tag], - }) - } + if err := bus.Dispatch(&searchQuery); err == nil { + for _, item := range searchQuery.Result { + result = append(result, dtos.PlaylistDashboard{ + Id: item.Id, + Title: item.Title, + Uri: item.Uri, + Order: dashboardTagOrder[tag], + }) } } } @@ -64,19 +62,19 @@ func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboar return result } -func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistId int64) (dtos.PlaylistDashboardsSlice, error) { - playlistItems, _ := LoadPlaylistItems(playlistId) +func LoadPlaylistDashboards(orgID int64, signedInUser *m.SignedInUser, playlistID int64) (dtos.PlaylistDashboardsSlice, error) { + playlistItems, _ := LoadPlaylistItems(playlistID) - dashboardByIds := make([]int64, 0) + dashboardByIDs := make([]int64, 0) dashboardByTag := make([]string, 0) - dashboardIdOrder := make(map[int64]int) + dashboardIDOrder := make(map[int64]int) dashboardTagOrder := make(map[string]int) for _, i := range playlistItems { if i.Type == "dashboard_by_id" { - dashboardId, _ := strconv.ParseInt(i.Value, 10, 64) - dashboardByIds = append(dashboardByIds, dashboardId) - dashboardIdOrder[dashboardId] = i.Order + dashboardID, _ := strconv.ParseInt(i.Value, 10, 64) + dashboardByIDs = append(dashboardByIDs, dashboardID) + dashboardIDOrder[dashboardID] = i.Order } if i.Type == "dashboard_by_tag" { @@ -87,9 +85,9 @@ func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistI result := make(dtos.PlaylistDashboardsSlice, 0) - var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder) + var k, _ = populateDashboardsById(dashboardByIDs, dashboardIDOrder) result = append(result, k...) - result = append(result, populateDashboardsByTag(orgId, signedInUser, dashboardByTag, dashboardTagOrder)...) + result = append(result, populateDashboardsByTag(orgID, signedInUser, dashboardByTag, dashboardTagOrder)...) sort.Sort(result) return result, nil diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 5f4ec632c4d..b861a344c75 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -18,7 +18,6 @@ import ( "github.com/opentracing/opentracing-go" "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" @@ -42,14 +41,14 @@ type jwtToken struct { type DataSourceProxy struct { ds *m.DataSource - ctx *middleware.Context + ctx *m.ReqContext targetUrl *url.URL proxyPath string route *plugins.AppPluginRoute plugin *plugins.DataSourcePlugin } -func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *middleware.Context, proxyPath string) *DataSourceProxy { +func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy { targetUrl, _ := url.Parse(ds.Url) return &DataSourceProxy{ @@ -190,8 +189,14 @@ func (proxy *DataSourceProxy) validateRequest() error { } if proxy.ds.Type == m.DS_PROMETHEUS { - if proxy.ctx.Req.Request.Method != http.MethodGet || !strings.HasPrefix(proxy.proxyPath, "api/") { - return errors.New("GET is only allowed on proxied Prometheus datasource") + if proxy.ctx.Req.Request.Method == "DELETE" { + return errors.New("Deletes not allowed on proxied Prometheus datasource") + } + if proxy.ctx.Req.Request.Method == "PUT" { + return errors.New("Puts not allowed on proxied Prometheus datasource") + } + if proxy.ctx.Req.Request.Method == "POST" && !(proxy.proxyPath == "api/v1/query" || proxy.proxyPath == "api/v1/query_range") { + return errors.New("Posts not allowed on proxied Prometheus datasource except on /query and /query_range") } } @@ -255,7 +260,7 @@ func (proxy *DataSourceProxy) logRequest() { "body", body) } -func checkWhiteList(c *middleware.Context, host string) bool { +func checkWhiteList(c *m.ReqContext, host string) bool { if host != "" && len(setting.DataProxyWhiteList) > 0 { if _, exists := setting.DataProxyWhiteList[host]; !exists { c.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil) diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index a7a869b2a9f..3cf67d9178a 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -8,7 +8,6 @@ import ( macaron "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" @@ -61,7 +60,7 @@ func TestDSRouteRule(t *testing.T) { } req, _ := http.NewRequest("GET", "http://localhost/asd", nil) - ctx := &middleware.Context{ + ctx := &m.ReqContext{ Context: &macaron.Context{ Req: macaron.Request{Request: req}, }, @@ -104,7 +103,7 @@ func TestDSRouteRule(t *testing.T) { Convey("When proxying graphite", func() { plugin := &plugins.DataSourcePlugin{} ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE} - ctx := &middleware.Context{} + ctx := &m.ReqContext{} proxy := NewDataSourceProxy(ds, plugin, ctx, "/render") @@ -130,7 +129,7 @@ func TestDSRouteRule(t *testing.T) { Password: "password", } - ctx := &middleware.Context{} + ctx := &m.ReqContext{} proxy := NewDataSourceProxy(ds, plugin, ctx, "") requestUrl, _ := url.Parse("http://grafana.com/sub") @@ -160,7 +159,7 @@ func TestDSRouteRule(t *testing.T) { JsonData: json, } - ctx := &middleware.Context{} + ctx := &m.ReqContext{} proxy := NewDataSourceProxy(ds, plugin, ctx, "") requestUrl, _ := url.Parse("http://grafana.com/sub") @@ -186,7 +185,7 @@ func TestDSRouteRule(t *testing.T) { JsonData: json, } - ctx := &middleware.Context{} + ctx := &m.ReqContext{} proxy := NewDataSourceProxy(ds, plugin, ctx, "") requestUrl, _ := url.Parse("http://grafana.com/sub") diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go index 59138884228..eb78250838a 100644 --- a/pkg/api/pluginproxy/pluginproxy.go +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/util" @@ -38,7 +37,7 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http. return result, err } -func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy { +func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy { targetUrl, _ := url.Parse(route.Url) director := func(req *http.Request) { diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index e8c21541339..81b3ac10cef 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -5,13 +5,12 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" ) -func GetPluginList(c *middleware.Context) Response { +func GetPluginList(c *m.ReqContext) Response { typeFilter := c.Query("type") enabledFilter := c.Query("enabled") embeddedFilter := c.Query("embedded") @@ -79,48 +78,48 @@ func GetPluginList(c *middleware.Context) Response { return Json(200, result) } -func GetPluginSettingById(c *middleware.Context) Response { - pluginId := c.Params(":pluginId") +func GetPluginSettingByID(c *m.ReqContext) Response { + pluginID := c.Params(":pluginId") - if def, exists := plugins.Plugins[pluginId]; !exists { + def, exists := plugins.Plugins[pluginID] + if !exists { return ApiError(404, "Plugin not found, no installed plugin with that id", nil) - } else { - - dto := &dtos.PluginSetting{ - Type: def.Type, - Id: def.Id, - Name: def.Name, - Info: &def.Info, - Dependencies: &def.Dependencies, - Includes: def.Includes, - BaseUrl: def.BaseUrl, - Module: def.Module, - DefaultNavUrl: def.DefaultNavUrl, - LatestVersion: def.GrafanaNetVersion, - HasUpdate: def.GrafanaNetHasUpdate, - State: def.State, - } - - query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId} - if err := bus.Dispatch(&query); err != nil { - if err != m.ErrPluginSettingNotFound { - return ApiError(500, "Failed to get login settings", nil) - } - } else { - dto.Enabled = query.Result.Enabled - dto.Pinned = query.Result.Pinned - dto.JsonData = query.Result.JsonData - } - - return Json(200, dto) } + + dto := &dtos.PluginSetting{ + Type: def.Type, + Id: def.Id, + Name: def.Name, + Info: &def.Info, + Dependencies: &def.Dependencies, + Includes: def.Includes, + BaseUrl: def.BaseUrl, + Module: def.Module, + DefaultNavUrl: def.DefaultNavUrl, + LatestVersion: def.GrafanaNetVersion, + HasUpdate: def.GrafanaNetHasUpdate, + State: def.State, + } + + query := m.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId} + if err := bus.Dispatch(&query); err != nil { + if err != m.ErrPluginSettingNotFound { + return ApiError(500, "Failed to get login settings", nil) + } + } else { + dto.Enabled = query.Result.Enabled + dto.Pinned = query.Result.Pinned + dto.JsonData = query.Result.JsonData + } + + return Json(200, dto) } -func UpdatePluginSetting(c *middleware.Context, cmd m.UpdatePluginSettingCmd) Response { - pluginId := c.Params(":pluginId") +func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response { + pluginID := c.Params(":pluginId") cmd.OrgId = c.OrgId - cmd.PluginId = pluginId + cmd.PluginId = pluginID if _, ok := plugins.Apps[cmd.PluginId]; !ok { return ApiError(404, "Plugin not installed.", nil) @@ -133,38 +132,40 @@ func UpdatePluginSetting(c *middleware.Context, cmd m.UpdatePluginSettingCmd) Re return ApiSuccess("Plugin settings updated") } -func GetPluginDashboards(c *middleware.Context) Response { - pluginId := c.Params(":pluginId") +func GetPluginDashboards(c *m.ReqContext) Response { + pluginID := c.Params(":pluginId") - if list, err := plugins.GetPluginDashboards(c.OrgId, pluginId); err != nil { + list, err := plugins.GetPluginDashboards(c.OrgId, pluginID) + if err != nil { if notfound, ok := err.(plugins.PluginNotFoundError); ok { return ApiError(404, notfound.Error(), nil) } return ApiError(500, "Failed to get plugin dashboards", err) - } else { - return Json(200, list) } + + return Json(200, list) } -func GetPluginMarkdown(c *middleware.Context) Response { - pluginId := c.Params(":pluginId") +func GetPluginMarkdown(c *m.ReqContext) Response { + pluginID := c.Params(":pluginId") name := c.Params(":name") - if content, err := plugins.GetPluginMarkdown(pluginId, name); err != nil { + content, err := plugins.GetPluginMarkdown(pluginID, name) + if err != nil { if notfound, ok := err.(plugins.PluginNotFoundError); ok { return ApiError(404, notfound.Error(), nil) } return ApiError(500, "Could not get markdown file", err) - } else { - resp := Respond(200, content) - resp.Header("Content-Type", "text/plain; charset=utf-8") - return resp } + + resp := Respond(200, content) + resp.Header("Content-Type", "text/plain; charset=utf-8") + return resp } -func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand) Response { +func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Response { cmd := plugins.ImportDashboardCommand{ OrgId: c.OrgId, diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index 795b8994470..2b85c3dcee1 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -3,12 +3,11 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" ) // POST /api/preferences/set-home-dash -func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Response { +func SetHomeDashboard(c *m.ReqContext, cmd m.SavePreferencesCommand) Response { cmd.UserId = c.UserId cmd.OrgId = c.OrgId @@ -21,12 +20,12 @@ func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Respo } // GET /api/user/preferences -func GetUserPreferences(c *middleware.Context) Response { +func GetUserPreferences(c *m.ReqContext) Response { return getPreferencesFor(c.OrgId, c.UserId) } -func getPreferencesFor(orgId int64, userId int64) Response { - prefsQuery := m.GetPreferencesQuery{UserId: userId, OrgId: orgId} +func getPreferencesFor(orgID int64, userID int64) Response { + prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID} if err := bus.Dispatch(&prefsQuery); err != nil { return ApiError(500, "Failed to get preferences", err) @@ -42,14 +41,14 @@ func getPreferencesFor(orgId int64, userId int64) Response { } // PUT /api/user/preferences -func UpdateUserPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response { +func UpdateUserPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response { return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd) } -func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd) Response { +func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd) Response { saveCmd := m.SavePreferencesCommand{ - UserId: userId, - OrgId: orgId, + UserId: userID, + OrgId: orgID, Theme: dtoCmd.Theme, Timezone: dtoCmd.Timezone, HomeDashboardId: dtoCmd.HomeDashboardId, @@ -63,11 +62,11 @@ func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd } // GET /api/org/preferences -func GetOrgPreferences(c *middleware.Context) Response { +func GetOrgPreferences(c *m.ReqContext) Response { return getPreferencesFor(c.OrgId, 0) } // PUT /api/org/preferences -func UpdateOrgPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response { +func UpdateOrgPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response { return updatePreferencesFor(c.OrgId, 0, &dtoCmd) } diff --git a/pkg/api/quota.go b/pkg/api/quota.go index d8585435430..f92acaf470f 100644 --- a/pkg/api/quota.go +++ b/pkg/api/quota.go @@ -2,12 +2,11 @@ package api import ( "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) -func GetOrgQuotas(c *middleware.Context) Response { +func GetOrgQuotas(c *m.ReqContext) Response { if !setting.Quota.Enabled { return ApiError(404, "Quotas not enabled", nil) } @@ -20,7 +19,7 @@ func GetOrgQuotas(c *middleware.Context) Response { return Json(200, query.Result) } -func UpdateOrgQuota(c *middleware.Context, cmd m.UpdateOrgQuotaCmd) Response { +func UpdateOrgQuota(c *m.ReqContext, cmd m.UpdateOrgQuotaCmd) Response { if !setting.Quota.Enabled { return ApiError(404, "Quotas not enabled", nil) } @@ -37,7 +36,7 @@ func UpdateOrgQuota(c *middleware.Context, cmd m.UpdateOrgQuotaCmd) Response { return ApiSuccess("Organization quota updated") } -func GetUserQuotas(c *middleware.Context) Response { +func GetUserQuotas(c *m.ReqContext) Response { if !setting.Quota.Enabled { return ApiError(404, "Quotas not enabled", nil) } @@ -50,7 +49,7 @@ func GetUserQuotas(c *middleware.Context) Response { return Json(200, query.Result) } -func UpdateUserQuota(c *middleware.Context, cmd m.UpdateUserQuotaCmd) Response { +func UpdateUserQuota(c *m.ReqContext, cmd m.UpdateUserQuotaCmd) Response { if !setting.Quota.Enabled { return ApiError(404, "Quotas not enabled", nil) } diff --git a/pkg/api/render.go b/pkg/api/render.go index 65733cfab15..6e948ed294c 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -5,11 +5,11 @@ import ( "net/http" "github.com/grafana/grafana/pkg/components/renderer" - "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" ) -func RenderToPng(c *middleware.Context) { +func RenderToPng(c *m.ReqContext) { queryReader, err := util.NewUrlQueryReader(c.Req.URL) if err != nil { c.Handle(400, "Render parameters error", err) diff --git a/pkg/api/search.go b/pkg/api/search.go index f79385d83f8..8c2b708d5a2 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -5,40 +5,39 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" ) -func Search(c *middleware.Context) { +func Search(c *m.ReqContext) { query := c.Query("query") tags := c.QueryStrings("tag") starred := c.Query("starred") limit := c.QueryInt("limit") dashboardType := c.Query("type") - permission := models.PERMISSION_VIEW + permission := m.PERMISSION_VIEW if limit == 0 { limit = 1000 } if c.Query("permission") == "Edit" { - permission = models.PERMISSION_EDIT + permission = m.PERMISSION_EDIT } - dbids := make([]int64, 0) + dbIDs := make([]int64, 0) for _, id := range c.QueryStrings("dashboardIds") { - dashboardId, err := strconv.ParseInt(id, 10, 64) + dashboardID, err := strconv.ParseInt(id, 10, 64) if err == nil { - dbids = append(dbids, dashboardId) + dbIDs = append(dbIDs, dashboardID) } } - folderIds := make([]int64, 0) + folderIDs := make([]int64, 0) for _, id := range c.QueryStrings("folderIds") { - folderId, err := strconv.ParseInt(id, 10, 64) + folderID, err := strconv.ParseInt(id, 10, 64) if err == nil { - folderIds = append(folderIds, folderId) + folderIDs = append(folderIDs, folderID) } } @@ -49,9 +48,9 @@ func Search(c *middleware.Context) { Limit: limit, IsStarred: starred == "true", OrgId: c.OrgId, - DashboardIds: dbids, + DashboardIds: dbIDs, Type: dashboardType, - FolderIds: folderIds, + FolderIds: folderIDs, Permission: permission, } diff --git a/pkg/api/signup.go b/pkg/api/signup.go index 36ece023087..838d2f9c0af 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -5,14 +5,13 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) // GET /api/user/signup/options -func GetSignUpOptions(c *middleware.Context) Response { +func GetSignUpOptions(c *m.ReqContext) Response { return Json(200, util.DynMap{ "verifyEmailEnabled": setting.VerifyEmailEnabled, "autoAssignOrg": setting.AutoAssignOrg, @@ -20,7 +19,7 @@ func GetSignUpOptions(c *middleware.Context) Response { } // POST /api/user/signup -func SignUp(c *middleware.Context, form dtos.SignUpForm) Response { +func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response { if !setting.AllowUserSignUp { return ApiError(401, "User signup is disabled", nil) } @@ -52,7 +51,7 @@ func SignUp(c *middleware.Context, form dtos.SignUpForm) Response { return Json(200, util.DynMap{"status": "SignUpCreated"}) } -func SignUpStep2(c *middleware.Context, form dtos.SignUpStep2Form) Response { +func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response { if !setting.AllowUserSignUp { return ApiError(401, "User signup is disabled", nil) } diff --git a/pkg/api/stars.go b/pkg/api/stars.go index c6f9d037eba..5361f64eea6 100644 --- a/pkg/api/stars.go +++ b/pkg/api/stars.go @@ -2,11 +2,10 @@ package api import ( "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" ) -func StarDashboard(c *middleware.Context) Response { +func StarDashboard(c *m.ReqContext) Response { if !c.IsSignedIn { return ApiError(412, "You need to sign in to star dashboards", nil) } @@ -24,7 +23,7 @@ func StarDashboard(c *middleware.Context) Response { return ApiSuccess("Dashboard starred!") } -func UnstarDashboard(c *middleware.Context) Response { +func UnstarDashboard(c *m.ReqContext) Response { cmd := m.UnstarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")} diff --git a/pkg/api/team.go b/pkg/api/team.go index f11eca68b91..13e23df27c8 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -3,13 +3,12 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" ) // POST /api/teams -func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response { +func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response { cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamNameTaken { @@ -25,7 +24,7 @@ func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response { } // PUT /api/teams/:teamId -func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response { +func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":teamId") if err := bus.Dispatch(&cmd); err != nil { @@ -39,7 +38,7 @@ func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response { } // DELETE /api/teams/:teamId -func DeleteTeamById(c *middleware.Context) Response { +func DeleteTeamByID(c *m.ReqContext) Response { if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil { if err == m.ErrTeamNotFound { return ApiError(404, "Failed to delete Team. ID not found", nil) @@ -50,7 +49,7 @@ func DeleteTeamById(c *middleware.Context) Response { } // GET /api/teams/search -func SearchTeams(c *middleware.Context) Response { +func SearchTeams(c *m.ReqContext) Response { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 @@ -83,7 +82,7 @@ func SearchTeams(c *middleware.Context) Response { } // GET /api/teams/:teamId -func GetTeamById(c *middleware.Context) Response { +func GetTeamByID(c *m.ReqContext) Response { query := m.GetTeamByIdQuery{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 8586ac04fdb..4fb05b016e3 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -3,13 +3,12 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" ) // GET /api/teams/:teamId/members -func GetTeamMembers(c *middleware.Context) Response { +func GetTeamMembers(c *m.ReqContext) Response { query := m.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")} if err := bus.Dispatch(&query); err != nil { @@ -24,7 +23,7 @@ func GetTeamMembers(c *middleware.Context) Response { } // POST /api/teams/:teamId/members -func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response { +func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response { cmd.TeamId = c.ParamsInt64(":teamId") cmd.OrgId = c.OrgId @@ -46,7 +45,7 @@ func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response { } // DELETE /api/teams/:teamId/members/:userId -func RemoveTeamMember(c *middleware.Context) Response { +func RemoveTeamMember(c *m.ReqContext) Response { if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil { if err == m.ErrTeamNotFound { return ApiError(404, "Team not found", nil) diff --git a/pkg/api/user.go b/pkg/api/user.go index 9a041d30272..4469fab2d29 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -3,24 +3,23 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) // GET /api/user (current authenticated user) -func GetSignedInUser(c *middleware.Context) Response { +func GetSignedInUser(c *m.ReqContext) Response { return getUserUserProfile(c.UserId) } // GET /api/users/:id -func GetUserById(c *middleware.Context) Response { +func GetUserByID(c *m.ReqContext) Response { return getUserUserProfile(c.ParamsInt64(":id")) } -func getUserUserProfile(userId int64) Response { - query := m.GetUserProfileQuery{UserId: userId} +func getUserUserProfile(userID int64) Response { + query := m.GetUserProfileQuery{UserId: userID} if err := bus.Dispatch(&query); err != nil { if err == m.ErrUserNotFound { @@ -33,7 +32,7 @@ func getUserUserProfile(userId int64) Response { } // GET /api/users/lookup -func GetUserByLoginOrEmail(c *middleware.Context) Response { +func GetUserByLoginOrEmail(c *m.ReqContext) Response { query := m.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")} if err := bus.Dispatch(&query); err != nil { if err == m.ErrUserNotFound { @@ -55,7 +54,7 @@ func GetUserByLoginOrEmail(c *middleware.Context) Response { } // POST /api/user -func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response { +func UpdateSignedInUser(c *m.ReqContext, cmd m.UpdateUserCommand) Response { if setting.AuthProxyEnabled { if setting.AuthProxyHeaderProperty == "email" && cmd.Email != c.Email { return ApiError(400, "Not allowed to change email when auth proxy is using email property", nil) @@ -69,21 +68,21 @@ func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response } // POST /api/users/:id -func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) Response { +func UpdateUser(c *m.ReqContext, cmd m.UpdateUserCommand) Response { cmd.UserId = c.ParamsInt64(":id") return handleUpdateUser(cmd) } //POST /api/users/:id/using/:orgId -func UpdateUserActiveOrg(c *middleware.Context) Response { - userId := c.ParamsInt64(":id") - orgId := c.ParamsInt64(":orgId") +func UpdateUserActiveOrg(c *m.ReqContext) Response { + userID := c.ParamsInt64(":id") + orgID := c.ParamsInt64(":orgId") - if !validateUsingOrg(userId, orgId) { + if !validateUsingOrg(userID, orgID) { return ApiError(401, "Not a valid organization", nil) } - cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId} + cmd := m.SetUsingOrgCommand{UserId: userID, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { return ApiError(500, "Failed to change active organization", err) @@ -108,17 +107,17 @@ func handleUpdateUser(cmd m.UpdateUserCommand) Response { } // GET /api/user/orgs -func GetSignedInUserOrgList(c *middleware.Context) Response { +func GetSignedInUserOrgList(c *m.ReqContext) Response { return getUserOrgList(c.UserId) } // GET /api/user/:id/orgs -func GetUserOrgList(c *middleware.Context) Response { +func GetUserOrgList(c *m.ReqContext) Response { return getUserOrgList(c.ParamsInt64(":id")) } -func getUserOrgList(userId int64) Response { - query := m.GetUserOrgListQuery{UserId: userId} +func getUserOrgList(userID int64) Response { + query := m.GetUserOrgListQuery{UserId: userID} if err := bus.Dispatch(&query); err != nil { return ApiError(500, "Failed to get user organizations", err) @@ -127,8 +126,8 @@ func getUserOrgList(userId int64) Response { return Json(200, query.Result) } -func validateUsingOrg(userId int64, orgId int64) bool { - query := m.GetUserOrgListQuery{UserId: userId} +func validateUsingOrg(userID int64, orgID int64) bool { + query := m.GetUserOrgListQuery{UserId: userID} if err := bus.Dispatch(&query); err != nil { return false @@ -137,7 +136,7 @@ func validateUsingOrg(userId int64, orgId int64) bool { // validate that the org id in the list valid := false for _, other := range query.Result { - if other.OrgId == orgId { + if other.OrgId == orgID { valid = true } } @@ -146,14 +145,14 @@ func validateUsingOrg(userId int64, orgId int64) bool { } // POST /api/user/using/:id -func UserSetUsingOrg(c *middleware.Context) Response { - orgId := c.ParamsInt64(":id") +func UserSetUsingOrg(c *m.ReqContext) Response { + orgID := c.ParamsInt64(":id") - if !validateUsingOrg(c.UserId, orgId) { + if !validateUsingOrg(c.UserId, orgID) { return ApiError(401, "Not a valid organization", nil) } - cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId} + cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { return ApiError(500, "Failed to change active organization", err) @@ -163,14 +162,14 @@ func UserSetUsingOrg(c *middleware.Context) Response { } // GET /profile/switch-org/:id -func ChangeActiveOrgAndRedirectToHome(c *middleware.Context) { - orgId := c.ParamsInt64(":id") +func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) { + orgID := c.ParamsInt64(":id") - if !validateUsingOrg(c.UserId, orgId) { + if !validateUsingOrg(c.UserId, orgID) { NotFoundHandler(c) } - cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId} + cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { NotFoundHandler(c) @@ -179,7 +178,7 @@ func ChangeActiveOrgAndRedirectToHome(c *middleware.Context) { c.Redirect(setting.AppSubUrl + "/") } -func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) Response { +func ChangeUserPassword(c *m.ReqContext, cmd m.ChangeUserPasswordCommand) Response { if setting.LdapEnabled || setting.AuthProxyEnabled { return ApiError(400, "Not allowed to change password when LDAP or Auth Proxy is enabled", nil) } @@ -211,7 +210,7 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) } // GET /api/users -func SearchUsers(c *middleware.Context) Response { +func SearchUsers(c *m.ReqContext) Response { query, err := searchUser(c) if err != nil { return ApiError(500, "Failed to fetch users", err) @@ -221,7 +220,7 @@ func SearchUsers(c *middleware.Context) Response { } // GET /api/users/search -func SearchUsersWithPaging(c *middleware.Context) Response { +func SearchUsersWithPaging(c *m.ReqContext) Response { query, err := searchUser(c) if err != nil { return ApiError(500, "Failed to fetch users", err) @@ -230,7 +229,7 @@ func SearchUsersWithPaging(c *middleware.Context) Response { return Json(200, query.Result) } -func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) { +func searchUser(c *m.ReqContext) (*m.SearchUsersQuery, error) { perPage := c.QueryInt("perpage") if perPage <= 0 { perPage = 1000 @@ -258,7 +257,7 @@ func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) { return query, nil } -func SetHelpFlag(c *middleware.Context) Response { +func SetHelpFlag(c *m.ReqContext) Response { flag := c.ParamsInt64(":id") bitmask := &c.HelpFlags1 @@ -276,7 +275,7 @@ func SetHelpFlag(c *middleware.Context) Response { return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1}) } -func ClearHelpFlags(c *middleware.Context) Response { +func ClearHelpFlags(c *m.ReqContext) Response { cmd := m.SetUserHelpFlagCommand{ UserId: c.UserId, HelpFlags1: m.HelpFlags1(0), diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 8ed3196e4ad..0338b3d6aa2 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -120,7 +120,7 @@ func (g *GrafanaServerImpl) initLogging() { } func (g *GrafanaServerImpl) startHttpServer() error { - g.httpServer = api.NewHttpServer() + g.httpServer = api.NewHTTPServer() err := g.httpServer.Start(g.context) diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index 313f7892707..26751ddd5c7 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -72,7 +72,9 @@ func RenderToPng(params *RenderOpts) (string, error) { localDomain = setting.HttpAddr } - url := fmt.Sprintf("%s://%s:%s/%s", setting.Protocol, localDomain, setting.HttpPort, params.Path) + // &render=1 signals to the legacy redirect layer to + // avoid redirect these requests. + url := fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, localDomain, setting.HttpPort, params.Path) binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable)) scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js")) diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 65697a616ea..37e79c01071 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -7,6 +7,7 @@ import ( "gopkg.in/macaron.v1" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" ) @@ -15,17 +16,17 @@ type AuthOptions struct { ReqSignedIn bool } -func getRequestUserId(c *Context) int64 { - userId := c.Session.Get(SESS_KEY_USERID) +func getRequestUserId(c *m.ReqContext) int64 { + userID := c.Session.Get(session.SESS_KEY_USERID) - if userId != nil { - return userId.(int64) + if userID != nil { + return userID.(int64) } return 0 } -func getApiKey(c *Context) string { +func getApiKey(c *m.ReqContext) string { header := c.Req.Header.Get("Authorization") parts := strings.SplitN(header, " ", 2) if len(parts) == 2 && parts[0] == "Bearer" { @@ -36,7 +37,7 @@ func getApiKey(c *Context) string { return "" } -func accessForbidden(c *Context) { +func accessForbidden(c *m.ReqContext) { if c.IsApiRequest() { c.JsonApiErr(403, "Permission denied", nil) return @@ -45,7 +46,7 @@ func accessForbidden(c *Context) { c.Redirect(setting.AppSubUrl + "/") } -func notAuthorized(c *Context) { +func notAuthorized(c *m.ReqContext) { if c.IsApiRequest() { c.JsonApiErr(401, "Unauthorized", nil) return @@ -57,7 +58,7 @@ func notAuthorized(c *Context) { } func RoleAuth(roles ...m.RoleType) macaron.Handler { - return func(c *Context) { + return func(c *m.ReqContext) { ok := false for _, role := range roles { if role == c.OrgRole { @@ -72,7 +73,7 @@ func RoleAuth(roles ...m.RoleType) macaron.Handler { } func Auth(options *AuthOptions) macaron.Handler { - return func(c *Context) { + return func(c *m.ReqContext) { if !c.IsSignedIn && options.ReqSignedIn && !c.AllowAnonymous { notAuthorized(c) return diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index 3247805ec09..4d2a7a98908 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -10,10 +10,11 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/login" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" ) -func initContextWithAuthProxy(ctx *Context, orgId int64) bool { +func initContextWithAuthProxy(ctx *m.ReqContext, orgId int64) bool { if !setting.AuthProxyEnabled { return false } @@ -58,7 +59,7 @@ func initContextWithAuthProxy(ctx *Context, orgId int64) bool { } // initialize session - if err := ctx.Session.Start(ctx); err != nil { + if err := ctx.Session.Start(ctx.Context); err != nil { log.Error(3, "Failed to start session", err) return false } @@ -66,12 +67,12 @@ func initContextWithAuthProxy(ctx *Context, orgId int64) bool { // Make sure that we cannot share a session between different users! if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId { // remove session - if err := ctx.Session.Destory(ctx); err != nil { + if err := ctx.Session.Destory(ctx.Context); err != nil { log.Error(3, "Failed to destroy session, err") } // initialize a new session - if err := ctx.Session.Start(ctx); err != nil { + if err := ctx.Session.Start(ctx.Context); err != nil { log.Error(3, "Failed to start session", err) } } @@ -89,17 +90,17 @@ func initContextWithAuthProxy(ctx *Context, orgId int64) bool { ctx.SignedInUser = query.Result ctx.IsSignedIn = true - ctx.Session.Set(SESS_KEY_USERID, ctx.UserId) + ctx.Session.Set(session.SESS_KEY_USERID, ctx.UserId) return true } -var syncGrafanaUserWithLdapUser = func(ctx *Context, query *m.GetSignedInUserQuery) error { +var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error { if setting.LdapEnabled { expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix() var lastLdapSync int64 - if lastLdapSyncInSession := ctx.Session.Get(SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil { + if lastLdapSyncInSession := ctx.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil { lastLdapSync = lastLdapSyncInSession.(int64) } @@ -113,14 +114,14 @@ var syncGrafanaUserWithLdapUser = func(ctx *Context, query *m.GetSignedInUserQue } } - ctx.Session.Set(SESS_KEY_LASTLDAPSYNC, time.Now().Unix()) + ctx.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix()) } } return nil } -func checkAuthenticationProxy(ctx *Context, proxyHeaderValue string) error { +func checkAuthenticationProxy(ctx *m.ReqContext, proxyHeaderValue string) error { if len(strings.TrimSpace(setting.AuthProxyWhitelist)) > 0 { proxies := strings.Split(setting.AuthProxyWhitelist, ",") remoteAddrSplit := strings.Split(ctx.Req.RemoteAddr, ":") diff --git a/pkg/middleware/auth_proxy_test.go b/pkg/middleware/auth_proxy_test.go index 4da0f52bbcf..b3c011bd870 100644 --- a/pkg/middleware/auth_proxy_test.go +++ b/pkg/middleware/auth_proxy_test.go @@ -6,8 +6,10 @@ import ( "github.com/grafana/grafana/pkg/login" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" + "gopkg.in/macaron.v1" ) func TestAuthProxyWithLdapEnabled(t *testing.T) { @@ -29,45 +31,45 @@ func TestAuthProxyWithLdapEnabled(t *testing.T) { Convey("When session variable lastLdapSync not set, call syncSignedInUser and set lastLdapSync", func() { // arrange - session := mockSession{} - ctx := Context{Session: &session} - So(session.Get(SESS_KEY_LASTLDAPSYNC), ShouldBeNil) + sess := mockSession{} + ctx := m.ReqContext{Session: &sess} + So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeNil) // act syncGrafanaUserWithLdapUser(&ctx, &query) // assert So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue) - So(session.Get(SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, 0) + So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, 0) }) Convey("When session variable not expired, don't sync and don't change session var", func() { // arrange - session := mockSession{} - ctx := Context{Session: &session} + sess := mockSession{} + ctx := m.ReqContext{Session: &sess} now := time.Now().Unix() - session.Set(SESS_KEY_LASTLDAPSYNC, now) + sess.Set(session.SESS_KEY_LASTLDAPSYNC, now) // act syncGrafanaUserWithLdapUser(&ctx, &query) // assert - So(session.Get(SESS_KEY_LASTLDAPSYNC), ShouldEqual, now) + So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldEqual, now) So(mockLdapAuther.syncSignedInUserCalled, ShouldBeFalse) }) Convey("When lastldapsync is expired, session variable should be updated", func() { // arrange - session := mockSession{} - ctx := Context{Session: &session} + sess := mockSession{} + ctx := m.ReqContext{Session: &sess} expiredTime := time.Now().Add(time.Duration(-120) * time.Minute).Unix() - session.Set(SESS_KEY_LASTLDAPSYNC, expiredTime) + sess.Set(session.SESS_KEY_LASTLDAPSYNC, expiredTime) // act syncGrafanaUserWithLdapUser(&ctx, &query) // assert - So(session.Get(SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime) + So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime) So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue) }) }) @@ -77,7 +79,7 @@ type mockSession struct { value interface{} } -func (s *mockSession) Start(c *Context) error { +func (s *mockSession) Start(c *macaron.Context) error { return nil } @@ -102,11 +104,11 @@ func (s *mockSession) Release() error { return nil } -func (s *mockSession) Destory(c *Context) error { +func (s *mockSession) Destory(c *macaron.Context) error { return nil } -func (s *mockSession) RegenerateId(c *Context) error { +func (s *mockSession) RegenerateId(c *macaron.Context) error { return nil } diff --git a/pkg/middleware/dashboard_redirect.go b/pkg/middleware/dashboard_redirect.go index 4a3812fb8a2..024b112e154 100644 --- a/pkg/middleware/dashboard_redirect.go +++ b/pkg/middleware/dashboard_redirect.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" "gopkg.in/macaron.v1" ) @@ -19,8 +20,8 @@ func getDashboardUrlBySlug(orgId int64, slug string) (string, error) { return m.GetDashboardUrl(query.Result.Uid, query.Result.Slug), nil } -func RedirectFromLegacyDashboardUrl() macaron.Handler { - return func(c *Context) { +func RedirectFromLegacyDashboardURL() macaron.Handler { + return func(c *m.ReqContext) { slug := c.Params("slug") if slug != "" { @@ -34,11 +35,16 @@ func RedirectFromLegacyDashboardUrl() macaron.Handler { } func RedirectFromLegacyDashboardSoloUrl() macaron.Handler { - return func(c *Context) { + return func(c *m.ReqContext) { slug := c.Params("slug") + renderRequest := c.QueryBool("render") if slug != "" { if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil { + if renderRequest && strings.Contains(url, setting.AppSubUrl) { + url = strings.Replace(url, setting.AppSubUrl, "", 1) + } + url = strings.Replace(url, "/d/", "/d-solo/", 1) url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) c.Redirect(url, 301) diff --git a/pkg/middleware/dashboard_redirect_test.go b/pkg/middleware/dashboard_redirect_test.go index 0af06347ed0..24eab2d7b79 100644 --- a/pkg/middleware/dashboard_redirect_test.go +++ b/pkg/middleware/dashboard_redirect_test.go @@ -13,7 +13,7 @@ import ( func TestMiddlewareDashboardRedirect(t *testing.T) { Convey("Given the dashboard redirect middleware", t, func() { bus.ClearBusHandlers() - redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardUrl() + redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloUrl() fakeDash := m.NewDashboard("Child dash") @@ -34,9 +34,9 @@ func TestMiddlewareDashboardRedirect(t *testing.T) { Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { So(sc.resp.Code, ShouldEqual, 301) - redirectUrl, _ := sc.resp.Result().Location() - So(redirectUrl.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)) - So(len(redirectUrl.Query()), ShouldEqual, 2) + redirectURL, _ := sc.resp.Result().Location() + So(redirectURL.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)) + So(len(redirectURL.Query()), ShouldEqual, 2) }) }) @@ -47,11 +47,11 @@ func TestMiddlewareDashboardRedirect(t *testing.T) { Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { So(sc.resp.Code, ShouldEqual, 301) - redirectUrl, _ := sc.resp.Result().Location() - expectedUrl := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) - expectedUrl = strings.Replace(expectedUrl, "/d/", "/d-solo/", 1) - So(redirectUrl.Path, ShouldEqual, expectedUrl) - So(len(redirectUrl.Query()), ShouldEqual, 2) + redirectURL, _ := sc.resp.Result().Location() + expectedURL := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) + expectedURL = strings.Replace(expectedURL, "/d/", "/d-solo/", 1) + So(redirectURL.Path, ShouldEqual, expectedURL) + So(len(redirectURL.Query()), ShouldEqual, 2) }) }) }) diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go index 94f707800be..2c63810b9c8 100644 --- a/pkg/middleware/logger.go +++ b/pkg/middleware/logger.go @@ -19,6 +19,7 @@ import ( "net/http" "time" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" "gopkg.in/macaron.v1" @@ -47,7 +48,7 @@ func Logger() macaron.Handler { } if ctx, ok := c.Data["ctx"]; ok { - ctxTyped := ctx.(*Context) + ctxTyped := ctx.(*m.ReqContext) if status == 500 { ctxTyped.Logger.Error("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ms", int64(timeTakenMs), "size", rw.Size(), "referer", req.Referer()) } else { diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index f0c952811cd..b5b244d5bff 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -2,7 +2,6 @@ package middleware import ( "strconv" - "strings" "gopkg.in/macaron.v1" @@ -11,29 +10,17 @@ import ( "github.com/grafana/grafana/pkg/log" l "github.com/grafana/grafana/pkg/login" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - "github.com/prometheus/client_golang/prometheus" ) -type Context struct { - *macaron.Context - *m.SignedInUser - - Session SessionStore - - IsSignedIn bool - IsRenderCall bool - AllowAnonymous bool - Logger log.Logger -} - func GetContextHandler() macaron.Handler { return func(c *macaron.Context) { - ctx := &Context{ + ctx := &m.ReqContext{ Context: c, SignedInUser: &m.SignedInUser{}, - Session: GetSession(), + Session: session.GetSession(), IsSignedIn: false, AllowAnonymous: false, Logger: log.New("context"), @@ -74,7 +61,7 @@ func GetContextHandler() macaron.Handler { } } -func initContextWithAnonymousUser(ctx *Context) bool { +func initContextWithAnonymousUser(ctx *m.ReqContext) bool { if !setting.AnonymousEnabled { return false } @@ -94,9 +81,9 @@ func initContextWithAnonymousUser(ctx *Context) bool { return true } -func initContextWithUserSessionCookie(ctx *Context, orgId int64) bool { +func initContextWithUserSessionCookie(ctx *m.ReqContext, orgId int64) bool { // initialize session - if err := ctx.Session.Start(ctx); err != nil { + if err := ctx.Session.Start(ctx.Context); err != nil { ctx.Logger.Error("Failed to start session", "error", err) return false } @@ -117,7 +104,7 @@ func initContextWithUserSessionCookie(ctx *Context, orgId int64) bool { return true } -func initContextWithApiKey(ctx *Context) bool { +func initContextWithApiKey(ctx *m.ReqContext) bool { var keyString string if keyString = getApiKey(ctx); keyString == "" { return false @@ -153,7 +140,7 @@ func initContextWithApiKey(ctx *Context) bool { return true } -func initContextWithBasicAuth(ctx *Context, orgId int64) bool { +func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool { if !setting.BasicAuthEnabled { return false @@ -195,70 +182,8 @@ func initContextWithBasicAuth(ctx *Context, orgId int64) bool { return true } -// Handle handles and logs error by given status. -func (ctx *Context) Handle(status int, title string, err error) { - if err != nil { - ctx.Logger.Error(title, "error", err) - if setting.Env != setting.PROD { - ctx.Data["ErrorMsg"] = err - } - } - - ctx.Data["Title"] = title - ctx.Data["AppSubUrl"] = setting.AppSubUrl - ctx.Data["Theme"] = "dark" - - ctx.HTML(status, "error") -} - -func (ctx *Context) JsonOK(message string) { - resp := make(map[string]interface{}) - resp["message"] = message - ctx.JSON(200, resp) -} - -func (ctx *Context) IsApiRequest() bool { - return strings.HasPrefix(ctx.Req.URL.Path, "/api") -} - -func (ctx *Context) JsonApiErr(status int, message string, err error) { - resp := make(map[string]interface{}) - - if err != nil { - ctx.Logger.Error(message, "error", err) - if setting.Env != setting.PROD { - resp["error"] = err.Error() - } - } - - switch status { - case 404: - resp["message"] = "Not Found" - case 500: - resp["message"] = "Internal Server Error" - } - - if message != "" { - resp["message"] = message - } - - ctx.JSON(status, resp) -} - -func (ctx *Context) HasUserRole(role m.RoleType) bool { - return ctx.OrgRole.Includes(role) -} - -func (ctx *Context) HasHelpFlag(flag m.HelpFlags1) bool { - return ctx.HelpFlags1.HasFlag(flag) -} - -func (ctx *Context) TimeRequest(timer prometheus.Summary) { - ctx.Data["perfmon.timer"] = timer -} - func AddDefaultResponseHeaders() macaron.Handler { - return func(ctx *Context) { + return func(ctx *m.ReqContext) { if ctx.IsApiRequest() && ctx.Req.Method == "GET" { ctx.Resp.Header().Add("Cache-Control", "no-cache") ctx.Resp.Header().Add("Pragma", "no-cache") diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index ffd8e8a0af0..c8e9e535cfa 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -7,10 +7,11 @@ import ( "path/filepath" "testing" - "github.com/go-macaron/session" + ms "github.com/go-macaron/session" "github.com/grafana/grafana/pkg/bus" l "github.com/grafana/grafana/pkg/login" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" . "github.com/smartystreets/goconvey/convey" @@ -130,8 +131,8 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario("UserId in session", func(sc *scenarioContext) { - sc.fakeReq("GET", "/").handler(func(c *Context) { - c.Session.Set(SESS_KEY_USERID, int64(12)) + sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) { + c.Session.Set(session.SESS_KEY_USERID, int64(12)) }).exec() bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { @@ -276,8 +277,8 @@ func TestMiddlewareContext(t *testing.T) { }) // create session - sc.fakeReq("GET", "/").handler(func(c *Context) { - c.Session.Set(SESS_KEY_USERID, int64(33)) + sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) { + c.Session.Set(session.SESS_KEY_USERID, int64(33)) }).exec() oldSessionID := sc.context.Session.ID() @@ -300,7 +301,7 @@ func TestMiddlewareContext(t *testing.T) { setting.LdapEnabled = true called := false - syncGrafanaUserWithLdapUser = func(ctx *Context, query *m.GetSignedInUserQuery) error { + syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error { called = true return nil } @@ -336,12 +337,12 @@ func middlewareScenario(desc string, fn scenarioFunc) { sc.m.Use(GetContextHandler()) // mock out gc goroutine - startSessionGC = func() {} - sc.m.Use(Sessioner(&session.Options{})) + session.StartSessionGC = func() {} + sc.m.Use(Sessioner(&ms.Options{}, 0)) sc.m.Use(OrgRedirect()) sc.m.Use(AddDefaultResponseHeaders()) - sc.defaultHandler = func(c *Context) { + sc.defaultHandler = func(c *m.ReqContext) { sc.context = c if sc.handlerFunc != nil { sc.handlerFunc(sc.context) @@ -356,7 +357,7 @@ func middlewareScenario(desc string, fn scenarioFunc) { type scenarioContext struct { m *macaron.Macaron - context *Context + context *m.ReqContext resp *httptest.ResponseRecorder apiKey string authHeader string @@ -436,4 +437,4 @@ func (sc *scenarioContext) exec() { } type scenarioFunc func(c *scenarioContext) -type handlerFunc func(c *Context) +type handlerFunc func(c *m.ReqContext) diff --git a/pkg/middleware/org_redirect.go b/pkg/middleware/org_redirect.go index 9dd764be1bb..db263c2a17a 100644 --- a/pkg/middleware/org_redirect.go +++ b/pkg/middleware/org_redirect.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/models" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "gopkg.in/macaron.v1" @@ -22,7 +22,7 @@ func OrgRedirect() macaron.Handler { return } - ctx, ok := c.Data["ctx"].(*Context) + ctx, ok := c.Data["ctx"].(*m.ReqContext) if !ok || !ctx.IsSignedIn { return } @@ -31,7 +31,7 @@ func OrgRedirect() macaron.Handler { return } - cmd := models.SetUsingOrgCommand{UserId: ctx.UserId, OrgId: orgId} + cmd := m.SetUsingOrgCommand{UserId: ctx.UserId, OrgId: orgId} if err := bus.Dispatch(&cmd); err != nil { if ctx.IsApiRequest() { ctx.JsonApiErr(404, "Not found", nil) diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go index 1f3d01f30f2..fa08154b250 100644 --- a/pkg/middleware/org_redirect_test.go +++ b/pkg/middleware/org_redirect_test.go @@ -6,7 +6,8 @@ import ( "fmt" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/models" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/session" . "github.com/smartystreets/goconvey/convey" ) @@ -14,16 +15,16 @@ func TestOrgRedirectMiddleware(t *testing.T) { Convey("Can redirect to correct org", t, func() { middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) { - sc.fakeReq("GET", "/").handler(func(c *Context) { - c.Session.Set(SESS_KEY_USERID, int64(12)) + sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) { + c.Session.Set(session.SESS_KEY_USERID, int64(12)) }).exec() - bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error { + bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error { return nil }) - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 1, UserId: 12} + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 1, UserId: 12} return nil }) @@ -36,16 +37,16 @@ func TestOrgRedirectMiddleware(t *testing.T) { }) middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) { - sc.fakeReq("GET", "/").handler(func(c *Context) { - c.Session.Set(SESS_KEY_USERID, int64(12)) + sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) { + c.Session.Set(session.SESS_KEY_USERID, int64(12)) }).exec() - bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error { + bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error { return fmt.Errorf("") }) - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 1, UserId: 12} + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 1, UserId: 12} return nil }) diff --git a/pkg/middleware/perf.go b/pkg/middleware/perf.go index e381121a47f..5b6ab6f2d0a 100644 --- a/pkg/middleware/perf.go +++ b/pkg/middleware/perf.go @@ -4,9 +4,11 @@ import ( "net/http" "gopkg.in/macaron.v1" + + m "github.com/grafana/grafana/pkg/models" ) func MeasureRequestTime() macaron.Handler { - return func(res http.ResponseWriter, req *http.Request, c *Context) { + return func(res http.ResponseWriter, req *http.Request, c *m.ReqContext) { } } diff --git a/pkg/middleware/quota.go b/pkg/middleware/quota.go index 23f98e78a7e..43efca43485 100644 --- a/pkg/middleware/quota.go +++ b/pkg/middleware/quota.go @@ -3,15 +3,15 @@ package middleware import ( "fmt" - "github.com/grafana/grafana/pkg/bus" - m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" "gopkg.in/macaron.v1" + + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/quota" ) func Quota(target string) macaron.Handler { - return func(c *Context) { - limitReached, err := QuotaReached(c, target) + return func(c *m.ReqContext) { + limitReached, err := quota.QuotaReached(c, target) if err != nil { c.JsonApiErr(500, "failed to get quota", err) return @@ -22,82 +22,3 @@ func Quota(target string) macaron.Handler { } } } - -func QuotaReached(c *Context, target string) (bool, error) { - if !setting.Quota.Enabled { - return false, nil - } - - // get the list of scopes that this target is valid for. Org, User, Global - scopes, err := m.GetQuotaScopes(target) - if err != nil { - return false, err - } - - for _, scope := range scopes { - c.Logger.Debug("Checking quota", "target", target, "scope", scope) - - switch scope.Name { - case "global": - if scope.DefaultLimit < 0 { - continue - } - if scope.DefaultLimit == 0 { - return true, nil - } - if target == "session" { - usedSessions := getSessionCount() - if int64(usedSessions) > scope.DefaultLimit { - c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit) - return true, nil - } - continue - } - query := m.GetGlobalQuotaByTargetQuery{Target: scope.Target} - if err := bus.Dispatch(&query); err != nil { - return true, err - } - if query.Result.Used >= scope.DefaultLimit { - return true, nil - } - case "org": - if !c.IsSignedIn { - continue - } - query := m.GetOrgQuotaByTargetQuery{OrgId: c.OrgId, Target: scope.Target, Default: scope.DefaultLimit} - if err := bus.Dispatch(&query); err != nil { - return true, err - } - if query.Result.Limit < 0 { - continue - } - if query.Result.Limit == 0 { - return true, nil - } - - if query.Result.Used >= query.Result.Limit { - return true, nil - } - case "user": - if !c.IsSignedIn || c.UserId == 0 { - continue - } - query := m.GetUserQuotaByTargetQuery{UserId: c.UserId, Target: scope.Target, Default: scope.DefaultLimit} - if err := bus.Dispatch(&query); err != nil { - return true, err - } - if query.Result.Limit < 0 { - continue - } - if query.Result.Limit == 0 { - return true, nil - } - - if query.Result.Used >= query.Result.Limit { - return true, nil - } - } - } - - return false, nil -} diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index b68aa485fa7..92c3d62674d 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" ) @@ -12,7 +13,7 @@ import ( func TestMiddlewareQuota(t *testing.T) { Convey("Given the grafana quota middleware", t, func() { - getSessionCount = func() int { + session.GetSessionCount = func() int { return 4 } @@ -74,8 +75,8 @@ func TestMiddlewareQuota(t *testing.T) { middlewareScenario("with user logged in", func(sc *scenarioContext) { // log us in, so we have a user_id and org_id in the context - sc.fakeReq("GET", "/").handler(func(c *Context) { - c.Session.Set(SESS_KEY_USERID, int64(12)) + sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) { + c.Session.Set(session.SESS_KEY_USERID, int64(12)) }).exec() bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go index a8bdf809637..ec289387aa4 100644 --- a/pkg/middleware/recovery.go +++ b/pkg/middleware/recovery.go @@ -24,6 +24,7 @@ import ( "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -106,7 +107,7 @@ func Recovery() macaron.Handler { panicLogger := log.Root // try to get request logger if ctx, ok := c.Data["ctx"]; ok { - ctxTyped := ctx.(*Context) + ctxTyped := ctx.(*m.ReqContext) panicLogger = ctxTyped.Logger } @@ -123,7 +124,7 @@ func Recovery() macaron.Handler { c.Data["ErrorMsg"] = string(stack) } - ctx, ok := c.Data["ctx"].(*Context) + ctx, ok := c.Data["ctx"].(*m.ReqContext) if ok && ctx.IsApiRequest() { resp := make(map[string]interface{}) diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 299186945ee..4bbedbc3b21 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -4,18 +4,20 @@ import ( "path/filepath" "testing" - "github.com/go-macaron/session" + ms "github.com/go-macaron/session" "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/session" . "github.com/smartystreets/goconvey/convey" "gopkg.in/macaron.v1" ) func TestRecoveryMiddleware(t *testing.T) { Convey("Given an api route that panics", t, func() { - apiUrl := "/api/whatever" - recoveryScenario("recovery middleware should return json", apiUrl, func(sc *scenarioContext) { + apiURL := "/api/whatever" + recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) { sc.handlerFunc = PanicHandler - sc.fakeReq("GET", apiUrl).exec() + sc.fakeReq("GET", apiURL).exec() sc.req.Header.Add("content-type", "application/json") So(sc.resp.Code, ShouldEqual, 500) @@ -25,10 +27,10 @@ func TestRecoveryMiddleware(t *testing.T) { }) Convey("Given a non-api route that panics", t, func() { - apiUrl := "/whatever" - recoveryScenario("recovery middleware should return html", apiUrl, func(sc *scenarioContext) { + apiURL := "/whatever" + recoveryScenario("recovery middleware should return html", apiURL, func(sc *scenarioContext) { sc.handlerFunc = PanicHandler - sc.fakeReq("GET", apiUrl).exec() + sc.fakeReq("GET", apiURL).exec() So(sc.resp.Code, ShouldEqual, 500) So(sc.resp.Header().Get("content-type"), ShouldEqual, "text/html; charset=UTF-8") @@ -37,7 +39,7 @@ func TestRecoveryMiddleware(t *testing.T) { }) } -func PanicHandler(c *Context) { +func PanicHandler(c *m.ReqContext) { panic("Handler has panicked") } @@ -60,12 +62,12 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) { sc.m.Use(GetContextHandler()) // mock out gc goroutine - startSessionGC = func() {} - sc.m.Use(Sessioner(&session.Options{})) + session.StartSessionGC = func() {} + sc.m.Use(Sessioner(&ms.Options{}, 0)) sc.m.Use(OrgRedirect()) sc.m.Use(AddDefaultResponseHeaders()) - sc.defaultHandler = func(c *Context) { + sc.defaultHandler = func(c *m.ReqContext) { sc.context = c if sc.handlerFunc != nil { sc.handlerFunc(sc.context) diff --git a/pkg/middleware/render_auth.go b/pkg/middleware/render_auth.go index d2f9c1b2b1a..225645e659e 100644 --- a/pkg/middleware/render_auth.go +++ b/pkg/middleware/render_auth.go @@ -10,7 +10,7 @@ import ( var renderKeysLock sync.Mutex var renderKeys map[string]*m.SignedInUser = make(map[string]*m.SignedInUser) -func initContextWithRenderAuth(ctx *Context) bool { +func initContextWithRenderAuth(ctx *m.ReqContext) bool { key := ctx.GetCookie("renderKey") if key == "" { return false diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index 4de111ff3d2..19cfa368b49 100644 --- a/pkg/middleware/session.go +++ b/pkg/middleware/session.go @@ -1,170 +1,21 @@ package middleware import ( - "math/rand" - "time" - - "github.com/go-macaron/session" - _ "github.com/go-macaron/session/memcache" - _ "github.com/go-macaron/session/mysql" - _ "github.com/go-macaron/session/postgres" - _ "github.com/go-macaron/session/redis" - "github.com/grafana/grafana/pkg/log" + ms "github.com/go-macaron/session" "gopkg.in/macaron.v1" + + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/session" ) -const ( - SESS_KEY_USERID = "uid" - SESS_KEY_OAUTH_STATE = "state" - SESS_KEY_APIKEY = "apikey_id" // used for render requests with api keys - SESS_KEY_LASTLDAPSYNC = "last_ldap_sync" -) +func Sessioner(options *ms.Options, sessionConnMaxLifetime int64) macaron.Handler { + session.Init(options, sessionConnMaxLifetime) -var sessionManager *session.Manager -var sessionOptions *session.Options -var startSessionGC func() -var getSessionCount func() int -var sessionLogger = log.New("session") - -func init() { - startSessionGC = func() { - sessionManager.GC() - sessionLogger.Debug("Session GC") - time.AfterFunc(time.Duration(sessionOptions.Gclifetime)*time.Second, startSessionGC) - } - getSessionCount = func() int { - return sessionManager.Count() - } -} - -func prepareOptions(opt *session.Options) *session.Options { - if len(opt.Provider) == 0 { - opt.Provider = "memory" - } - if len(opt.ProviderConfig) == 0 { - opt.ProviderConfig = "data/sessions" - } - if len(opt.CookieName) == 0 { - opt.CookieName = "grafana_sess" - } - if len(opt.CookiePath) == 0 { - opt.CookiePath = "/" - } - if opt.Gclifetime == 0 { - opt.Gclifetime = 3600 - } - if opt.Maxlifetime == 0 { - opt.Maxlifetime = opt.Gclifetime - } - if opt.IDLength == 0 { - opt.IDLength = 16 - } - - return opt -} - -func Sessioner(options *session.Options) macaron.Handler { - var err error - sessionOptions = prepareOptions(options) - sessionManager, err = session.NewManager(options.Provider, *options) - if err != nil { - panic(err) - } - - // start GC threads after some random seconds - rndSeconds := 10 + rand.Int63n(180) - time.AfterFunc(time.Duration(rndSeconds)*time.Second, startSessionGC) - - return func(ctx *Context) { + return func(ctx *m.ReqContext) { ctx.Next() - if err = ctx.Session.Release(); err != nil { + if err := ctx.Session.Release(); err != nil { panic("session(release): " + err.Error()) } } } - -func GetSession() SessionStore { - return &SessionWrapper{manager: sessionManager} -} - -type SessionStore interface { - // Set sets value to given key in session. - Set(interface{}, interface{}) error - // Get gets value by given key in session. - Get(interface{}) interface{} - // Delete deletes a key from session. - Delete(interface{}) interface{} - // ID returns current session ID. - ID() string - // Release releases session resource and save data to provider. - Release() error - // Destory deletes a session. - Destory(*Context) error - // init - Start(*Context) error - // RegenerateId regenerates the session id - RegenerateId(*Context) error -} - -type SessionWrapper struct { - session session.RawStore - manager *session.Manager -} - -func (s *SessionWrapper) Start(c *Context) error { - var err error - s.session, err = s.manager.Start(c.Context) - return err -} - -func (s *SessionWrapper) RegenerateId(c *Context) error { - var err error - s.session, err = s.manager.RegenerateId(c.Context) - return err -} - -func (s *SessionWrapper) Set(k interface{}, v interface{}) error { - if s.session != nil { - return s.session.Set(k, v) - } - return nil -} - -func (s *SessionWrapper) Get(k interface{}) interface{} { - if s.session != nil { - return s.session.Get(k) - } - return nil -} - -func (s *SessionWrapper) Delete(k interface{}) interface{} { - if s.session != nil { - return s.session.Delete(k) - } - return nil -} - -func (s *SessionWrapper) ID() string { - if s.session != nil { - return s.session.ID() - } - return "" -} - -func (s *SessionWrapper) Release() error { - if s.session != nil { - return s.session.Release() - } - return nil -} - -func (s *SessionWrapper) Destory(c *Context) error { - if s.session != nil { - if err := s.manager.Destory(c.Context); err != nil { - return err - } - s.session = nil - } - return nil -} diff --git a/pkg/middleware/validate_host.go b/pkg/middleware/validate_host.go index fa84e783767..63c4b3000e9 100644 --- a/pkg/middleware/validate_host.go +++ b/pkg/middleware/validate_host.go @@ -3,12 +3,13 @@ package middleware import ( "strings" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "gopkg.in/macaron.v1" ) func ValidateHostHeader(domain string) macaron.Handler { - return func(c *Context) { + return func(c *m.ReqContext) { // ignore local render calls if c.IsRenderCall { return diff --git a/pkg/models/context.go b/pkg/models/context.go new file mode 100644 index 00000000000..262f6550954 --- /dev/null +++ b/pkg/models/context.go @@ -0,0 +1,86 @@ +package models + +import ( + "strings" + + "github.com/prometheus/client_golang/prometheus" + "gopkg.in/macaron.v1" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/session" + "github.com/grafana/grafana/pkg/setting" +) + +type ReqContext struct { + *macaron.Context + *SignedInUser + + Session session.SessionStore + + IsSignedIn bool + IsRenderCall bool + AllowAnonymous bool + Logger log.Logger +} + +// Handle handles and logs error by given status. +func (ctx *ReqContext) Handle(status int, title string, err error) { + if err != nil { + ctx.Logger.Error(title, "error", err) + if setting.Env != setting.PROD { + ctx.Data["ErrorMsg"] = err + } + } + + ctx.Data["Title"] = title + ctx.Data["AppSubUrl"] = setting.AppSubUrl + ctx.Data["Theme"] = "dark" + + ctx.HTML(status, "error") +} + +func (ctx *ReqContext) JsonOK(message string) { + resp := make(map[string]interface{}) + resp["message"] = message + ctx.JSON(200, resp) +} + +func (ctx *ReqContext) IsApiRequest() bool { + return strings.HasPrefix(ctx.Req.URL.Path, "/api") +} + +func (ctx *ReqContext) JsonApiErr(status int, message string, err error) { + resp := make(map[string]interface{}) + + if err != nil { + ctx.Logger.Error(message, "error", err) + if setting.Env != setting.PROD { + resp["error"] = err.Error() + } + } + + switch status { + case 404: + resp["message"] = "Not Found" + case 500: + resp["message"] = "Internal Server Error" + } + + if message != "" { + resp["message"] = message + } + + ctx.JSON(status, resp) +} + +func (ctx *ReqContext) HasUserRole(role RoleType) bool { + return ctx.OrgRole.Includes(role) +} + +func (ctx *ReqContext) HasHelpFlag(flag HelpFlags1) bool { + return ctx.HelpFlags1.HasFlag(flag) +} + +func (ctx *ReqContext) TimeRequest(timer prometheus.Summary) { + ctx.Data["perfmon.timer"] = timer +} diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index d2b93c5c1d6..f2236ad8477 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -19,6 +19,7 @@ const ( DS_PROMETHEUS = "prometheus" DS_POSTGRES = "postgres" DS_MYSQL = "mysql" + DS_MSSQL = "mssql" DS_ACCESS_DIRECT = "direct" DS_ACCESS_PROXY = "proxy" ) @@ -68,6 +69,7 @@ var knownDatasourcePlugins map[string]bool = map[string]bool{ DS_OPENTSDB: true, DS_POSTGRES: true, DS_MYSQL: true, + DS_MSSQL: true, "opennms": true, "abhisant-druid-datasource": true, "dalmatinerdb-datasource": true, diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index 53012f3e817..fb4d63a1fe4 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -80,7 +80,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { User: cmd.User, } - savedDash, err := dashboards.NewService().SaveDashboard(dto) + savedDash, err := dashboards.NewService().ImportDashboard(dto) if err != nil { return err diff --git a/pkg/plugins/update_checker.go b/pkg/plugins/update_checker.go index a2594ad915a..68ccdeaf840 100644 --- a/pkg/plugins/update_checker.go +++ b/pkg/plugins/update_checker.go @@ -63,7 +63,7 @@ func checkForUpdates() { resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion) if err != nil { - log.Trace("Failed to get plugins repo from grafana.net, %v", err.Error()) + log.Trace("Failed to get plugins repo from grafana.com, %v", err.Error()) return } @@ -101,7 +101,7 @@ func checkForUpdates() { resp2, err := httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json") if err != nil { - log.Trace("Failed to get latest.json repo from github: %v", err.Error()) + log.Trace("Failed to get latest.json repo from github.com: %v", err.Error()) return } @@ -115,7 +115,7 @@ func checkForUpdates() { var githubLatest GithubLatest err = json.Unmarshal(body, &githubLatest) if err != nil { - log.Trace("Failed to unmarshal github latest, reading response from github: %v", err.Error()) + log.Trace("Failed to unmarshal github.com latest, reading response from github.com: %v", err.Error()) return } diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index a609824cbc8..2ae26c1a382 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -74,6 +74,21 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) for _, panelObj := range jsonWithPanels.Get("panels").MustArray() { panel := simplejson.NewFromAny(panelObj) + + collapsedJson, collapsed := panel.CheckGet("collapsed") + // check if the panel is collapsed + if collapsed && collapsedJson.MustBool() { + + // extract alerts from sub panels for collapsed panels + als, err := e.GetAlertFromPanels(panel) + if err != nil { + return nil, err + } + + alerts = append(alerts, als...) + continue + } + jsonAlert, hasAlert := panel.CheckGet("alert") if !hasAlert { @@ -143,10 +158,15 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) // validate _, err = NewRuleFromDBAlert(alert) - if err == nil && alert.ValidToSave() { + if err != nil { + return nil, err + } + + if alert.ValidToSave() { alerts = append(alerts, alert) } else { - return nil, err + e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId) + return nil, m.ErrDashboardContainsInvalidAlertData } } diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index 71f3026025d..3bda6c771fb 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -22,6 +22,7 @@ func TestAlertRuleExtraction(t *testing.T) { defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true} graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"} influxDBDs := &m.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"} + prom := &m.DataSource{Id: 17, OrgId: 1, Name: "Prometheus"} bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error { query.Result = []*m.DataSource{defaultDs, graphite2Ds} @@ -38,6 +39,10 @@ func TestAlertRuleExtraction(t *testing.T) { if query.Name == influxDBDs.Name { query.Result = influxDBDs } + if query.Name == prom.Name { + query.Result = prom + } + return nil }) @@ -150,6 +155,22 @@ func TestAlertRuleExtraction(t *testing.T) { }) }) + Convey("Panel with id set to zero should return error", func() { + panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json") + So(err, ShouldBeNil) + + dashJson, err := simplejson.NewJson([]byte(panelWithIdZero)) + So(err, ShouldBeNil) + dash := m.NewDashboardFromJson(dashJson) + extractor := NewDashAlertExtractor(dash, 1) + + _, err = extractor.GetAlerts() + + Convey("panel with id 0 should return error", func() { + So(err, ShouldNotBeNil) + }) + }) + Convey("Parse alerts from dashboard without rows", func() { json, err := ioutil.ReadFile("./test-data/v5-dashboard.json") So(err, ShouldBeNil) @@ -198,5 +219,26 @@ func TestAlertRuleExtraction(t *testing.T) { } }) }) + + Convey("Should be able to extract collapsed panels", func() { + json, err := ioutil.ReadFile("./test-data/collapsed-panels.json") + So(err, ShouldBeNil) + + dashJson, err := simplejson.NewJson(json) + So(err, ShouldBeNil) + + dash := m.NewDashboardFromJson(dashJson) + extractor := NewDashAlertExtractor(dash, 1) + + alerts, err := extractor.GetAlerts() + + Convey("Get rules without error", func() { + So(err, ShouldBeNil) + }) + + Convey("should be able to extract collapsed alerts", func() { + So(len(alerts), ShouldEqual, 4) + }) + }) }) } diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index 08f8e8be29c..d449167de13 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -46,25 +46,49 @@ type AlertmanagerNotifier struct { } func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool { + this.log.Debug("Should notify", "ruleId", evalContext.Rule.Id, "state", evalContext.Rule.State, "previousState", evalContext.PrevAlertState) + + // Do not notify when we become OK for the first time. + if (evalContext.PrevAlertState == m.AlertStatePending) && (evalContext.Rule.State == m.AlertStateOK) { + return false + } + // Notify on Alerting -> OK to resolve before alertmanager timeout. + if (evalContext.PrevAlertState == m.AlertStateAlerting) && (evalContext.Rule.State == m.AlertStateOK) { + return true + } return evalContext.Rule.State == m.AlertStateAlerting } -func (this *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error { +func (this *AlertmanagerNotifier) createAlert(evalContext *alerting.EvalContext, match *alerting.EvalMatch, ruleUrl string) *simplejson.Json { + alertJSON := simplejson.New() + alertJSON.Set("startsAt", evalContext.StartTime.UTC().Format(time.RFC3339)) + if evalContext.Rule.State == m.AlertStateOK { + alertJSON.Set("endsAt", time.Now().UTC().Format(time.RFC3339)) + } + alertJSON.Set("generatorURL", ruleUrl) - alerts := make([]interface{}, 0) - for _, match := range evalContext.EvalMatches { - alertJSON := simplejson.New() - alertJSON.Set("startsAt", evalContext.StartTime.UTC().Format(time.RFC3339)) - - if ruleUrl, err := evalContext.GetRuleUrl(); err == nil { - alertJSON.Set("generatorURL", ruleUrl) + // Annotations (summary and description are very commonly used). + alertJSON.SetPath([]string{"annotations", "summary"}, evalContext.Rule.Name) + description := "" + if evalContext.Rule.Message != "" { + description += evalContext.Rule.Message + } + if evalContext.Error != nil { + if description != "" { + description += "\n" } + description += "Error: " + evalContext.Error.Error() + } + if description != "" { + alertJSON.SetPath([]string{"annotations", "description"}, description) + } + if evalContext.ImagePublicUrl != "" { + alertJSON.SetPath([]string{"annotations", "image"}, evalContext.ImagePublicUrl) + } - if evalContext.Rule.Message != "" { - alertJSON.SetPath([]string{"annotations", "description"}, evalContext.Rule.Message) - } - - tags := make(map[string]string) + // Labels (from metrics tags + mandatory alertname). + tags := make(map[string]string) + if match != nil { if len(match.Tags) == 0 { tags["metric"] = match.Metric } else { @@ -72,10 +96,32 @@ func (this *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) erro tags[k] = v } } - tags["alertname"] = evalContext.Rule.Name - alertJSON.Set("labels", tags) + } + tags["alertname"] = evalContext.Rule.Name + alertJSON.Set("labels", tags) + return alertJSON +} - alerts = append(alerts, alertJSON) +func (this *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error { + this.log.Info("Sending Alertmanager alert", "ruleId", evalContext.Rule.Id, "notification", this.Name) + + ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("Failed get rule link", "error", err) + return err + } + + // Send one alert per matching series. + alerts := make([]interface{}, 0) + for _, match := range evalContext.EvalMatches { + alert := this.createAlert(evalContext, match, ruleUrl) + alerts = append(alerts, alert) + } + + // This happens on ExecutionError or NoData + if len(alerts) == 0 { + alert := this.createAlert(evalContext, nil, ruleUrl) + alerts = append(alerts, alert) } bodyJSON := simplejson.NewFromAny(alerts) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 601f8fc24b1..7a3cc71c4db 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -27,15 +27,21 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model } func defaultShouldNotify(context *alerting.EvalContext) bool { + // Only notify on state change. if context.PrevAlertState == context.Rule.State { return false } + // Do not notify when we become OK for the first time. if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) { return false } return true } +func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool { + return defaultShouldNotify(context) +} + func (n *NotifierBase) GetType() string { return n.Type } diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index c2029b1173c..e32b9d34f91 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -38,10 +38,6 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) }, nil } -func (this *DingDingNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - type DingDingNotifier struct { NotifierBase Url string diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 095f7c7156a..562ffbe1269 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -58,10 +58,6 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) { }, nil } -func (this *EmailNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Sending alert notification to", "addresses", this.Addresses) diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index b65f25b1422..f1f63d42a04 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -75,10 +75,6 @@ type HipChatNotifier struct { log log.Logger } -func (this *HipChatNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index e885d44405d..92f6489106b 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -57,10 +57,6 @@ type KafkaNotifier struct { log log.Logger } -func (this *KafkaNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error { state := evalContext.Rule.State diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index bc0b0c984a4..4fbaa2d543e 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -51,10 +51,6 @@ type LineNotifier struct { log log.Logger } -func (this *LineNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 82be9d1df4e..5d8b15160c4 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -72,10 +72,6 @@ type OpsGenieNotifier struct { log log.Logger } -func (this *OpsGenieNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error { var err error @@ -99,11 +95,16 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err return err } + customData := "Triggered metrics:\n\n" + for _, evt := range evalContext.EvalMatches { + customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value) + } + bodyJSON := simplejson.New() bodyJSON.Set("message", evalContext.Rule.Name) bodyJSON.Set("source", "Grafana") bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) - bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message)) + bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message, customData)) details := simplejson.New() details.Set("url", ruleUrl) diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index c4067abec3b..58484051432 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -1,7 +1,9 @@ package notifiers import ( + "os" "strconv" + "time" "fmt" @@ -38,7 +40,7 @@ func init() { } var ( - pagerdutyEventApiUrl string = "https://events.pagerduty.com/generic/2010-04-15/create_event.json" + pagerdutyEventApiUrl string = "https://events.pagerduty.com/v2/enqueue" ) func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) { @@ -63,10 +65,6 @@ type PagerdutyNotifier struct { log log.Logger } -func (this *PagerdutyNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve { @@ -85,28 +83,41 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Notifying Pagerduty", "event_type", eventType) + payloadJSON := simplejson.New() + payloadJSON.Set("summary", evalContext.Rule.Name+" - "+evalContext.Rule.Message) + if hostname, err := os.Hostname(); err == nil { + payloadJSON.Set("source", hostname) + } + payloadJSON.Set("severity", "critical") + payloadJSON.Set("timestamp", time.Now()) + payloadJSON.Set("component", "Grafana") + payloadJSON.Set("custom_details", customData) + bodyJSON := simplejson.New() - bodyJSON.Set("service_key", this.Key) - bodyJSON.Set("description", evalContext.Rule.Name+" - "+evalContext.Rule.Message) - bodyJSON.Set("client", "Grafana") - bodyJSON.Set("details", customData) - bodyJSON.Set("event_type", eventType) - bodyJSON.Set("incident_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) + bodyJSON.Set("routing_key", this.Key) + bodyJSON.Set("event_action", eventType) + bodyJSON.Set("dedup_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) + bodyJSON.Set("payload", payloadJSON) ruleUrl, err := evalContext.GetRuleUrl() if err != nil { this.log.Error("Failed get rule link", "error", err) return err } + links := make([]interface{}, 1) + linkJSON := simplejson.New() + linkJSON.Set("href", ruleUrl) bodyJSON.Set("client_url", ruleUrl) + bodyJSON.Set("client", "Grafana") + links[0] = linkJSON + bodyJSON.Set("links", links) if evalContext.ImagePublicUrl != "" { contexts := make([]interface{}, 1) imageJSON := simplejson.New() - imageJSON.Set("type", "image") imageJSON.Set("src", evalContext.ImagePublicUrl) contexts[0] = imageJSON - bodyJSON.Set("contexts", contexts) + bodyJSON.Set("images", contexts) } body, _ := bodyJSON.MarshalJSON() @@ -115,6 +126,9 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { Url: pagerdutyEventApiUrl, Body: string(body), HttpMethod: "POST", + HttpHeader: map[string]string{ + "Content-Type": "application/json", + }, } if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index 067c02a4a5d..cbe9e16801a 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -123,10 +123,6 @@ type PushoverNotifier struct { log log.Logger } -func (this *PushoverNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { ruleUrl, err := evalContext.GetRuleUrl() if err != nil { diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index 7a34d51b493..9f77801d458 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -71,10 +71,6 @@ type SensuNotifier struct { log log.Logger } -func (this *SensuNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Sending sensu result") diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index c5bb9344e30..e051a71740a 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -98,10 +98,6 @@ type SlackNotifier struct { log log.Logger } -func (this *SlackNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 851e5a01c75..9a9e93dbc47 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -47,10 +47,6 @@ type TeamsNotifier struct { log log.Logger } -func (this *TeamsNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 0ca1ad3dfe0..5cbdad60906 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -12,6 +12,10 @@ import ( "os" ) +const ( + captionLengthLimit = 200 +) + var ( telegramApiUrl string = "https://api.telegram.org/bot%s/%s" ) @@ -82,88 +86,81 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) } func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) *m.SendWebhookSync { - var imageFile *os.File - var err error - if sendImageInline { - imageFile, err = os.Open(evalContext.ImageOnDiskPath) - defer imageFile.Close() - if err != nil { - sendImageInline = false // fall back to text message + cmd, err := this.buildMessageInlineImage(evalContext) + if err == nil { + return cmd + } else { + this.log.Error("Could not generate Telegram message with inline image.", "err", err) } } - message := "" + return this.buildMessageLinkedImage(evalContext) +} - if sendImageInline { - // Telegram's API does not allow HTML formatting for image captions. - message = fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) - } else { - message = fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) - } +func (this *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) *m.SendWebhookSync { + message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) ruleUrl, err := evalContext.GetRuleUrl() if err == nil { message = message + fmt.Sprintf("URL: %s\n", ruleUrl) } - if !sendImageInline { - // only attach this if we are not sending it inline. - if evalContext.ImagePublicUrl != "" { - message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl) - } - } - - metrics := "" - fieldLimitCount := 4 - for index, evt := range evalContext.EvalMatches { - metrics += fmt.Sprintf("\n%s: %s", evt.Metric, evt.Value) - if index > fieldLimitCount { - break - } + if evalContext.ImagePublicUrl != "" { + message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl) } + metrics := generateMetricsMessage(evalContext) if metrics != "" { - if sendImageInline { - // Telegram's API does not allow HTML formatting for image captions. - message = message + fmt.Sprintf("\nMetrics:%s", metrics) - } else { - message = message + fmt.Sprintf("\nMetrics:%s", metrics) - } + message = message + fmt.Sprintf("\nMetrics:%s", metrics) } - var body bytes.Buffer + cmd := this.generateTelegramCmd(message, "text", "sendMessage", func(w *multipart.Writer) { + fw, _ := w.CreateFormField("parse_mode") + fw.Write([]byte("html")) + }) + return cmd +} +func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalContext) (*m.SendWebhookSync, error) { + var imageFile *os.File + var err error + + imageFile, err = os.Open(evalContext.ImageOnDiskPath) + defer imageFile.Close() + if err != nil { + return nil, err + } + + ruleUrl, err := evalContext.GetRuleUrl() + + metrics := generateMetricsMessage(evalContext) + message := generateImageCaption(evalContext, ruleUrl, metrics) + + cmd := this.generateTelegramCmd(message, "caption", "sendPhoto", func(w *multipart.Writer) { + fw, _ := w.CreateFormFile("photo", evalContext.ImageOnDiskPath) + io.Copy(fw, imageFile) + }) + return cmd, nil +} + +func (this *TelegramNotifier) generateTelegramCmd(message string, messageField string, apiAction string, extraConf func(writer *multipart.Writer)) *m.SendWebhookSync { + var body bytes.Buffer w := multipart.NewWriter(&body) + fw, _ := w.CreateFormField("chat_id") fw.Write([]byte(this.ChatID)) - if sendImageInline { - fw, _ = w.CreateFormField("caption") - fw.Write([]byte(message)) + fw, _ = w.CreateFormField(messageField) + fw.Write([]byte(message)) - fw, _ = w.CreateFormFile("photo", evalContext.ImageOnDiskPath) - io.Copy(fw, imageFile) - } else { - fw, _ = w.CreateFormField("text") - fw.Write([]byte(message)) - - fw, _ = w.CreateFormField("parse_mode") - fw.Write([]byte("html")) - } + extraConf(w) w.Close() - apiMethod := "" - if sendImageInline { - this.log.Info("Sending telegram image notification", "photo", evalContext.ImageOnDiskPath, "chat_id", this.ChatID, "bot_token", this.BotToken) - apiMethod = "sendPhoto" - } else { - this.log.Info("Sending telegram text notification", "chat_id", this.ChatID, "bot_token", this.BotToken) - apiMethod = "sendMessage" - } + this.log.Info("Sending telegram notification", "chat_id", this.ChatID, "bot_token", this.BotToken, "apiAction", apiAction) + url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiAction) - url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiMethod) cmd := &m.SendWebhookSync{ Url: url, Body: body.String(), @@ -175,8 +172,49 @@ func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, se return cmd } -func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) +func generateMetricsMessage(evalContext *alerting.EvalContext) string { + metrics := "" + fieldLimitCount := 4 + for index, evt := range evalContext.EvalMatches { + metrics += fmt.Sprintf("\n%s: %s", evt.Metric, evt.Value) + if index > fieldLimitCount { + break + } + } + return metrics +} + +func generateImageCaption(evalContext *alerting.EvalContext, ruleUrl string, metrics string) string { + message := evalContext.GetNotificationTitle() + + if len(evalContext.Rule.Message) > 0 { + message = fmt.Sprintf("%s\nMessage: %s", message, evalContext.Rule.Message) + } + + if len(message) > captionLengthLimit { + message = message[0:captionLengthLimit] + + } + + if len(ruleUrl) > 0 { + urlLine := fmt.Sprintf("\nURL: %s", ruleUrl) + message = appendIfPossible(message, urlLine, captionLengthLimit) + } + + if metrics != "" { + metricsLines := fmt.Sprintf("\n\nMetrics:%s", metrics) + message = appendIfPossible(message, metricsLines, captionLengthLimit) + } + + return message +} + +func appendIfPossible(message string, extra string, sizeLimit int) string { + if len(extra)+len(message) <= sizeLimit { + return message + extra + } + log.Debug("Line too long for image caption.", "value", extra) + return message } func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { diff --git a/pkg/services/alerting/notifiers/telegram_test.go b/pkg/services/alerting/notifiers/telegram_test.go index 3e8066e273b..05be787dced 100644 --- a/pkg/services/alerting/notifiers/telegram_test.go +++ b/pkg/services/alerting/notifiers/telegram_test.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" . "github.com/smartystreets/goconvey/convey" ) @@ -50,6 +51,71 @@ func TestTelegramNotifier(t *testing.T) { So(telegramNotifier.ChatID, ShouldEqual, "-1234567890") }) + Convey("generateCaption should generate a message with all pertinent details", func() { + evalContext := alerting.NewEvalContext(nil, &alerting.Rule{ + Name: "This is an alarm", + Message: "Some kind of message.", + State: m.AlertStateOK, + }) + + caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "") + So(len(caption), ShouldBeLessThanOrEqualTo, 200) + So(caption, ShouldContainSubstring, "Some kind of message.") + So(caption, ShouldContainSubstring, "[OK] This is an alarm") + So(caption, ShouldContainSubstring, "http://grafa.url/abcdef") + }) + + Convey("When generating a message", func() { + + Convey("URL should be skipped if it's too long", func() { + evalContext := alerting.NewEvalContext(nil, &alerting.Rule{ + Name: "This is an alarm", + Message: "Some kind of message.", + State: m.AlertStateOK, + }) + + caption := generateImageCaption(evalContext, + "http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "foo bar") + So(len(caption), ShouldBeLessThanOrEqualTo, 200) + So(caption, ShouldContainSubstring, "Some kind of message.") + So(caption, ShouldContainSubstring, "[OK] This is an alarm") + So(caption, ShouldContainSubstring, "foo bar") + So(caption, ShouldNotContainSubstring, "http") + }) + + Convey("Message should be trimmed if it's too long", func() { + evalContext := alerting.NewEvalContext(nil, &alerting.Rule{ + Name: "This is an alarm", + Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it.", + State: m.AlertStateOK, + }) + + caption := generateImageCaption(evalContext, + "http://grafa.url/foo", + "") + So(len(caption), ShouldBeLessThanOrEqualTo, 200) + So(caption, ShouldContainSubstring, "[OK] This is an alarm") + So(caption, ShouldNotContainSubstring, "http") + So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise ") + }) + + Convey("Metrics should be skipped if they dont fit", func() { + evalContext := alerting.NewEvalContext(nil, &alerting.Rule{ + Name: "This is an alarm", + Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ", + State: m.AlertStateOK, + }) + + caption := generateImageCaption(evalContext, + "http://grafa.url/foo", + "foo bar long song") + So(len(caption), ShouldBeLessThanOrEqualTo, 200) + So(caption, ShouldContainSubstring, "[OK] This is an alarm") + So(caption, ShouldNotContainSubstring, "http") + So(caption, ShouldNotContainSubstring, "foo bar") + }) + }) }) }) } diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index b8455dcfbfd..e4ffffc9108 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -114,10 +114,6 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { }, nil } -func (this *ThreemaNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error { notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID) notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID) diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index a2b770dfd02..4b4db553cde 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -68,10 +68,6 @@ type VictoropsNotifier struct { log log.Logger } -func (this *VictoropsNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - // Notify sends notification to Victorops via POST to URL endpoint func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index d2d6ec636b7..4c97ed2b75e 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -65,10 +65,6 @@ type WebhookNotifier struct { log log.Logger } -func (this *WebhookNotifier) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) -} - func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Sending webhook") diff --git a/pkg/services/alerting/test-data/collapsed-panels.json b/pkg/services/alerting/test-data/collapsed-panels.json new file mode 100644 index 00000000000..29109788955 --- /dev/null +++ b/pkg/services/alerting/test-data/collapsed-panels.json @@ -0,0 +1,597 @@ +{ + "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, + "id": 127, + "links": [], + "panels": [ + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 9, + "title": "Row title", + "type": "row" + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 200 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "frequency": "10s", + "handler": 1, + "name": "Panel Title alert", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 1 + }, + "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": "go_goroutines", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 200 + } + ], + "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 + } + ] + }, + { + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 14, + "limit": 10, + "links": [], + "onlyAlertsOnDashboard": true, + "show": "current", + "sortOrder": 1, + "stateFilter": [], + "title": "Panel Title", + "type": "alertlist" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 6, + "panels": [ + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 200 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "frequency": "10s", + "handler": 1, + "name": "Panel 2 alert", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 11 + }, + "id": 11, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "go_goroutines", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 200 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel 2", + "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 + } + ] + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 200 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "frequency": "10s", + "handler": 1, + "name": "Panel 4 alert", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 11 + }, + "id": 15, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "go_goroutines", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 200 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel 4", + "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 + } + ] + } + ], + "title": "Row title", + "type": "row" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 4, + "title": "Row title", + "type": "row" + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 200 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "frequency": "10s", + "handler": 1, + "name": "Panel 3 alert", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 12 + }, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "go_goroutines", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 200 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel 3", + "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 Copy", + "uid": "6v5pg36zk", + "version": 17 +} diff --git a/pkg/services/alerting/test-data/panel-with-id-0.json b/pkg/services/alerting/test-data/panel-with-id-0.json new file mode 100644 index 00000000000..d1f314a4f55 --- /dev/null +++ b/pkg/services/alerting/test-data/panel-with-id-0.json @@ -0,0 +1,63 @@ +{ + "id": 57, + "title": "Graphite 4", + "originalTitle": "Graphite 4", + "tags": ["graphite"], + "rows": [ + { + "panels": [ + { + "title": "Active desktop users", + "id": 0, + "editable": true, + "type": "graph", + "targets": [ + { + "refId": "A", + "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)" + } + ], + "datasource": null, + "alert": { + "name": "name1", + "message": "desc1", + "handler": 1, + "frequency": "60s", + "conditions": [ + { + "type": "query", + "query": {"params": ["A", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] + } + }, + { + "title": "Active mobile users", + "id": 4, + "targets": [ + {"refId": "A", "target": ""}, + {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"} + ], + "datasource": "graphite2", + "alert": { + "name": "name2", + "message": "desc2", + "handler": 0, + "frequency": "60s", + "severity": "warning", + "conditions": [ + { + "type": "query", + "query": {"params": ["B", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] + } + } + ] + } +] + } diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index 1f39394d757..02a6ffc8330 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -13,6 +13,7 @@ import ( // DashboardService service for operating on dashboards type DashboardService interface { SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) + ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) } // DashboardProvisioningService service for operating on provisioned dashboards @@ -214,6 +215,20 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da return cmd.Result, nil } +func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) { + cmd, err := dr.buildSaveDashboardCommand(dto, false) + if err != nil { + return nil, err + } + + err = bus.Dispatch(cmd) + if err != nil { + return nil, err + } + + return cmd.Result, nil +} + type FakeDashboardService struct { SaveDashboardResult *models.Dashboard SaveDashboardError error @@ -230,6 +245,10 @@ func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO) (*models.Das return s.SaveDashboardResult, s.SaveDashboardError } +func (s *FakeDashboardService) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) { + return s.SaveDashboard(dto) +} + func MockDashboardService(mock *FakeDashboardService) { NewService = func() DashboardService { return mock diff --git a/pkg/services/quota/quota.go b/pkg/services/quota/quota.go new file mode 100644 index 00000000000..2ec399437e6 --- /dev/null +++ b/pkg/services/quota/quota.go @@ -0,0 +1,87 @@ +package quota + +import ( + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/session" + "github.com/grafana/grafana/pkg/setting" +) + +func QuotaReached(c *m.ReqContext, target string) (bool, error) { + if !setting.Quota.Enabled { + return false, nil + } + + // get the list of scopes that this target is valid for. Org, User, Global + scopes, err := m.GetQuotaScopes(target) + if err != nil { + return false, err + } + + for _, scope := range scopes { + c.Logger.Debug("Checking quota", "target", target, "scope", scope) + + switch scope.Name { + case "global": + if scope.DefaultLimit < 0 { + continue + } + if scope.DefaultLimit == 0 { + return true, nil + } + if target == "session" { + usedSessions := session.GetSessionCount() + if int64(usedSessions) > scope.DefaultLimit { + c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit) + return true, nil + } + continue + } + query := m.GetGlobalQuotaByTargetQuery{Target: scope.Target} + if err := bus.Dispatch(&query); err != nil { + return true, err + } + if query.Result.Used >= scope.DefaultLimit { + return true, nil + } + case "org": + if !c.IsSignedIn { + continue + } + query := m.GetOrgQuotaByTargetQuery{OrgId: c.OrgId, Target: scope.Target, Default: scope.DefaultLimit} + if err := bus.Dispatch(&query); err != nil { + return true, err + } + if query.Result.Limit < 0 { + continue + } + if query.Result.Limit == 0 { + return true, nil + } + + if query.Result.Used >= query.Result.Limit { + return true, nil + } + case "user": + if !c.IsSignedIn || c.UserId == 0 { + continue + } + query := m.GetUserQuotaByTargetQuery{UserId: c.UserId, Target: scope.Target, Default: scope.DefaultLimit} + if err := bus.Dispatch(&query); err != nil { + return true, err + } + if query.Result.Limit < 0 { + continue + } + if query.Result.Limit == 0 { + return true, nil + } + + if query.Result.Used >= query.Result.Limit { + return true, nil + } + } + } + + return false, nil +} diff --git a/vendor/github.com/go-macaron/session/mysql/mysql.go b/pkg/services/session/mysql.go similarity index 87% rename from vendor/github.com/go-macaron/session/mysql/mysql.go rename to pkg/services/session/mysql.go index a16db779c63..f8c5d828cfa 100644 --- a/vendor/github.com/go-macaron/session/mysql/mysql.go +++ b/pkg/services/session/mysql.go @@ -108,6 +108,7 @@ func (p *MysqlProvider) Init(expire int64, connStr string) (err error) { p.expire = expire p.c, err = sql.Open("mysql", connStr) + p.c.SetConnMaxLifetime(time.Second * time.Duration(sessionConnMaxLifetime)) if err != nil { return err } @@ -141,12 +142,29 @@ func (p *MysqlProvider) Read(sid string) (session.RawStore, error) { // Exist returns true if session with given ID exists. func (p *MysqlProvider) Exist(sid string) bool { + exists, err := p.queryExists(sid) + + if err != nil { + exists, err = p.queryExists(sid) + } + + if err != nil { + log.Printf("session/mysql: error checking if session exists: %v", err) + return false + } + + return exists +} + +func (p *MysqlProvider) queryExists(sid string) (bool, error) { var data []byte err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data) + if err != nil && err != sql.ErrNoRows { - panic("session/mysql: error checking existence: " + err.Error()) + return false, err } - return err != sql.ErrNoRows + + return err != sql.ErrNoRows, nil } // Destory deletes a session by session ID. @@ -185,7 +203,12 @@ func (p *MysqlProvider) Count() (total int) { // GC calls GC to clean expired sessions. func (p *MysqlProvider) GC() { - if _, err := p.c.Exec("DELETE FROM session WHERE expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire); err != nil { + var err error + if _, err = p.c.Exec("DELETE FROM session WHERE expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire); err != nil { + _, err = p.c.Exec("DELETE FROM session WHERE expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire) + } + + if err != nil { log.Printf("session/mysql: error garbage collecting: %v", err) } } diff --git a/pkg/services/session/session.go b/pkg/services/session/session.go new file mode 100644 index 00000000000..5873a6a5b72 --- /dev/null +++ b/pkg/services/session/session.go @@ -0,0 +1,175 @@ +package session + +import ( + "math/rand" + "time" + + ms "github.com/go-macaron/session" + _ "github.com/go-macaron/session/memcache" + _ "github.com/go-macaron/session/postgres" + _ "github.com/go-macaron/session/redis" + "github.com/grafana/grafana/pkg/log" + "gopkg.in/macaron.v1" +) + +const ( + SESS_KEY_USERID = "uid" + SESS_KEY_OAUTH_STATE = "state" + SESS_KEY_APIKEY = "apikey_id" // used for render requests with api keys + SESS_KEY_LASTLDAPSYNC = "last_ldap_sync" +) + +var sessionManager *ms.Manager +var sessionOptions *ms.Options +var StartSessionGC func() +var GetSessionCount func() int +var sessionLogger = log.New("session") +var sessionConnMaxLifetime int64 + +func init() { + StartSessionGC = func() { + sessionManager.GC() + sessionLogger.Debug("Session GC") + time.AfterFunc(time.Duration(sessionOptions.Gclifetime)*time.Second, StartSessionGC) + } + GetSessionCount = func() int { + return sessionManager.Count() + } +} + +func Init(options *ms.Options, connMaxLifetime int64) { + var err error + sessionOptions = prepareOptions(options) + sessionConnMaxLifetime = connMaxLifetime + sessionManager, err = ms.NewManager(options.Provider, *options) + if err != nil { + panic(err) + } + + // start GC threads after some random seconds + rndSeconds := 10 + rand.Int63n(180) + time.AfterFunc(time.Duration(rndSeconds)*time.Second, StartSessionGC) +} + +func prepareOptions(opt *ms.Options) *ms.Options { + if len(opt.Provider) == 0 { + opt.Provider = "memory" + } + if len(opt.ProviderConfig) == 0 { + opt.ProviderConfig = "data/sessions" + } + if len(opt.CookieName) == 0 { + opt.CookieName = "grafana_sess" + } + if len(opt.CookiePath) == 0 { + opt.CookiePath = "/" + } + if opt.Gclifetime == 0 { + opt.Gclifetime = 3600 + } + if opt.Maxlifetime == 0 { + opt.Maxlifetime = opt.Gclifetime + } + if opt.IDLength == 0 { + opt.IDLength = 16 + } + + return opt +} + +func GetSession() SessionStore { + return &SessionWrapper{manager: sessionManager} +} + +type SessionStore interface { + // Set sets value to given key in session. + Set(interface{}, interface{}) error + // Get gets value by given key in session. + Get(interface{}) interface{} + // Delete deletes a key from session. + Delete(interface{}) interface{} + // ID returns current session ID. + ID() string + // Release releases session resource and save data to provider. + Release() error + // Destory deletes a session. + Destory(*macaron.Context) error + // init + Start(*macaron.Context) error + // RegenerateId regenerates the session id + RegenerateId(*macaron.Context) error +} + +type SessionWrapper struct { + session ms.RawStore + manager *ms.Manager +} + +func (s *SessionWrapper) Start(c *macaron.Context) error { + // See https://github.com/grafana/grafana/issues/11155 for details on why + // a recover and retry is needed + defer func() error { + if err := recover(); err != nil { + var retryErr error + s.session, retryErr = s.manager.Start(c) + return retryErr + } + + return nil + }() + + var err error + s.session, err = s.manager.Start(c) + return err +} + +func (s *SessionWrapper) RegenerateId(c *macaron.Context) error { + var err error + s.session, err = s.manager.RegenerateId(c) + return err +} + +func (s *SessionWrapper) Set(k interface{}, v interface{}) error { + if s.session != nil { + return s.session.Set(k, v) + } + return nil +} + +func (s *SessionWrapper) Get(k interface{}) interface{} { + if s.session != nil { + return s.session.Get(k) + } + return nil +} + +func (s *SessionWrapper) Delete(k interface{}) interface{} { + if s.session != nil { + return s.session.Delete(k) + } + return nil +} + +func (s *SessionWrapper) ID() string { + if s.session != nil { + return s.session.ID() + } + return "" +} + +func (s *SessionWrapper) Release() error { + if s.session != nil { + return s.session.Release() + } + return nil +} + +func (s *SessionWrapper) Destory(c *macaron.Context) error { + if s.session != nil { + if err := s.manager.Destory(c); err != nil { + return err + } + s.session = nil + } + return nil +} diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 8c751f0cada..e99367e4d6f 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -10,6 +10,9 @@ import ( m "github.com/grafana/grafana/pkg/models" ) +// timeNow makes it possible to test usage of time +var timeNow = time.Now + func init() { bus.AddHandler("sql", SaveAlerts) bus.AddHandler("sql", HandleAlertsQuery) @@ -129,7 +132,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { return nil } -func DeleteAlertDefinition(dashboardId int64, sess *DBSession) error { +func deleteAlertDefinition(dashboardId int64, sess *DBSession) error { alerts := make([]*m.Alert, 0) sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) @@ -147,7 +150,7 @@ func SaveAlerts(cmd *m.SaveAlertsCommand) error { return err } - if err := upsertAlerts(existingAlerts, cmd, sess); err != nil { + if err := updateAlerts(existingAlerts, cmd, sess); err != nil { return err } @@ -159,7 +162,7 @@ func SaveAlerts(cmd *m.SaveAlertsCommand) error { }) } -func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBSession) error { +func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBSession) error { for _, alert := range cmd.Alerts { update := false var alertToUpdate *m.Alert @@ -175,7 +178,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS if update { if alertToUpdate.ContainsUpdates(alert) { - alert.Updated = time.Now() + alert.Updated = timeNow() alert.State = alertToUpdate.State sess.MustCols("message") _, err := sess.Id(alert.Id).Update(alert) @@ -186,10 +189,10 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS sqlog.Debug("Alert updated", "name", alert.Name, "id", alert.Id) } } else { - alert.Updated = time.Now() - alert.Created = time.Now() + alert.Updated = timeNow() + alert.Created = timeNow() alert.State = m.AlertStatePending - alert.NewStateDate = time.Now() + alert.NewStateDate = timeNow() _, err := sess.Insert(alert) if err != nil { @@ -252,8 +255,8 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error { } alert.State = cmd.State - alert.StateChanges += 1 - alert.NewStateDate = time.Now() + alert.StateChanges++ + alert.NewStateDate = timeNow() alert.EvalData = cmd.EvalData if cmd.Error == "" { @@ -276,11 +279,13 @@ func PauseAlert(cmd *m.PauseAlertCommand) error { var buffer bytes.Buffer params := make([]interface{}, 0) - buffer.WriteString(`UPDATE alert SET state = ?`) + buffer.WriteString(`UPDATE alert SET state = ?, new_state_date = ?`) if cmd.Paused { params = append(params, string(m.AlertStatePaused)) + params = append(params, timeNow()) } else { params = append(params, string(m.AlertStatePending)) + params = append(params, timeNow()) } buffer.WriteString(` WHERE id IN (?` + strings.Repeat(",?", len(cmd.AlertIds)-1) + `)`) @@ -306,7 +311,7 @@ func PauseAllAlerts(cmd *m.PauseAllAlertCommand) error { newState = string(m.AlertStatePending) } - res, err := sess.Exec(`UPDATE alert SET state = ?`, newState) + res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow()) if err != nil { return err } diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index de86ae87a4f..296d16c2f45 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -6,9 +6,26 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" + "time" ) +func mockTimeNow() { + var timeSeed int64 + timeNow = func() time.Time { + fakeNow := time.Unix(timeSeed, 0) + timeSeed += 1 + return fakeNow + } +} + +func resetTimeNow() { + timeNow = time.Now +} + func TestAlertingDataAccess(t *testing.T) { + mockTimeNow() + defer resetTimeNow() + Convey("Testing Alerting data access", t, func() { InitTestDB(t) @@ -50,13 +67,11 @@ func TestAlertingDataAccess(t *testing.T) { So(err, ShouldBeNil) }) - Convey("can pause alert", func() { - cmd := &m.PauseAllAlertCommand{ - Paused: true, - } + alert, _ := getAlertById(1) + stateDateBeforePause := alert.NewStateDate - err = PauseAllAlerts(cmd) - So(err, ShouldBeNil) + Convey("can pause all alerts", func() { + pauseAllAlerts(true) Convey("cannot updated paused alert", func() { cmd := &m.SetAlertStateCommand{ @@ -67,6 +82,19 @@ func TestAlertingDataAccess(t *testing.T) { err = SetAlertState(cmd) So(err, ShouldNotBeNil) }) + + Convey("pausing alerts should update their NewStateDate", func() { + alert, _ = getAlertById(1) + stateDateAfterPause := alert.NewStateDate + So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterPause) + }) + + Convey("unpausing alerts should update their NewStateDate again", func() { + pauseAllAlerts(false) + alert, _ = getAlertById(1) + stateDateAfterUnpause := alert.NewStateDate + So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterUnpause) + }) }) }) @@ -214,3 +242,90 @@ func TestAlertingDataAccess(t *testing.T) { }) }) } + +func TestPausingAlerts(t *testing.T) { + mockTimeNow() + defer resetTimeNow() + + Convey("Given an alert", t, func() { + InitTestDB(t) + + testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert") + alert, _ := insertTestAlert("Alerting title", "Alerting message", testDash.OrgId, testDash.Id, simplejson.New()) + + stateDateBeforePause := alert.NewStateDate + stateDateAfterPause := stateDateBeforePause + Convey("when paused", func() { + pauseAlert(testDash.OrgId, 1, true) + + Convey("the NewStateDate should be updated", func() { + alert, _ := getAlertById(1) + + stateDateAfterPause = alert.NewStateDate + So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterPause) + }) + }) + + Convey("when unpaused", func() { + pauseAlert(testDash.OrgId, 1, false) + + Convey("the NewStateDate should be updated again", func() { + alert, _ := getAlertById(1) + + stateDateAfterUnpause := alert.NewStateDate + So(stateDateAfterPause, ShouldHappenBefore, stateDateAfterUnpause) + }) + }) + }) +} +func pauseAlert(orgId int64, alertId int64, pauseState bool) (int64, error) { + cmd := &m.PauseAlertCommand{ + OrgId: orgId, + AlertIds: []int64{alertId}, + Paused: pauseState, + } + err := PauseAlert(cmd) + So(err, ShouldBeNil) + return cmd.ResultCount, err +} +func insertTestAlert(title string, message string, orgId int64, dashId int64, settings *simplejson.Json) (*m.Alert, error) { + items := []*m.Alert{ + { + PanelId: 1, + DashboardId: dashId, + OrgId: orgId, + Name: title, + Message: message, + Settings: settings, + Frequency: 1, + }, + } + + cmd := m.SaveAlertsCommand{ + Alerts: items, + DashboardId: dashId, + OrgId: orgId, + UserId: 1, + } + + err := SaveAlerts(&cmd) + return cmd.Alerts[0], err +} + +func getAlertById(id int64) (*m.Alert, error) { + q := &m.GetAlertByIdQuery{ + Id: id, + } + err := GetAlertById(q) + So(err, ShouldBeNil) + return q.Result, err +} + +func pauseAllAlerts(pauseState bool) error { + cmd := &m.PauseAllAlertCommand{ + Paused: pauseState, + } + err := PauseAllAlerts(cmd) + So(err, ShouldBeNil) + return err +} diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 5ee34183628..8a89c3d942c 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -330,7 +330,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { } } - if err := DeleteAlertDefinition(dashboard.Id, sess); err != nil { + if err := deleteAlertDefinition(dashboard.Id, sess); err != nil { return nil } diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index 40d6cf5bcb2..4c92c097931 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -3,7 +3,6 @@ package sqlstore import ( "testing" - "github.com/go-xorm/xorm" . "github.com/smartystreets/goconvey/convey" m "github.com/grafana/grafana/pkg/models" @@ -11,10 +10,8 @@ import ( ) func TestDashboardFolderDataAccess(t *testing.T) { - var x *xorm.Engine - Convey("Testing DB", t, func() { - x = InitTestDB(t) + InitTestDB(t) Convey("Given one dashboard folder with two dashboards and one dashboard in the root folder", func() { folder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp") @@ -49,6 +46,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}, } err := SearchDashboards(query) + So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index e0a73b9a49a..9124a686236 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" @@ -15,10 +14,8 @@ import ( ) func TestDashboardDataAccess(t *testing.T) { - var x *xorm.Engine - Convey("Testing DB", t, func() { - x = InitTestDB(t) + InitTestDB(t) Convey("Given saved dashboard", func() { savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp") diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index a8bd36ac8a3..0fde3f27c01 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -125,7 +125,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error { condition := m.GetCondition() if condition != nil { sql, args := condition.Sql(mg.dialect) - results, err := sess.Query(sql, args...) + results, err := sess.SQL(sql).Query(args...) if err != nil || len(results) == 0 { mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id()) return sess.Rollback() diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index c57d15a48d5..63b20aa6e86 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -2,6 +2,7 @@ package sqlstore import ( "testing" + "time" . "github.com/smartystreets/goconvey/convey" @@ -241,6 +242,8 @@ func TestAccountDataAccess(t *testing.T) { func testHelperUpdateDashboardAcl(dashboardId int64, items ...m.DashboardAcl) error { cmd := m.UpdateDashboardAclCommand{DashboardId: dashboardId} for _, item := range items { + item.Created = time.Now() + item.Updated = time.Now() cmd.Items = append(cmd.Items, &item) } return UpdateDashboardAcl(&cmd) diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go index 0a857efce40..3db3fc2657e 100644 --- a/pkg/services/sqlstore/quota.go +++ b/pkg/services/sqlstore/quota.go @@ -2,6 +2,7 @@ package sqlstore import ( "fmt" + "time" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" @@ -98,8 +99,9 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error { return inTransaction(func(sess *DBSession) error { //Check if quota is already defined in the DB quota := m.Quota{ - Target: cmd.Target, - OrgId: cmd.OrgId, + Target: cmd.Target, + OrgId: cmd.OrgId, + Updated: time.Now(), } has, err := sess.Get("a) if err != nil { @@ -107,6 +109,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error { } quota.Limit = cmd.Limit if has == false { + quota.Created = time.Now() //No quota in the DB for this target, so create a new one. if _, err := sess.Insert("a); err != nil { return err @@ -198,8 +201,9 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error { return inTransaction(func(sess *DBSession) error { //Check if quota is already defined in the DB quota := m.Quota{ - Target: cmd.Target, - UserId: cmd.UserId, + Target: cmd.Target, + UserId: cmd.UserId, + Updated: time.Now(), } has, err := sess.Get("a) if err != nil { @@ -207,6 +211,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error { } quota.Limit = cmd.Limit if has == false { + quota.Created = time.Now() //No quota in the DB for this target, so create a new one. if _, err := sess.Insert("a); err != nil { return err diff --git a/pkg/services/sqlstore/quota_test.go b/pkg/services/sqlstore/quota_test.go index 5ef618e166d..ed6565b1c3f 100644 --- a/pkg/services/sqlstore/quota_test.go +++ b/pkg/services/sqlstore/quota_test.go @@ -104,12 +104,12 @@ func TestQuotaCommandsAndQueries(t *testing.T) { }) }) Convey("Given saved user quota for org", func() { - userQoutaCmd := m.UpdateUserQuotaCmd{ + userQuotaCmd := m.UpdateUserQuotaCmd{ UserId: userId, Target: "org_user", Limit: 10, } - err := UpdateUserQuota(&userQoutaCmd) + err := UpdateUserQuota(&userQuotaCmd) So(err, ShouldBeNil) Convey("Should be able to get saved quota by user id and target", func() { diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 5843c5c300b..6aace350193 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" @@ -23,6 +24,8 @@ import ( "github.com/go-xorm/xorm" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" + + _ "github.com/grafana/grafana/pkg/tsdb/mssql" ) type DatabaseConfig struct { @@ -33,6 +36,7 @@ type DatabaseConfig struct { ServerCertName string MaxOpenConn int MaxIdleConn int + ConnMaxLifetime int } var ( @@ -157,18 +161,20 @@ func getEngine() (*xorm.Engine, error) { engine, err := xorm.NewEngine(DbCfg.Type, cnnstr) if err != nil { return nil, err - } else { - engine.SetMaxOpenConns(DbCfg.MaxOpenConn) - engine.SetMaxIdleConns(DbCfg.MaxIdleConn) - debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false) - if !debugSql { - engine.SetLogger(&xorm.DiscardLogger{}) - } else { - engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm"))) - engine.ShowSQL(true) - engine.ShowExecTime(true) - } } + + engine.SetMaxOpenConns(DbCfg.MaxOpenConn) + engine.SetMaxIdleConns(DbCfg.MaxIdleConn) + engine.SetConnMaxLifetime(time.Second * time.Duration(DbCfg.ConnMaxLifetime)) + debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false) + if !debugSql { + engine.SetLogger(&xorm.DiscardLogger{}) + } else { + engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm"))) + engine.ShowSQL(true) + engine.ShowExecTime(true) + } + return engine, nil } @@ -202,6 +208,7 @@ func LoadConfig() { } DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0) DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0) + DbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400) if DbCfg.Type == "sqlite3" { UseSQLite3 = true @@ -225,8 +232,8 @@ var ( func InitTestDB(t *testing.T) *xorm.Engine { selectedDb := dbSqlite - //selectedDb := dbMySql - //selectedDb := dbPostgres + // selectedDb := dbMySql + // selectedDb := dbPostgres var x *xorm.Engine var err error @@ -245,6 +252,9 @@ func InitTestDB(t *testing.T) *xorm.Engine { x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr) } + x.DatabaseTZ = time.UTC + x.TZLocation = time.UTC + // x.ShowSQL() if err != nil { diff --git a/pkg/services/sqlstore/sqlutil/sqlutil.go b/pkg/services/sqlstore/sqlutil/sqlutil.go index 26a58811e0f..46306ac8e0d 100644 --- a/pkg/services/sqlstore/sqlutil/sqlutil.go +++ b/pkg/services/sqlstore/sqlutil/sqlutil.go @@ -14,6 +14,7 @@ type TestDB struct { var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:"} var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci"} var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"} +var TestDB_Mssql = TestDB{DriverName: "mssql", ConnStr: "server=localhost;port=1433;database=grafanatest;user id=grafana;password=Password!"} func CleanDB(x *xorm.Engine) { if x.DriverName() == "postgres" { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 6099388f668..5b79e866964 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -131,7 +131,8 @@ var ( PluginAppsSkipVerifyTLS bool // Session settings. - SessionOptions session.Options + SessionOptions session.Options + SessionConnMaxLifetime int64 // Global setting objects. Cfg *ini.File @@ -634,6 +635,8 @@ func readSessionConfig() { if SessionOptions.CookiePath == "" { SessionOptions.CookiePath = "/" } + + SessionConnMaxLifetime = Cfg.Section("session").Key("conn_max_lifetime").MustInt64(14400) } func initLogging() { diff --git a/pkg/social/generic_oauth.go b/pkg/social/generic_oauth.go index 2411e0006a2..b92d64ad9fc 100644 --- a/pkg/social/generic_oauth.go +++ b/pkg/social/generic_oauth.go @@ -180,6 +180,7 @@ type UserInfoJson struct { func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { var data UserInfoJson + var err error if s.extractToken(&data, token) != true { response, err := HttpGet(client, s.apiUrl) @@ -193,20 +194,17 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token) } } - name, err := s.extractName(data) - if err != nil { - return nil, err + name := s.extractName(&data) + + email := s.extractEmail(&data) + if email == "" { + email, err = s.FetchPrivateEmail(client) + if err != nil { + return nil, err + } } - email, err := s.extractEmail(data, client) - if err != nil { - return nil, err - } - - login, err := s.extractLogin(data, email) - if err != nil { - return nil, err - } + login := s.extractLogin(&data, email) userInfo := &BasicUserInfo{ Name: name, @@ -251,49 +249,55 @@ func (s *SocialGenericOAuth) extractToken(data *UserInfoJson, token *oauth2.Toke return false } + email := s.extractEmail(data) + if email == "" { + s.log.Debug("No email found in id_token", "json", string(payload), "data", data) + return false + } + s.log.Debug("Received id_token", "json", string(payload), "data", data) return true } -func (s *SocialGenericOAuth) extractEmail(data UserInfoJson, client *http.Client) (string, error) { +func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string { if data.Email != "" { - return data.Email, nil + return data.Email } if data.Attributes["email:primary"] != nil { - return data.Attributes["email:primary"][0], nil + return data.Attributes["email:primary"][0] } if data.Upn != "" { emailAddr, emailErr := mail.ParseAddress(data.Upn) if emailErr == nil { - return emailAddr.Address, nil + return emailAddr.Address } } - return s.FetchPrivateEmail(client) + return "" } -func (s *SocialGenericOAuth) extractLogin(data UserInfoJson, email string) (string, error) { +func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson, email string) string { if data.Login != "" { - return data.Login, nil + return data.Login } if data.Username != "" { - return data.Username, nil + return data.Username } - return email, nil + return email } -func (s *SocialGenericOAuth) extractName(data UserInfoJson) (string, error) { +func (s *SocialGenericOAuth) extractName(data *UserInfoJson) string { if data.Name != "" { - return data.Name, nil + return data.Name } if data.DisplayName != "" { - return data.DisplayName, nil + return data.DisplayName } - return "", nil + return "" } diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index d5bdd010269..3879dce4ea6 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -152,8 +152,6 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simpl MetricName: aws.String(query.MetricName), Dimensions: query.Dimensions, Period: aws.Int64(int64(query.Period)), - StartTime: aws.Time(startTime), - EndTime: aws.Time(endTime), } if len(query.Statistics) > 0 { params.Statistics = query.Statistics @@ -162,15 +160,36 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simpl params.ExtendedStatistics = query.ExtendedStatistics } - if setting.Env == setting.DEV { - plog.Debug("CloudWatch query", "raw query", params) + // 1 minutes resolutin metrics is stored for 15 days, 15 * 24 * 60 = 21600 + if query.HighResolution && (((endTime.Unix() - startTime.Unix()) / int64(query.Period)) > 21600) { + return nil, errors.New("too long query period") } + var resp *cloudwatch.GetMetricStatisticsOutput + for startTime.Before(endTime) { + params.StartTime = aws.Time(startTime) + if query.HighResolution { + startTime = startTime.Add(time.Duration(1440*query.Period) * time.Second) + } else { + startTime = endTime + } + params.EndTime = aws.Time(startTime) - resp, err := client.GetMetricStatisticsWithContext(ctx, params, request.WithResponseReadTimeout(10*time.Second)) - if err != nil { - return nil, err + if setting.Env == setting.DEV { + plog.Debug("CloudWatch query", "raw query", params) + } + + partResp, err := client.GetMetricStatisticsWithContext(ctx, params, request.WithResponseReadTimeout(10*time.Second)) + if err != nil { + return nil, err + } + if resp != nil { + resp.Datapoints = append(resp.Datapoints, partResp.Datapoints...) + } else { + resp = partResp + + } + metrics.M_Aws_CloudWatch_GetMetricStatistics.Inc() } - metrics.M_Aws_CloudWatch_GetMetricStatistics.Inc() queryRes, err := parseResponse(resp, query) if err != nil { @@ -274,6 +293,8 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) { alias = "{{metric}}_{{stat}}" } + highResolution := model.Get("highResolution").MustBool(false) + return &CloudWatchQuery{ Region: region, Namespace: namespace, @@ -283,6 +304,7 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) { ExtendedStatistics: aws.StringSlice(extendedStatistics), Period: period, Alias: alias, + HighResolution: highResolution, }, nil } diff --git a/pkg/tsdb/cloudwatch/cloudwatch_test.go b/pkg/tsdb/cloudwatch/cloudwatch_test.go index 5c322a44d56..719edba08ba 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_test.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_test.go @@ -31,6 +31,7 @@ func TestCloudWatch(t *testing.T) { "p90.00" ], "period": "60", + "highResolution": false, "alias": "{{metric}}_{{stat}}" } ` diff --git a/pkg/tsdb/cloudwatch/types.go b/pkg/tsdb/cloudwatch/types.go index c2a5ab8c3d7..0737b64686d 100644 --- a/pkg/tsdb/cloudwatch/types.go +++ b/pkg/tsdb/cloudwatch/types.go @@ -13,4 +13,5 @@ type CloudWatchQuery struct { ExtendedStatistics []*string Period int Alias string + HighResolution bool } diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go new file mode 100644 index 00000000000..92c6ede148e --- /dev/null +++ b/pkg/tsdb/mssql/macros.go @@ -0,0 +1,129 @@ +package mssql + +import ( + "fmt" + "regexp" + "strings" + "time" + + "strconv" + + "github.com/grafana/grafana/pkg/tsdb" +) + +const rsIdentifier = `([_a-zA-Z0-9]+)` +const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` + +type MsSqlMacroEngine struct { + TimeRange *tsdb.TimeRange + Query *tsdb.Query +} + +func NewMssqlMacroEngine() tsdb.SqlMacroEngine { + return &MsSqlMacroEngine{} +} + +func (m *MsSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + m.TimeRange = timeRange + m.Query = query + rExp, _ := regexp.Compile(sExpr) + var macroError error + + sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { + args := strings.Split(groups[2], ",") + for i, arg := range args { + args[i] = strings.Trim(arg, " ") + } + res, err := m.evaluateMacro(groups[1], args) + if err != nil && macroError == nil { + macroError = err + return "macro_error()" + } + return res + }) + + if macroError != nil { + return "", macroError + } + + return sql, nil +} + +func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string { + result := "" + lastIndex := 0 + + for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { + groups := []string{} + for i := 0; i < len(v); i += 2 { + groups = append(groups, str[v[i]:v[i+1]]) + } + + result += str[lastIndex:v[0]] + repl(groups) + lastIndex = v[1] + } + + return result + str[lastIndex:] +} + +func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { + switch name { + case "__time": + if len(args) == 0 { + return "", fmt.Errorf("missing time column argument for macro %v", name) + } + return fmt.Sprintf("%s AS time", args[0]), nil + case "__utcTime": + if len(args) == 0 { + return "", fmt.Errorf("missing time column argument for macro %v", name) + } + return fmt.Sprintf("DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s) AS time", args[0]), nil + case "__timeEpoch": + if len(args) == 0 { + return "", fmt.Errorf("missing time column argument for macro %v", name) + } + return fmt.Sprintf("DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s) ) AS time", args[0]), nil + case "__timeFilter": + if len(args) == 0 { + return "", fmt.Errorf("missing time column argument for macro %v", name) + } + return fmt.Sprintf("%s >= DATEADD(s, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND %s <= DATEADD(s, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + case "__timeFrom": + return fmt.Sprintf("DATEADD(second, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil + case "__timeTo": + return fmt.Sprintf("DATEADD(second, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + case "__timeGroup": + if len(args) < 2 { + return "", fmt.Errorf("macro %v needs time column and interval", name) + } + interval, err := time.ParseDuration(strings.Trim(args[1], `'"`)) + if err != nil { + return "", fmt.Errorf("error parsing interval %v", args[1]) + } + if len(args) == 3 { + m.Query.Model.Set("fill", true) + m.Query.Model.Set("fillInterval", interval.Seconds()) + if args[2] == "NULL" { + m.Query.Model.Set("fillNull", true) + } else { + floatVal, err := strconv.ParseFloat(args[2], 64) + if err != nil { + return "", fmt.Errorf("error parsing fill value %v", args[2]) + } + m.Query.Model.Set("fillValue", floatVal) + } + } + return fmt.Sprintf("cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s))/%.0f as int)*%.0f as int)", args[0], interval.Seconds(), interval.Seconds()), nil + case "__unixEpochFilter": + if len(args) == 0 { + return "", fmt.Errorf("missing time column argument for macro %v", name) + } + return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + case "__unixEpochFrom": + return fmt.Sprintf("%d", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil + case "__unixEpochTo": + return fmt.Sprintf("%d", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + default: + return "", fmt.Errorf("Unknown macro %v", name) + } +} diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go new file mode 100644 index 00000000000..db1f5670924 --- /dev/null +++ b/pkg/tsdb/mssql/macros_test.go @@ -0,0 +1,131 @@ +package mssql + +import ( + "testing" + + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" + . "github.com/smartystreets/goconvey/convey" +) + +func TestMacroEngine(t *testing.T) { + Convey("MacroEngine", t, func() { + engine := &MsSqlMacroEngine{} + timeRange := &tsdb.TimeRange{From: "5m", To: "now"} + query := &tsdb.Query{ + Model: simplejson.New(), + } + + Convey("interpolate __time function", func() { + sql, err := engine.Interpolate(query, nil, "select $__time(time_column)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select time_column AS time") + }) + + Convey("interpolate __utcTime function", func() { + sql, err := engine.Interpolate(query, nil, "select $__utcTime(time_column)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) AS time") + }) + + Convey("interpolate __timeEpoch function", func() { + sql, err := engine.Interpolate(query, nil, "select $__timeEpoch(time_column)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) AS time") + }) + + Convey("interpolate __timeEpoch function wrapped in aggregation", func() { + sql, err := engine.Interpolate(query, nil, "select min($__timeEpoch(time_column))") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select min(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) AS time)") + }) + + Convey("interpolate __timeFilter function", func() { + sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "WHERE time_column >= DATEADD(s, 18446744066914186738+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND time_column <= DATEADD(s, 18446744066914187038+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')") + }) + + Convey("interpolate __timeGroup function", func() { + sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "GROUP BY cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column))/300 as int)*300 as int)") + }) + + Convey("interpolate __timeGroup function with spaces around arguments", func() { + sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "GROUP BY cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column))/300 as int)*300 as int)") + }) + + Convey("interpolate __timeGroup function with fill (value = NULL)", func() { + _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', NULL)") + + fill := query.Model.Get("fill").MustBool() + fillNull := query.Model.Get("fillNull").MustBool() + fillInterval := query.Model.Get("fillInterval").MustInt() + + So(err, ShouldBeNil) + So(fill, ShouldBeTrue) + So(fillNull, ShouldBeTrue) + So(fillInterval, ShouldEqual, 5*time.Minute.Seconds()) + }) + + Convey("interpolate __timeGroup function with fill (value = float)", func() { + _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', 1.5)") + + fill := query.Model.Get("fill").MustBool() + fillValue := query.Model.Get("fillValue").MustFloat64() + fillInterval := query.Model.Get("fillInterval").MustInt() + + So(err, ShouldBeNil) + So(fill, ShouldBeTrue) + So(fillValue, ShouldEqual, 1.5) + So(fillInterval, ShouldEqual, 5*time.Minute.Seconds()) + }) + + Convey("interpolate __timeFrom function", func() { + sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select DATEADD(second, 18446744066914186738+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')") + }) + + Convey("interpolate __timeTo function", func() { + sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select DATEADD(second, 18446744066914187038+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')") + }) + + Convey("interpolate __unixEpochFilter function", func() { + sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(18446744066914186738)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select 18446744066914186738 >= 18446744066914186738 AND 18446744066914186738 <= 18446744066914187038") + }) + + Convey("interpolate __unixEpochFrom function", func() { + sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select 18446744066914186738") + }) + + Convey("interpolate __unixEpochTo function", func() { + sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select 18446744066914187038") + }) + }) +} diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go new file mode 100644 index 00000000000..af68ca0424e --- /dev/null +++ b/pkg/tsdb/mssql/mssql.go @@ -0,0 +1,328 @@ +package mssql + +import ( + "container/list" + "context" + "database/sql" + "fmt" + "strconv" + "strings" + + "time" + + "math" + + _ "github.com/denisenkom/go-mssqldb" + "github.com/go-xorm/core" + "github.com/grafana/grafana/pkg/components/null" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/tsdb" +) + +type MssqlQueryEndpoint struct { + sqlEngine tsdb.SqlEngine + log log.Logger +} + +func init() { + tsdb.RegisterTsdbQueryEndpoint("mssql", NewMssqlQueryEndpoint) +} + +func NewMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { + endpoint := &MssqlQueryEndpoint{ + log: log.New("tsdb.mssql"), + } + + endpoint.sqlEngine = &tsdb.DefaultSqlEngine{ + MacroEngine: NewMssqlMacroEngine(), + } + + cnnstr := generateConnectionString(datasource) + endpoint.log.Debug("getEngine", "connection", cnnstr) + + if err := endpoint.sqlEngine.InitEngine("mssql", datasource, cnnstr); err != nil { + return nil, err + } + + return endpoint, nil +} + +func generateConnectionString(datasource *models.DataSource) string { + password := "" + for key, value := range datasource.SecureJsonData.Decrypt() { + if key == "password" { + password = value + break + } + } + + hostParts := strings.Split(datasource.Url, ":") + if len(hostParts) < 2 { + hostParts = append(hostParts, "1433") + } + + server, port := hostParts[0], hostParts[1] + return fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;", + server, + port, + datasource.Database, + datasource.User, + password, + ) +} + +// Query is the main function for the MssqlQueryEndpoint +func (e *MssqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { + return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable) +} + +func (e MssqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { + columnNames, err := rows.Columns() + columnCount := len(columnNames) + + if err != nil { + return err + } + + rowLimit := 1000000 + rowCount := 0 + timeIndex := -1 + + table := &tsdb.Table{ + Columns: make([]tsdb.TableColumn, columnCount), + Rows: make([]tsdb.RowValues, 0), + } + + for i, name := range columnNames { + table.Columns[i].Text = name + + // check if there is a column named time + switch name { + case "time": + timeIndex = i + } + } + + columnTypes, err := rows.ColumnTypes() + if err != nil { + return err + } + + for ; rows.Next(); rowCount++ { + if rowCount > rowLimit { + return fmt.Errorf("MsSQL query row limit exceeded, limit %d", rowLimit) + } + + values, err := e.getTypedRowData(columnTypes, rows) + if err != nil { + return err + } + + // convert column named time to unix timestamp to make + // native datetime mssql types work in annotation queries + if timeIndex != -1 { + switch value := values[timeIndex].(type) { + case time.Time: + values[timeIndex] = float64(value.Unix()) + } + } + + table.Rows = append(table.Rows, values) + } + + result.Tables = append(result.Tables, table) + result.Meta.Set("rowCount", rowCount) + return nil +} + +func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) { + values := make([]interface{}, len(types)) + valuePtrs := make([]interface{}, len(types)) + + for i, stype := range types { + e.log.Debug("type", "type", stype) + valuePtrs[i] = &values[i] + } + + if err := rows.Scan(valuePtrs...); err != nil { + return nil, err + } + + // convert types not handled by denisenkom/go-mssqldb + // unhandled types are returned as []byte + for i := 0; i < len(types); i++ { + if value, ok := values[i].([]byte); ok == true { + switch types[i].DatabaseTypeName() { + case "MONEY", "SMALLMONEY", "DECIMAL": + if v, err := strconv.ParseFloat(string(value), 64); err == nil { + values[i] = v + } else { + e.log.Debug("Rows", "Error converting numeric to float", value) + } + default: + e.log.Debug("Rows", "Unknown database type", types[i].DatabaseTypeName(), "value", value) + values[i] = string(value) + } + } + } + + return values, nil +} + +func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { + pointsBySeries := make(map[string]*tsdb.TimeSeries) + seriesByQueryOrder := list.New() + + columnNames, err := rows.Columns() + if err != nil { + return err + } + + columnTypes, err := rows.ColumnTypes() + if err != nil { + return err + } + + rowLimit := 1000000 + rowCount := 0 + timeIndex := -1 + metricIndex := -1 + + // check columns of resultset: a column named time is mandatory + // the first text column is treated as metric name unless a column named metric is present + for i, col := range columnNames { + switch col { + case "time": + timeIndex = i + case "metric": + metricIndex = i + default: + if metricIndex == -1 { + switch columnTypes[i].DatabaseTypeName() { + case "VARCHAR", "CHAR", "NVARCHAR", "NCHAR": + metricIndex = i + } + } + } + } + + if timeIndex == -1 { + return fmt.Errorf("Found no column named time") + } + + fillMissing := query.Model.Get("fill").MustBool(false) + var fillInterval float64 + fillValue := null.Float{} + if fillMissing { + fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 + if query.Model.Get("fillNull").MustBool(false) == false { + fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() + fillValue.Valid = true + } + } + + for rows.Next() { + var timestamp float64 + var value null.Float + var metric string + + if rowCount > rowLimit { + return fmt.Errorf("MSSQL query row limit exceeded, limit %d", rowLimit) + } + + values, err := e.getTypedRowData(columnTypes, rows) + if err != nil { + return err + } + + switch columnValue := values[timeIndex].(type) { + case int64: + timestamp = float64(columnValue * 1000) + case float64: + timestamp = columnValue * 1000 + case time.Time: + timestamp = (float64(columnValue.Unix()) * 1000) + float64(columnValue.Nanosecond()/1e6) // in case someone is trying to map times beyond 2262 :D + default: + return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp") + } + + if metricIndex >= 0 { + if columnValue, ok := values[metricIndex].(string); ok == true { + metric = columnValue + } else { + return fmt.Errorf("Column metric must be of type CHAR, VARCHAR, NCHAR or NVARCHAR. metric column name: %s type: %s but datatype is %T", columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex]) + } + } + + for i, col := range columnNames { + if i == timeIndex || i == metricIndex { + continue + } + + switch columnValue := values[i].(type) { + case int64: + value = null.FloatFrom(float64(columnValue)) + case float64: + value = null.FloatFrom(columnValue) + case nil: + value.Valid = false + default: + return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue) + } + if metricIndex == -1 { + metric = col + } + + series, exist := pointsBySeries[metric] + if exist == false { + series = &tsdb.TimeSeries{Name: metric} + pointsBySeries[metric] = series + seriesByQueryOrder.PushBack(metric) + } + + if fillMissing { + var intervalStart float64 + if exist == false { + intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) + } else { + intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval + } + + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + + for i := intervalStart; i < timestamp; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } + } + + series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)}) + + e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value) + } + } + + for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { + key := elem.Value.(string) + result.Series = append(result.Series, pointsBySeries[key]) + + if fillMissing { + series := pointsBySeries[key] + // fill in values from last fetched value till interval end + intervalStart := series.Points[len(series.Points)-1][1].Float64 + intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) + + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } + } + } + + result.Meta.Set("rowCount", rowCount) + return nil +} diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go new file mode 100644 index 00000000000..88b35b1aa2c --- /dev/null +++ b/pkg/tsdb/mssql/mssql_test.go @@ -0,0 +1,686 @@ +package mssql + +import ( + "fmt" + "math/rand" + "strings" + "testing" + "time" + + "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" + "github.com/grafana/grafana/pkg/tsdb" + . "github.com/smartystreets/goconvey/convey" +) + +// To run this test, remove the Skip from SkipConvey +// and set up a MSSQL db named grafanatest and a user/password grafana/Password! +// Use the docker/blocks/mssql_tests/docker-compose.yaml to spin up a +// preconfigured MSSQL server suitable for running these tests. +// If needed, change the variable below to the IP address of the database. +var serverIP string = "localhost" + +func TestMSSQL(t *testing.T) { + SkipConvey("MSSQL", t, func() { + x := InitMSSQLTestDB(t) + + endpoint := &MssqlQueryEndpoint{ + sqlEngine: &tsdb.DefaultSqlEngine{ + MacroEngine: NewMssqlMacroEngine(), + XormEngine: x, + }, + log: log.New("tsdb.mssql"), + } + + sess := x.NewSession() + defer sess.Close() + + fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC) + + Convey("Given a table with different native data types", func() { + sql := ` + IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL + DROP TABLE dbo.[mssql_types] + + CREATE TABLE [mssql_types] ( + c_bit bit, + + c_tinyint tinyint, + c_smallint smallint, + c_int int, + c_bigint bigint, + + c_money money, + c_smallmoney smallmoney, + c_numeric numeric(10,5), + c_real real, + c_decimal decimal(10,2), + c_float float, + + c_char char(10), + c_varchar varchar(10), + c_text text, + + c_nchar nchar(12), + c_nvarchar nvarchar(12), + c_ntext ntext, + + c_datetime datetime, + c_datetime2 datetime2, + c_smalldatetime smalldatetime, + c_date date, + c_time time, + c_datetimeoffset datetimeoffset + ) + ` + + _, err := sess.Exec(sql) + So(err, ShouldBeNil) + + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + dtFormat := "2006-01-02 15:04:05.999999999" + d := dt.Format(dtFormat) + dt2 := time.Date(2018, 3, 14, 21, 20, 6, 8896406e2, time.UTC) + dt2Format := "2006-01-02 15:04:05.999999999 -07:00" + d2 := dt2.Format(dt2Format) + + sql = fmt.Sprintf(` + INSERT INTO [mssql_types] + SELECT + 1, 5, 20020, 980300, 1420070400, '$20000.15', '£2.15', 12345.12, + 1.11, 2.22, 3.33, + 'char10', 'varchar10', 'text', + N'☺nchar12☺', N'☺nvarchar12☺', N'☺text☺', + CAST('%s' AS DATETIME), CAST('%s' AS DATETIME2), CAST('%s' AS SMALLDATETIME), CAST('%s' AS DATE), CAST('%s' AS TIME), SWITCHOFFSET(CAST('%s' AS DATETIMEOFFSET), '-07:00') + `, d, d2, d, d, d, d2) + + _, err = sess.Exec(sql) + So(err, ShouldBeNil) + + Convey("When doing a table query should map MSSQL column types to Go types", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT * FROM mssql_types", + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["A"] + So(err, ShouldBeNil) + + column := queryResult.Tables[0].Rows[0] + + So(column[0].(bool), ShouldEqual, true) + + So(column[1].(int64), ShouldEqual, 5) + So(column[2].(int64), ShouldEqual, 20020) + So(column[3].(int64), ShouldEqual, 980300) + So(column[4].(int64), ShouldEqual, 1420070400) + + So(column[5].(float64), ShouldEqual, 20000.15) + So(column[6].(float64), ShouldEqual, 2.15) + So(column[7].(float64), ShouldEqual, 12345.12) + So(column[8].(float64), ShouldEqual, 1.1100000143051147) + So(column[9].(float64), ShouldEqual, 2.22) + So(column[10].(float64), ShouldEqual, 3.33) + + So(column[11].(string), ShouldEqual, "char10 ") + So(column[12].(string), ShouldEqual, "varchar10") + So(column[13].(string), ShouldEqual, "text") + + So(column[14].(string), ShouldEqual, "☺nchar12☺ ") + So(column[15].(string), ShouldEqual, "☺nvarchar12☺") + So(column[16].(string), ShouldEqual, "☺text☺") + + So(column[17].(time.Time), ShouldEqual, dt) + So(column[18].(time.Time), ShouldEqual, dt2) + So(column[19].(time.Time), ShouldEqual, dt.Truncate(time.Minute)) + So(column[20].(time.Time), ShouldEqual, dt.Truncate(24*time.Hour)) + So(column[21].(time.Time), ShouldEqual, time.Date(1, 1, 1, dt.Hour(), dt.Minute(), dt.Second(), dt.Nanosecond(), time.UTC)) + So(column[22].(time.Time), ShouldEqual, dt2.In(time.FixedZone("UTC", int(-7*time.Hour)))) + }) + }) + + Convey("Given a table with metrics that lacks data for some series ", func() { + sql := ` + IF OBJECT_ID('dbo.[metric]', 'U') IS NOT NULL + DROP TABLE dbo.[metric] + + CREATE TABLE [metric] ( + time datetime, + value int + ) + ` + + _, err := sess.Exec(sql) + So(err, ShouldBeNil) + + type metric struct { + Time time.Time + Value int64 + } + + series := []*metric{} + firstRange := genTimeRangeByInterval(fromStart, 10*time.Minute, 10*time.Second) + secondRange := genTimeRangeByInterval(fromStart.Add(20*time.Minute), 10*time.Minute, 10*time.Second) + + for _, t := range firstRange { + series = append(series, &metric{ + Time: t, + Value: 15, + }) + } + + for _, t := range secondRange { + series = append(series, &metric{ + Time: t, + Value: 20, + }) + } + + dtFormat := "2006-01-02 15:04:05.999999999" + for _, s := range series { + sql = fmt.Sprintf(` + INSERT INTO metric (time, value) + VALUES(CAST('%s' AS DATETIME), %d) + `, s.Time.Format(dtFormat), s.Value) + + _, err = sess.Exec(sql) + So(err, ShouldBeNil) + } + + Convey("When doing a metric query using timeGroup", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["A"] + So(err, ShouldBeNil) + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + + So(len(points), ShouldEqual, 4) + actualValueFirst := points[0][0].Float64 + actualTimeFirst := time.Unix(int64(points[0][1].Float64)/1000, 0) + So(actualValueFirst, ShouldEqual, 15) + So(actualTimeFirst, ShouldEqual, fromStart) + + actualValueLast := points[3][0].Float64 + actualTimeLast := time.Unix(int64(points[3][1].Float64)/1000, 0) + So(actualValueLast, ShouldEqual, 20) + So(actualTimeLast, ShouldEqual, fromStart.Add(25*time.Minute)) + }) + + Convey("When doing a metric query using timeGroup with NULL fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["A"] + So(err, ShouldBeNil) + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + + So(len(points), ShouldEqual, 7) + actualValueFirst := points[0][0].Float64 + actualTimeFirst := time.Unix(int64(points[0][1].Float64)/1000, 0) + So(actualValueFirst, ShouldEqual, 15) + So(actualTimeFirst, ShouldEqual, fromStart) + + actualNullPoint := points[3][0] + actualNullTime := time.Unix(int64(points[3][1].Float64)/1000, 0) + So(actualNullPoint.Valid, ShouldBeFalse) + So(actualNullTime, ShouldEqual, fromStart.Add(15*time.Minute)) + + actualValueLast := points[5][0].Float64 + actualTimeLast := time.Unix(int64(points[5][1].Float64)/1000, 0) + So(actualValueLast, ShouldEqual, 20) + So(actualTimeLast, ShouldEqual, fromStart.Add(25*time.Minute)) + + actualLastNullPoint := points[6][0] + actualLastNullTime := time.Unix(int64(points[6][1].Float64)/1000, 0) + So(actualLastNullPoint.Valid, ShouldBeFalse) + So(actualLastNullTime, ShouldEqual, fromStart.Add(30*time.Minute)) + + }) + + Convey("When doing a metric query using timeGroup with float fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', 1.5) AS time, avg(value) as value FROM metric GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["A"] + So(err, ShouldBeNil) + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + + So(points[6][0].Float64, ShouldEqual, 1.5) + }) + }) + + Convey("Given a table with metrics having multiple values and measurements", func() { + sql := ` + IF OBJECT_ID('dbo.[metric_values]', 'U') IS NOT NULL + DROP TABLE dbo.[metric_values] + + CREATE TABLE [metric_values] ( + time datetime, + measurement nvarchar(100), + valueOne int, + valueTwo int, + ) + ` + + _, err := sess.Exec(sql) + So(err, ShouldBeNil) + + type metricValues struct { + Time time.Time + Measurement string + ValueOne int64 + ValueTwo int64 + } + + rand.Seed(time.Now().Unix()) + rnd := func(min, max int64) int64 { + return rand.Int63n(max-min) + min + } + + series := []*metricValues{} + for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) { + series = append(series, &metricValues{ + Time: t, + Measurement: "Metric A", + ValueOne: rnd(0, 100), + ValueTwo: rnd(0, 100), + }) + series = append(series, &metricValues{ + Time: t, + Measurement: "Metric B", + ValueOne: rnd(0, 100), + ValueTwo: rnd(0, 100), + }) + } + + dtFormat := "2006-01-02 15:04:05" + for _, s := range series { + sql = fmt.Sprintf(` + INSERT metric_values (time, measurement, valueOne, valueTwo) + VALUES(CAST('%s' AS DATETIME), '%s', %d, %d) + `, s.Time.Format(dtFormat), s.Measurement, s.ValueOne, s.ValueTwo) + + _, err = sess.Exec(sql) + So(err, ShouldBeNil) + } + + Convey("When doing a metric query grouping by time and select metric column should return correct series", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["A"] + So(err, ShouldBeNil) + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 2) + So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one") + So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") + }) + + Convey("When doing a metric query grouping by time should return correct series", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["A"] + So(err, ShouldBeNil) + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 2) + So(queryResult.Series[0].Name, ShouldEqual, "valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "valueTwo") + }) + + Convey("Given a stored procedure that takes @from and @to in epoch time", func() { + sql := ` + IF object_id('sp_test_epoch') IS NOT NULL + DROP PROCEDURE sp_test_epoch + ` + + _, err := sess.Exec(sql) + So(err, ShouldBeNil) + + sql = ` + CREATE PROCEDURE sp_test_epoch( + @from int, + @to int + ) AS + BEGIN + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + measurement + ' - value one' as metric, + avg(valueOne) as value + FROM + metric_values + WHERE + time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + measurement + UNION ALL + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + measurement + ' - value two' as metric, + avg(valueTwo) as value + FROM + metric_values + WHERE + time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + measurement + ORDER BY 1 + END + ` + + _, err = sess.Exec(sql) + So(err, ShouldBeNil) + + Convey("When doing a metric query using stored procedure should return correct result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `DECLARE + @from int = $__unixEpochFrom(), + @to int = $__unixEpochTo() + + EXEC dbo.sp_test_epoch @from, @to`, + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: "1521117000000", + To: "1521122100000", + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["A"] + So(err, ShouldBeNil) + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 4) + So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one") + So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") + So(queryResult.Series[2].Name, ShouldEqual, "Metric A - value two") + So(queryResult.Series[3].Name, ShouldEqual, "Metric B - value two") + }) + }) + + Convey("Given a stored procedure that takes @from and @to in datetime", func() { + sql := ` + IF object_id('sp_test_datetime') IS NOT NULL + DROP PROCEDURE sp_test_datetime + ` + + _, err := sess.Exec(sql) + So(err, ShouldBeNil) + + sql = ` + CREATE PROCEDURE sp_test_datetime( + @from datetime, + @to datetime + ) AS + BEGIN + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + measurement + ' - value one' as metric, + avg(valueOne) as value + FROM + metric_values + WHERE + time >= @from AND time <= @to + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + measurement + UNION ALL + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + measurement + ' - value two' as metric, + avg(valueTwo) as value + FROM + metric_values + WHERE + time >= @from AND time <= @to + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + measurement + ORDER BY 1 + END + ` + + _, err = sess.Exec(sql) + So(err, ShouldBeNil) + + Convey("When doing a metric query using stored procedure should return correct result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `DECLARE + @from int = $__unixEpochFrom(), + @to int = $__unixEpochTo() + + EXEC dbo.sp_test_epoch @from, @to`, + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: "1521117000000", + To: "1521122100000", + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["A"] + So(err, ShouldBeNil) + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 4) + So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one") + So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") + So(queryResult.Series[2].Name, ShouldEqual, "Metric A - value two") + So(queryResult.Series[3].Name, ShouldEqual, "Metric B - value two") + }) + }) + }) + + Convey("Given a table with event data", func() { + sql := ` + IF OBJECT_ID('dbo.[event]', 'U') IS NOT NULL + DROP TABLE dbo.[event] + + CREATE TABLE [event] ( + time_sec bigint, + description nvarchar(100), + tags nvarchar(100), + ) + ` + + _, err := sess.Exec(sql) + So(err, ShouldBeNil) + + type event struct { + TimeSec int64 + Description string + Tags string + } + + events := []*event{} + for _, t := range genTimeRangeByInterval(fromStart.Add(-20*time.Minute), 60*time.Minute, 25*time.Minute) { + events = append(events, &event{ + TimeSec: t.Unix(), + Description: "Someone deployed something", + Tags: "deploy", + }) + events = append(events, &event{ + TimeSec: t.Add(5 * time.Minute).Unix(), + Description: "New support ticket registered", + Tags: "ticket", + }) + } + + for _, e := range events { + sql = fmt.Sprintf(` + INSERT [event] (time_sec, description, tags) + VALUES(%d, '%s', '%s') + `, e.TimeSec, e.Description, e.Tags) + + _, err = sess.Exec(sql) + So(err, ShouldBeNil) + } + + Convey("When doing an annotation query of deploy events should return expected result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT time_sec as time, description as [text], tags FROM [event] WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC", + "format": "table", + }), + RefId: "Deploys", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Deploys"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + }) + + Convey("When doing an annotation query of ticket events should return expected result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT time_sec as time, description as [text], tags FROM [event] WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC", + "format": "table", + }), + RefId: "Tickets", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Tickets"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + }) + }) + }) +} + +func InitMSSQLTestDB(t *testing.T) *xorm.Engine { + x, err := xorm.NewEngine(sqlutil.TestDB_Mssql.DriverName, strings.Replace(sqlutil.TestDB_Mssql.ConnStr, "localhost", serverIP, 1)) + + // x.ShowSQL() + + if err != nil { + t.Fatalf("Failed to init mssql db %v", err) + } + + sqlutil.CleanDB(x) + + return x +} + +func genTimeRangeByInterval(from time.Time, duration time.Duration, interval time.Duration) []time.Time { + durationSec := int64(duration.Seconds()) + intervalSec := int64(interval.Seconds()) + timeRange := []time.Time{} + + for i := int64(0); i < durationSec; i += intervalSec { + timeRange = append(timeRange, from) + from = from.Add(time.Duration(int64(time.Second) * intervalSec)) + } + + return timeRange +} diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index 29daa0c3bb4..692b891eddd 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -22,23 +22,10 @@ import ( ) type OpenTsdbExecutor struct { - //*models.DataSource - //httpClient *http.Client } func NewOpenTsdbExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { - /* - httpClient, err := datasource.GetHttpClient() - - if err != nil { - return nil, err - } - */ - - return &OpenTsdbExecutor{ - //DataSource: datasource, - //httpClient: httpClient, - }, nil + return &OpenTsdbExecutor{}, nil } var ( diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx index 586a8f05b4c..e56659ad789 100644 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ b/public/app/containers/ManageDashboards/FolderSettings.tsx @@ -143,7 +143,7 @@ export class FolderSettings extends React.Component { className="btn btn-success" disabled={!folder.folder.canSave || !folder.folder.hasChanged} > - Save + Save