diff --git a/CHANGELOG.md b/CHANGELOG.md index 5baddec644d..fdea0378b1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,9 @@ * **Elasticsearch**: Support to set Precision Threshold for Unique Count metric, closes [#4689](https://github.com/grafana/grafana/issues/4689) * **Navigation**: Add search to org swithcer, closes [#2609](https://github.com/grafana/grafana/issues/2609) * **Database**: Allow database config using one propertie, closes [#5456](https://github.com/grafana/grafana/pull/5456) -* **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613) +* **Graphite**: Add support for groupByNodes, closes [#5613](https://github.com/grafana/grafana/pull/5613) * **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827) +* **OpenTSDB**: Add support for explicitTags for OpenTSDB>=2.3, closes [#6360](https://github.com/grafana/grafana/pull/6361) * **OAuth**: Add support for generic oauth, closes [#4718](https://github.com/grafana/grafana/pull/4718) * **Cloudwatch**: Add support to expand multi select template variable, closes [#5003](https://github.com/grafana/grafana/pull/5003) * **Graph Panel**: Now supports flexible lower/upper bounds on Y-Max and Y-Min, PR [#5720](https://github.com/grafana/grafana/pull/5720) @@ -27,6 +28,7 @@ * **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070) * **Snapshot**: Can view embedded panels/png rendered panels in snapshots without login, fixes [#3769](https://github.com/grafana/grafana/pull/3769) * **Elasticsearch**: Fix for query template variable when looking up terms without query, no longer relies on elasticsearch default field, fixes [#3887](https://github.com/grafana/grafana/pull/3887) +* **Elasticsearch**: Fix for displaying IP address used in terms aggregations, fixes [#4393](https://github.com/grafana/grafana/pull/4393) * **PNG Rendering**: Fix for server side rendering when using auth proxy, fixes [#5906](https://github.com/grafana/grafana/pull/5906) # 3.1.2 (unreleased) diff --git a/Gruntfile.js b/Gruntfile.js index 1f96048746b..50ac332d894 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -12,11 +12,15 @@ module.exports = function (grunt) { platform: process.platform.replace('win32', 'windows'), }; - if (process.platform.match(/^win/)) { - config.arch = process.env.hasOwnProperty('ProgramFiles(x86)') ? 'x64' : 'x86'; - } + if (grunt.option('arch')) { + config.arch = grunt.option('arch'); + } else { + config.arch = os.arch(); - config.arch = grunt.option('arch') || os.arch(); + if (process.platform.match(/^win/)) { + config.arch = process.env.hasOwnProperty('ProgramFiles(x86)') ? 'x64' : 'x86'; + } + } config.phjs = grunt.option('phjsToRelease'); diff --git a/Makefile b/Makefile index e335993b8b0..40597a33f79 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,28 @@ all: deps build -deps: +deps-go: go run build.go setup + +deps-js: npm install -build: +deps: deps-go deps-js + +build-go: go run build.go build + +build-js: npm run build -test: +build: build-go build-js + +test-go: go test -v ./pkg/... + +test-js: npm test +test: test-go test-js + run: ./bin/grafana-server diff --git a/README.md b/README.md index 98f4b4d3c9d..0a969f248d8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [Website](http://grafana.org) | [Twitter](https://twitter.com/grafana) | [IRC](https://webchat.freenode.net/?channels=grafana) | -![](https://brandfolder.com/api/favicon/icon?size=16&domain=www.slack.com) +[![Slack](https://brandfolder.com/api/favicon/icon?size=16&domain=www.slack.com)](http://slack.raintank.io) [Slack](http://slack.raintank.io) | [Email](mailto:contact@grafana.org) @@ -74,7 +74,7 @@ Be sure to read the [getting started guide](http://docs.grafana.org/guides/getti ## Run from master If you want to build a package yourself, or contribute. Here is a guide for how to do that. You can always find -the latest master builds [here](http://grafana.org/download/builds) +the latest master builds [here](http://grafana.org/builds) ### Dependencies @@ -87,11 +87,11 @@ the latest master builds [here](http://grafana.org/download/builds) go get github.com/grafana/grafana ``` -Since imports of dependencies use the absolute path github.com/grafana/grafana within the $GOPATH, -you will need to put your version of the code in $GOPATH/src/github.com/grafana/grafana to be able +Since imports of dependencies use the absolute path `github.com/grafana/grafana` within the `$GOPATH`, +you will need to put your version of the code in `$GOPATH/src/github.com/grafana/grafana` to be able to develop and build grafana on a cloned repository. To do so, you can clone your forked repository -directly to $GOPATH/src/github.com/grafana or you can create a symbolic link from your version -of the code to $GOPATH/src/github.com/grafana/grafana. The last options makes it possible to change +directly to `$GOPATH/src/github.com/grafana` or you can create a symbolic link from your version +of the code to `$GOPATH/src/github.com/grafana/grafana`. The last options makes it possible to change easily the grafana repository you want to build. ```bash go get github.com/*your_account*/grafana @@ -108,7 +108,7 @@ go run build.go build ### Building frontend assets -To build less to css for the frontend you will need a recent version of of **node (v4+)**, +To build less to css for the frontend you will need a recent version of **node (v4+)**, npm (v2.5.0) and grunt (v0.4.5). Run the following: ```bash @@ -135,7 +135,7 @@ bra run ./bin/grafana-server ``` -Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin). +Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`). ### Dev config @@ -147,7 +147,7 @@ You only need to add the options you want to override. Config files are applied 3. custom.ini ## Create a pull request -Before or after you create a pull request, sign the [contributor license agreement](http://grafana.org/docs/contributing/cla.html). +Before or after you create a pull request, sign the [contributor license agreement](http://docs.grafana.org/project/cla/). ## Contribute If you have any idea for an improvement or found a bug do not hesitate to open an issue. diff --git a/appveyor.yml b/appveyor.yml index 1b6027b5eb6..756dbf8fba5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -25,7 +25,13 @@ install: build_script: - go run build.go build - grunt release + - go run build.go sha1-dist + - cp dist/* . artifacts: - - path: dist/* + - path: grafana-*windows-*.* name: binzip + +deploy: + - provider: Environment + name: GrafanaBuildsS3 diff --git a/build.go b/build.go index 202caa1837b..de828d3ef91 100644 --- a/build.go +++ b/build.go @@ -5,6 +5,7 @@ package main import ( "bytes" "crypto/md5" + "crypto/sha1" "encoding/json" "flag" "fmt" @@ -85,17 +86,24 @@ func main() { case "package": grunt(gruntBuildArg("release")...) createLinuxPackages() + sha1FilesInDist() case "pkg-rpm": grunt(gruntBuildArg("release")...) createRpmPackages() + sha1FilesInDist() case "pkg-deb": grunt(gruntBuildArg("release")...) createDebPackages() + sha1FilesInDist() + + case "sha1-dist": + sha1FilesInDist() case "latest": makeLatestDistCopies() + sha1FilesInDist() case "clean": clean() @@ -501,3 +509,38 @@ func md5File(file string) error { return out.Close() } + +func sha1FilesInDist() { + filepath.Walk("./dist", func(path string, f os.FileInfo, err error) error { + if strings.Contains(path, ".sha1") == false { + sha1File(path) + } + return nil + }) +} + +func sha1File(file string) error { + fd, err := os.Open(file) + if err != nil { + return err + } + defer fd.Close() + + h := sha1.New() + _, err = io.Copy(h, fd) + if err != nil { + return err + } + + out, err := os.Create(file + ".sha1") + if err != nil { + return err + } + + _, err = fmt.Fprintf(out, "%x\n", h.Sum(nil)) + if err != nil { + return err + } + + return out.Close() +} diff --git a/circle.yml b/circle.yml index 007d0b29217..39dc626df04 100644 --- a/circle.yml +++ b/circle.yml @@ -28,3 +28,4 @@ deployment: owner: grafana commands: - ./scripts/trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} + - ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} diff --git a/conf/defaults.ini b/conf/defaults.ini index 39e10280645..cbc239937c6 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -267,6 +267,7 @@ auto_sign_up = true [auth.ldap] enabled = false config_file = /etc/grafana/ldap.toml +allow_sign_up = true #################################### SMTP / Emailing ##################### [smtp] @@ -292,6 +293,9 @@ mode = console, file # Either "debug", "info", "warn", "error", "critical", default is "info" level = info +# optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug +filters = + # For "console" mode only [log.console] level = @@ -401,7 +405,8 @@ global_session = -1 # \______(_______;;;)__;;;) [alerting] -enabled = true +# Makes it possible to turn off alert rule execution. +execute_alerts = true #################################### Internal Grafana Metrics ############ # Metrics available at HTTP API Url /api/metrics @@ -421,7 +426,7 @@ url = https://grafana.net #################################### External Image Storage ############## [external_image_storage] # You can choose between (s3, webdav) -provider = s3 +provider = [external_image_storage.s3] bucket_url = diff --git a/conf/sample.ini b/conf/sample.ini index bb575a596b0..e1ed408210a 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -252,6 +252,7 @@ [auth.ldap] ;enabled = false ;config_file = /etc/grafana/ldap.toml +;allow_sign_up = true #################################### SMTP / Emailing ########################## [smtp] @@ -276,6 +277,10 @@ # Either "trace", "debug", "info", "warn", "error", "critical", default is "info" ;level = info +# optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug +;filters = + + # For "console" mode only [log.console] ;level = @@ -350,7 +355,8 @@ # \______(_______;;;)__;;;) [alerting] -;enabled = false +# Makes it possible to turn off alert rule execution. +;execute_alerts = true #################################### Internal Grafana Metrics ########################## # Metrics available at HTTP API Url /api/metrics @@ -375,8 +381,8 @@ #################################### External image storage ########################## [external_image_storage] # Used for uploading images to public servers so they can be included in slack/email messages. -# you can choose between (s3, webdav or internal) -;provider = s3 +# you can choose between (s3, webdav) +;provider = [external_image_storage.s3] ;bucket_url = diff --git a/docker/blocks/influxdb/config.toml b/docker/blocks/influxdb/config.toml deleted file mode 100644 index 23834b22e3d..00000000000 --- a/docker/blocks/influxdb/config.toml +++ /dev/null @@ -1,75 +0,0 @@ -bind-address = "0.0.0.0" - -[logging] -level = "debug" -file = "/opt/influxdb/shared/data/influxdb.log" # stdout to log to standard out - -[admin] -port = 8083 # binding is disabled if the port isn't set -assets = "/opt/influxdb/current/admin" - -[api] -port = 8086 # binding is disabled if the port isn't set - -read-timeout = "5s" - -[input_plugins] - - [input_plugins.graphite] - enabled = true - port = 2004 - database = "graphite" # store graphite data in this database - - -[raft] -port = 8090 -dir = "/opt/influxdb/shared/data/raft" - -[storage] -dir = "/opt/influxdb/shared/data/db" -# How many requests to potentially buffer in memory. If the buffer gets filled then writes -# will still be logged and once the local storage has caught up (or compacted) the writes -# will be replayed from the WAL -write-buffer-size = 10000 -default-engine = "rocksdb" -max-open-shards = 0 -point-batch-size = 100 -write-batch-size = 5000000 -retention-sweep-period = "10m" - -[storage.engines.rocksdb] -max-open-files = 1000 -lru-cache-size = "200m" - -[storage.engines.leveldb] -max-open-files = 1000 -lru-cache-size = "200m" - -[cluster] -protobuf_port = 8099 -protobuf_timeout = "2s" # the write timeout on the protobuf conn any duration parseable by time.ParseDuration -protobuf_heartbeat = "200ms" # the heartbeat interval between the servers. must be parseable by time.ParseDuration -protobuf_min_backoff = "1s" # the minimum backoff after a failed heartbeat attempt -protobuf_max_backoff = "10s" # the maxmimum backoff after a failed heartbeat attempt -write-buffer-size = 10000 -ax-response-buffer-size = 100000 -oncurrent-shard-query-limit = 10 - -[sharding] - replication-factor = 1 - - [sharding.short-term] - duration = "7d" - split = 1 - - [sharding.long-term] - duration = "30d" - split = 1 - # split-random = "/^Hf.*/" - -[wal] -dir = "/opt/influxdb/shared/data/wal" -flush-after = 1000 # the number of writes after which wal will be flushed, 0 for flushing on every write -bookmark-after = 1000 # the number of writes after which a bookmark will be created -index-after = 1000 -requests-per-logfile = 10000 diff --git a/docker/blocks/influxdb/fig b/docker/blocks/influxdb/fig index bdb4a274634..8821c010a98 100644 --- a/docker/blocks/influxdb/fig +++ b/docker/blocks/influxdb/fig @@ -1,9 +1,12 @@ influxdb: - image: tutum/influxdb:0.12 + image: influxdb:latest + container_name: influxdb ports: - "2004:2004" - "8083:8083" - "8086:8086" + volumes: + - ./blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf fake-influxdb-data: image: grafana/fake-data-gen diff --git a/docker/blocks/influxdb/influxdb.conf b/docker/blocks/influxdb/influxdb.conf new file mode 100644 index 00000000000..c0331ce7449 --- /dev/null +++ b/docker/blocks/influxdb/influxdb.conf @@ -0,0 +1,92 @@ +reporting-disabled = false + +[meta] + # Where the metadata/raft database is stored + dir = "/var/lib/influxdb/meta" + + retention-autocreate = true + + # If log messages are printed for the meta service + logging-enabled = true + pprof-enabled = false + + # The default duration for leases. + lease-duration = "1m0s" + +[data] + # Controls if this node holds time series data shards in the cluster + enabled = true + + dir = "/var/lib/influxdb/data" + + # These are the WAL settings for the storage engine >= 0.9.3 + wal-dir = "/var/lib/influxdb/wal" + wal-logging-enabled = true + + +[coordinator] + write-timeout = "10s" + max-concurrent-queries = 0 + query-timeout = "0" + log-queries-after = "0" + max-select-point = 0 + max-select-series = 0 + max-select-buckets = 0 + +[retention] + enabled = true + check-interval = "30m" + +[shard-precreation] + enabled = true + check-interval = "10m" + advance-period = "30m" + +[monitor] + store-enabled = true # Whether to record statistics internally. + store-database = "_internal" # The destination database for recorded statistics + store-interval = "10s" # The interval at which to record statistics + +[admin] + enabled = true + bind-address = ":8083" + https-enabled = false + https-certificate = "/etc/ssl/influxdb.pem" + +[http] + enabled = true + bind-address = ":8086" + auth-enabled = true + log-enabled = true + write-tracing = false + pprof-enabled = false + https-enabled = false + https-certificate = "/etc/ssl/influxdb.pem" + ### Use a separate private key location. + # https-private-key = "" + max-row-limit = 10000 + realm = "InfluxDB" + + unix-socket-enabled = false # enable http service over unix domain socket + # bind-socket = "/var/run/influxdb.sock" + +[subscriber] + enabled = true + +[[graphite]] + enabled = false + +[[collectd]] + enabled = false + +[[opentsdb]] + enabled = false + +[[udp]] + enabled = false + +[continuous_queries] + log-enabled = true + enabled = true + # run-interval = "1s" # interval for how often continuous queries will be checked if they need to run + diff --git a/docs/Dockerfile b/docs/Dockerfile index 765ca86dbb7..7652cbd3e3f 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,25 +1,12 @@ FROM grafana/docs-base:latest -# TODO: need the full repo source to get the git version info -COPY . /src +# to get the git info for this repo +# COPY config.toml /site -# Reset the /docs dir so we can replace the theme meta with the new repo's git info -RUN git reset --hard +RUN rm -rf /site/content/* -# Then copy the desired docs into the /docs/sources/ dir -COPY ./sources/ /docs/sources +COPY ./sources /site/content/ -COPY ./VERSION /docs/VERSION +COPY awsconfig /site -COPY ./changed-files /docs/changed-files - -# adding the image spec will require Docker 1.5 and `docker build -f docs/Dockerfile .` -#COPY ./image/spec/v1.md /docs/sources/reference/image-spec-v1.md - -# TODO: don't do this - look at merging the yml file in build.sh -COPY ./mkdocs.yml /docs/mkdocs.yml - -COPY ./s3_website.json /docs/s3_website.json - -# Then build everything together, ready for mkdocs -RUN /docs/build.sh +VOLUME ["/site/content"] diff --git a/docs/Makefile b/docs/Makefile index b1a72adc3f8..718c4b52be7 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,28 +1,21 @@ -.PHONY: all binary build cross default docs docs-build docs-shell shell test test-unit test-integration test-integration-cli test-docker-py validate - -# env vars passed through directly to Docker's build scripts -# to allow things like `make DOCKER_CLIENTONLY=1 binary` easily -# `docs/sources/contributing/devenvironment.md ` and `project/PACKAGERS.md` have some limited documentation of some of these -DOCKER_ENVS := \ - -e BUILDFLAGS \ - -e DOCKER_CLIENTONLY \ - -e DOCKER_EXECDRIVER \ - -e DOCKER_GRAPHDRIVER \ - -e TESTDIRS \ - -e TESTFLAGS \ - -e TIMEOUT -# note: we _cannot_ add "-e DOCKER_BUILDTAGS" here because even if it's unset in the shell, that would shadow the "ENV DOCKER_BUILDTAGS" set in our Dockerfile, which is very important for our official builds - -# to allow `make DOCSDIR=docs docs-shell` (to create a bind mount in docs) -DOCS_MOUNT := $(if $(DOCSDIR),-v $(CURDIR)/$(DOCSDIR):/$(DOCSDIR)) +.PHONY: all default docs docs-build docs-shell shell test +# to allow `make DOCSDIR=1 docs-shell` (to create a bind mount in docs) +DOCS_MOUNT := $(if $(DOCSDIR),-v $(CURDIR):/docs/content/grafana/) # to allow `make DOCSPORT=9000 docs` -DOCSPORT := 8180 +DOCSPORT := 3004 + +# Get the IP ADDRESS +DOCKER_IP=$(shell python -c "import urlparse ; print urlparse.urlparse('$(DOCKER_HOST)').hostname or ''") +HUGO_BASE_URL=$(shell test -z "$(DOCKER_IP)" && echo localhost || echo "$(DOCKER_IP)") +HUGO_BIND_IP=0.0.0.0 GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) -DOCKER_DOCS_IMAGE := grafana-docs-base$(if $(GIT_BRANCH),:$(GIT_BRANCH)) +GIT_BRANCH_CLEAN := $(shell echo $(GIT_BRANCH) | sed -e "s/[^[:alnum:]]/-/g") +DOCKER_DOCS_IMAGE := grafana/grafana-docs DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET -e NOCACHE +SOURCES_HOST_DIR := "$(shell pwd)/sources" # for some docs workarounds (see below in "docs-build" target) GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null) @@ -30,21 +23,28 @@ GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null) default: docs docs: docs-build - $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" mkdocs serve + $(DOCKER_RUN_DOCS) -p 3004:3004 -p 3005:3005 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt && grunt connect --port=3004" + +docs-watch: docs-build + $(DOCKER_RUN_DOCS) -p 3004:3004 -p 3005:3005 -v $(SOURCES_HOST_DIR):/site/content -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt --env=dev-docs && grunt connect --port=3004 & grunt watch --port=3004 --env=dev-docs" + +docs-watch-mac: docs-build + $(DOCKER_RUN_DOCS) -p 3004:3004 -p 3005:3005 -v $(SOURCES_HOST_DIR):/site/content -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt --env=dev-docs-mac && grunt connect --port=3004 & grunt watch --port=3004 --env=dev-docs-mac" + +publish: docs-build + $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh staging-docs v3.1" + +publish-prod: docs-build + $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh prod-docs root" + +docs-draft: docs-build + $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --buildDrafts="true" --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP) docs-shell: docs-build $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" bash -docs-release: docs-build - $(DOCKER_RUN_DOCS) -e OPTIONS -e BUILD_ROOT -e DISTRIBUTION_ID \ - -v $(CURDIR)/awsconfig:/docs/awsconfig \ - "$(DOCKER_DOCS_IMAGE)" ./release.sh - -docs-test: docs-build - $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh +test: docs-build + $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" docs-build: - git fetch https://github.com/grafana/grafana.git docs-2.6 && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files - echo "$(GIT_BRANCH)" > GIT_BRANCH - echo "$(GITCOMMIT)" > GITCOMMIT docker build -t "$(DOCKER_DOCS_IMAGE)" . diff --git a/docs/config.toml b/docs/config.toml new file mode 100644 index 00000000000..c8d093a81d7 --- /dev/null +++ b/docs/config.toml @@ -0,0 +1,70 @@ +baseurl = "http://localhost:3002/" +languageCode = "en-us" +title = "Grafana Docs" +canonifyurls = false +relativeURLs = false +verbose = true +enableRobotsTXT = true +disableSitemap = false +disableRSS = true + +[[menu.top]] + name = "Docs" + url = "" + weight = 1 + +[[menu.top]] + name = "Community" + url = "/community" + weight = 2 + +[[menu.top]] + name = "Support" + url = "/support" + weight = 3 + +[[menu.top]] + name = "Plugins" + url = "https://grafana.net/plugins" + weight = 4 + +[[menu.top]] + name = "Dashboards" + url = "https://grafana.net/dashboards" + weight = 5 + +[[menu.top]] + name = "Hosting" + url = "/hosting" + weight = 6 + +[[menu.top]] + name = "Github" + url = "https://github.com/grafana/grafana" + weight = 7 + +## Main +[[menu.main]] + name = "Feature Gallery" + url = "/features" + weight = 1 + +[[menu.main]] + name = "Live Demo" + url = "http://play.grafana.org" + weight = 2 + +[[menu.main]] + name = "Download" + url = "/download" + weight = 3 + +[[menu.main]] + name = "Blog" + url = "/blog" + weight = 4 + + + + + diff --git a/docs/sources/administration/index.md b/docs/sources/administration/index.md new file mode 100644 index 00000000000..334285db08b --- /dev/null +++ b/docs/sources/administration/index.md @@ -0,0 +1,11 @@ ++++ +title = "Administration" +description = "Administration" +type = "docs" +[menu.docs] +name = "Administration" +identifier = "admin" +weight = 2 ++++ + + diff --git a/docs/sources/alerting/alerting.md b/docs/sources/alerting/alerting.md index 942327c2eac..308c7881436 100644 --- a/docs/sources/alerting/alerting.md +++ b/docs/sources/alerting/alerting.md @@ -1,28 +1,34 @@ ---- -page_title: Alerting -page_description: Alerting for Grafana -page_keywords: alerting, grafana, plugins, documentation ---- - -# Alerting - -> Alerting is still in very early development. Please be aware. - -The roadmap for alerting in Grafana have been changing rapidly during last 2-3 months. So make sure you follow the disucssion in the [alerting issue](https://github.com/grafana/grafana/issues/2209). - -## Introduction - -> Alerting is turned off by default and have to be enabled in the config file. - -Grafana lets you define alert rules based on metrics queries on dashboards. Every alert is connected to a panel and when ever the query for the panel is updated the alerting rule is also updated. -So far only the graph panel supports alerting. To enable alerting for a panel go to the alerting tab and press 'Create alert' button. - -## Alert status page - -You can overview all your current alerts on the alert stats page at /alerting - -## Alert notifications - -When an alert is triggered it goes to the notification handler who takes care of sending emails or push data as webhooks. -The alert notifications can be configured on /alerting/notifications - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/sources/archive.md b/docs/sources/archive.md new file mode 100644 index 00000000000..af4ab5e7a8f --- /dev/null +++ b/docs/sources/archive.md @@ -0,0 +1,21 @@ ++++ +title = "Docs Archive" +keywords = ["grafana", "archive", "documentation", "guide"] +type = "docs" +[menu.docs] +name = "Docs Archive" +weight = 200 ++++ + +# Docs Archive + +Here you can find links to older versions of the documentation that might be better suited for your version +of Grafana. + +- [Latest](/) +- [Version 3.1](/v3.1) +- [Version 3.0](/v3.0) +- [Version 2.6](/v2.6) +- [Version 2.5](/v2.5) +- [Version 2.1](/v2.1) +- [Version 2.0](/v2.0) diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index 69338eaba40..2c2acb2ca56 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -1,16 +1,22 @@ ----- -page_title: Cloudwatch -page_description: Cloudwatch grafana datasource documentation -page_keywords: Cloudwatch, grafana, documentation, datasource, docs ---- ++++ +title = "AWS CloudWatch" +description = "Guide for using CloudWatch in Grafana" +keywords = ["grafana", "cloudwatch", "guide"] +type = "docs" +[menu.docs] +name = "AWS Cloudwatch" +identifier = "cloudwatch" +parent = "datasources" +weight = 10 ++++ -# CloudWatch +# Using AWS CloudWatch in Grafana Grafana ships with built in support for CloudWatch. You just have to add it as a data source and you will be ready to build dashboards for you CloudWatch metrics. ## Adding the data source -![](/img/cloudwatch/cloudwatch_add.png) +![](img/docs/cloudwatch/cloudwatch_add.png) 1. Open the side menu by clicking the the Grafana icon in the top header. 2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. @@ -52,7 +58,7 @@ Example content: ## Metric Query Editor -![](/img/cloudwatch/query_editor.png) +![](img/docs/cloudwatch/query_editor.png) You need to specify a namespace, metric, at least one stat, and at least one dimension. @@ -93,7 +99,7 @@ Example `ec2_instance_attribute()` query ec2_instance_attribute(us-east-1, InstanceId, { "tag:Environment": [ "production" ] }) -![](/img/v2/cloudwatch_templating.png) +![](img/docs/v2/cloudwatch_templating.png) ## Cost diff --git a/docs/sources/datasources/elasticsearch.md b/docs/sources/datasources/elasticsearch.md index e669d760985..3bc7d00e980 100644 --- a/docs/sources/datasources/elasticsearch.md +++ b/docs/sources/datasources/elasticsearch.md @@ -1,17 +1,23 @@ ----- -page_title: Elasticsearch -page_description: Elasticsearch grafana datasource documentation -page_keywords: Elasticsearch, grafana, kibana, documentation, datasource, docs ---- ++++ +title = "Using Elasticsearch in Grafana" +description = "Guide for using Elasticsearch in Grafana" +keywords = ["grafana", "elasticsearch", "guide"] +type = "docs" +[menu.docs] +name = "Elasticsearch" +parent = "datasources" +weight = 3 ++++ -# Elasticsearch +# Using Elasticsearch in Grafana Grafana ships with advanced support for Elasticsearch. You can do many types of simple or complex elasticsearch queries to visualize logs or metrics stored in elasticsearch. You can also annotate your graphs with log events stored in elasticsearch. ## Adding the data source -![](/img/v2/add_Graphite.jpg) + +![](img/docs/v2/add_Graphite.jpg) 1. Open the side menu by clicking the the Grafana icon in the top header. 2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. @@ -41,14 +47,14 @@ Elasticsearch from the browser. You do this by specifying these to options in yo ### Index settings -![](/img/elasticsearch/elasticsearch_ds_details.png) +![](img/docs/elasticsearch/elasticsearch_ds_details.png) Here you can specify a default for the `time field` and specify the name of your elasticsearch index. You can use a time pattern for the index name or a wildcard. ## Metric Query editor -![](/img/elasticsearch/query_editor.png) +![](img/docs/elasticsearch/query_editor.png) The Elasticsearch query editor allows you to select multiple metrics and group by multiple terms or filters. Use the plus and minus icons to the right to add / remove metrics or group bys. Some metrics and group by have options, click the option text to expand the the row to view and edit metric or group by options. @@ -60,7 +66,7 @@ If you have Elasticsearch 2.x and Grafana 2.6 or above then you can use pipeline to hide metrics from appearing in the graph. This is useful for metrics you only have in the query to be used in a pipeline metric. -![](/img/elasticsearch/pipeline_metrics_editor.png) +![](img/docs/elasticsearch/pipeline_metrics_editor.png) ## Templating @@ -86,7 +92,3 @@ The Elasticsearch datasource supports two types of queries you can use to fill t Use lucene format. - -## Annotations -TODO - diff --git a/docs/sources/datasources/graphite.md b/docs/sources/datasources/graphite.md index feb896c1c02..e73dd591967 100644 --- a/docs/sources/datasources/graphite.md +++ b/docs/sources/datasources/graphite.md @@ -1,17 +1,23 @@ ----- -page_title: Graphite query guide -page_description: Graphite query guide -page_keywords: grafana, graphite, metrics, query, documentation ---- ++++ +title = "Using Graphite in Grafana" +description = "Guide for using graphite in Grafana" +keywords = ["grafana", "graphite", "guide"] +type = "docs" +[menu.docs] +name = "Graphite" +identifier = "graphite" +parent = "datasources" +weight = 1 ++++ -# Graphite +# Using Graphite in Grafana Grafana has an advanced Graphite query editor that lets you quickly navigate the metric space, add functions, change function parameters and much more. The editor can handle all types of graphite queries. It can even handle complex nested queries through the use of query references. ## Adding the data source -![](/img/v2/add_Graphite.jpg) +![](img/docs/v2/add_Graphite.jpg) 1. Open the side menu by clicking the the Grafana icon in the top header. 2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. @@ -40,7 +46,7 @@ Direct access is still supported because in some cases it may be useful to acces Click the ``Select metric`` link to start navigating the metric space. One you start you can continue using the mouse or keyboard arrow keys. You can select a wildcard and still continue. -![](/img/animated_gifs/graphite_query1.gif) +![](img/docs/animated_gifs/graphite_query1.gif) ### Functions Click the plus icon to the right to add a function. You can search for the function or select it from the menu. Once @@ -48,13 +54,13 @@ a function is selected it will be added and your focus will be in the text box o a parameter just click on it and it will turn into a text box. To delete a function click the function name followed by the x icon. -![](/img/animated_gifs/graphite_query2.gif) +![](img/docs/animated_gifs/graphite_query2.gif) ### Optional parameters Some functions like aliasByNode support an optional second argument. To add this parameter specify for example 3,-2 as the first parameter and the function editor will adapt and move the -2 to a second parameter. To remove the second optional parameter just click on it and leave it blank and the editor will remove it. -![](/img/animated_gifs/func_editor_optional_params.gif) +![](img/docs/animated_gifs/func_editor_optional_params.gif) ## Point consolidation @@ -74,7 +80,7 @@ values that exists in the wildcard position. You can also create nested variables that use other variables in their definition. For example `apps.$app.servers.*` uses the variable `$app` in its query definition. -![](/img/v2/templated_variable_parameter.png) +![](img/docs/v2/templated_variable_parameter.png) ## Query Reference diff --git a/docs/sources/datasources/index.md b/docs/sources/datasources/index.md new file mode 100644 index 00000000000..e15a2720ef8 --- /dev/null +++ b/docs/sources/datasources/index.md @@ -0,0 +1,11 @@ ++++ +title = "Data Sources" +type = "docs" +[menu.docs] +name = "Data Sources" +identifier = "datasources" +parent = "features" +weight = 5 ++++ + + diff --git a/docs/sources/datasources/influxdb.md b/docs/sources/datasources/influxdb.md index c66d1ef5b69..9191ffb1201 100644 --- a/docs/sources/datasources/influxdb.md +++ b/docs/sources/datasources/influxdb.md @@ -1,15 +1,20 @@ ----- -page_title: InfluxDB query guide -page_description: InfluxDB query guide -page_keywords: grafana, influxdb, metrics, query, documentation ---- ++++ +title = "Using InfluxDB in Grafana" +description = "Guide for using InfluxDB in Grafana" +keywords = ["grafana", "influxdb", "guide"] +type = "docs" +[menu.docs] +name = "InfluxDB" +parent = "datasources" +weight = 3 ++++ -# InfluxDB +# Using InfluxDB in Grafana Grafana ships with very a feature data source plugin for InfluxDB. Supporting a feature rich query editor, annotation and templating queries. ## Adding the data source -![](/img/v2/add_Influx.jpg) +![](img/docs/v2/add_Influx.jpg) 1. Open the side menu by clicking the the Grafana icon in the top header. 2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. @@ -36,7 +41,7 @@ Password | Database user's password ## Query Editor -![](/img/influxdb/editor_v3.png) +![](assets/img/blog/v2.6/influxdb_editor_v3.gif) You find the InfluxDB editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the panel title, then edit. The editor allows you to select metrics and tags. @@ -55,7 +60,7 @@ In the `SELECT` row you can specify what fields and functions you want to use. I group by time you need an aggregation function. Some functions like derivative require an aggregation function. The editor tries simplify and unify this part of the query. For example: -![](/img/influxdb/select_editor.png) +![](img/docs/influxdb/select_editor.png) The above will generate the following InfluxDB `SELECT` clause: @@ -88,7 +93,7 @@ You can switch to raw query mode by clicking hamburger icon and then `Switch edi ### Table query / raw data -![](/img/influxdb/raw_data.png) +![](assets/img/blog/v2.6/table_influxdb_logs.png) You can remove the group by time by clicking on the `time` part and then the `x` icon. You can change the option `Format As` to `Table` if you want to show raw data in the `Table` panel. @@ -113,7 +118,7 @@ SHOW TAG VALUES WITH KEY = "hostname" WHERE region =~ /$region/ > Always you `regex values` or `regex wildcard` for All format or multi select format. -![](/img/influxdb/templating_simple_ex1.png) +![](img/docs/influxdb/templating_simple_ex1.png) ## Annotations Annotations allows you to overlay rich event information on top of graphs. diff --git a/docs/sources/datasources/kairosdb.md b/docs/sources/datasources/kairosdb.md index 2a2adf94acd..799a7e85a85 100644 --- a/docs/sources/datasources/kairosdb.md +++ b/docs/sources/datasources/kairosdb.md @@ -8,10 +8,10 @@ page_keywords: grafana, kairosdb, documentation Grafana v2.1 brings initial support for KairosDB Datasources. While the process of adding the datasource is similar to adding a Graphite or OpenTSDB datasource type, Kairos DB does have a few different options for building queries. ## Adding the data source to Grafana -![](/img/v2/add_KairosDB.jpg) +![](img/v2/add_KairosDB.jpg) -1. Open the side menu by clicking the the Grafana icon in the top header. -2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. +1. Open the side menu by clicking the the Grafana icon in the top header. +2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. > NOTE: If this link is missing in the side menu it means that your current user does not have the `Admin` role for the current organization. @@ -30,7 +30,7 @@ Access | Proxy = access via Grafana backend, Direct = access directly from brows ## Query editor Open a graph in edit mode by click the title. -![](/img/v2/kairos_query_editor.jpg) +![](img/v2/kairos_query_editor.jpg) For details on KairosDB metric queries checkout the official. - [Query Metrics - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/QueryMetrics.html). diff --git a/docs/sources/datasources/opentsdb.md b/docs/sources/datasources/opentsdb.md index b3ca5b8ea8f..4d7f8f8f6c7 100644 --- a/docs/sources/datasources/opentsdb.md +++ b/docs/sources/datasources/opentsdb.md @@ -1,14 +1,20 @@ ---- -page_title: OpenTSDB Guide -page_description: OpenTSDB guide for Grafana -page_keywords: grafana, opentsdb, documentation ---- ++++ +title = "Using OpenTSDB in Grafana" +description = "Guide for using OpenTSDB in Grafana" +keywords = ["grafana", "opentsdb", "guide"] +type = "docs" +[menu.docs] +name = "OpenTSDB" +parent = "datasources" +weight = 5 ++++ + +# Using OpenTSDB in Grafana + +{{< docs-imagebox img="img/docs/v2/add_OpenTSDB.png" max-width="14rem" >}} -# OpenTSDB Guide The newest release of Grafana adds additional functionality when using an OpenTSDB Data source. -![](/img/v2/add_OpenTSDB.png) - 1. Open the side menu by clicking the the Grafana icon in the top header. 2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. @@ -31,7 +37,7 @@ Open a graph in edit mode by click the title. Query editor will differ if the da > Note: While using Opentsdb 2.2 datasource, make sure you use either Filters or Tags as they are mutually exclusive. If used together, might give you weird results. -![](/img/v2/opentsdb_query_editor.png) +![](img/docs/v2/opentsdb_query_editor.png) ### Auto complete suggestions As soon as you start typing metric names, tag names and tag values , you should see highlighted auto complete suggestions for them. @@ -55,7 +61,7 @@ If you do not see template variables being populated in `Preview of values` sect One template variable can be used to filter tag values for another template varible. Very importantly, the order of the parameters matter in tag_values function. First parameter is the metric name, second parameter is the tag key for which you need to find tag values, and after that all other dependent template variables. Some examples are mentioned below to make nested template queries work successfully. - tag_values(cpu, hostname, env=$env) // return tag values for cpu metric, selected env tag value and tag key hostname + tag_values(cpu, hostname, env=$env) // return tag values for cpu metric, selected env tag value and tag key hostname tag_values(cpu, hostanme, env=$env, region=$region) // return tag values for cpu metric, selected env tag value, selected region tag value and tag key hostname > Note: This is required for the OpenTSDB `lookup` api to work. diff --git a/docs/sources/datasources/prometheus.md b/docs/sources/datasources/prometheus.md index 0e981c89823..8483be7d38a 100644 --- a/docs/sources/datasources/prometheus.md +++ b/docs/sources/datasources/prometheus.md @@ -1,17 +1,24 @@ ----- -page_title: Prometheus query guide -page_description: Prometheus query guide -page_keywords: grafana, prometheus, metrics, query, documentation ---- ++++ +title = "Using Prometheus in Grafana" +description = "Guide for using Prometheus in Grafana" +keywords = ["grafana", "prometheus", "guide"] +type = "docs" +[menu.docs] +name = "Prometheus" +parent = "datasources" +weight = 2 ++++ + + +# Using Prometheus in Grafana -# Prometheus Grafana includes support for Prometheus Datasources. While the process of adding the datasource is similar to adding a Graphite or OpenTSDB datasource type, Prometheus does have a few different options for building queries. ## Adding the data source to Grafana -![](/img/v2/add_Prometheus.png) +![](img/v2/add_Prometheus.png) -1. Open the side menu by clicking the the Grafana icon in the top header. -2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. +1. Open the side menu by clicking the the Grafana icon in the top header. +2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. > NOTE: If this link is missing in the side menu it means that your current user does not have the `Admin` role for the current organization. @@ -35,7 +42,7 @@ Password | Database user's password ## Query editor Open a graph in edit mode by click the title. -![](/img/v2/prometheus_editor.png) +![](img/v2/prometheus_editor.png) For details on Prometheus metric queries check out the Prometheus documentation - [Query Metrics - Prometheus documentation](http://prometheus.io/docs/querying/basics/). @@ -65,4 +72,4 @@ label_values(hostname) You can also use raw queries & regular expressions to extract anything you might need. -![](/img/v2/prometheus_templating.png) +![](img/v2/prometheus_templating.png) diff --git a/docs/sources/features/dashboard/index.md b/docs/sources/features/dashboard/index.md new file mode 100644 index 00000000000..86e15ac3390 --- /dev/null +++ b/docs/sources/features/dashboard/index.md @@ -0,0 +1,8 @@ ++++ +title = "Dashboard Features" +type = "docs" +[menu.docs] +identifier = "dashboard_features" +parent = "features" +weight = 4 ++++ diff --git a/docs/sources/features/index.md b/docs/sources/features/index.md new file mode 100644 index 00000000000..1352e9f0576 --- /dev/null +++ b/docs/sources/features/index.md @@ -0,0 +1,11 @@ ++++ +title = "Beginner Guides" +description = "Beginner guides" +type = "docs" +[menu.docs] +name = "Features" +identifier = "features" +weight = 3 ++++ + + diff --git a/docs/sources/features/panels/index.md b/docs/sources/features/panels/index.md new file mode 100644 index 00000000000..7e50d3592d0 --- /dev/null +++ b/docs/sources/features/panels/index.md @@ -0,0 +1,8 @@ ++++ +title = "Panels" +type = "docs" +[menu.docs] +parent = "features" +identifier = "panels" +weight = 3 ++++ diff --git a/docs/sources/features/whatsnew/index.md b/docs/sources/features/whatsnew/index.md new file mode 100644 index 00000000000..30357af4668 --- /dev/null +++ b/docs/sources/features/whatsnew/index.md @@ -0,0 +1,9 @@ ++++ +title = "What's New in Grafana" +[menu.docs] +name = "What's New In Grafana" +identifier = "whatsnew" +weight = 2 ++++ + + diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md index b654554fa48..1d84c4d0435 100644 --- a/docs/sources/guides/basic_concepts.md +++ b/docs/sources/guides/basic_concepts.md @@ -1,27 +1,31 @@ ----- -page_title: Graphite query guide -page_description: Graphite query guide -page_keywords: grafana, graphite, metrics, query, documentation ---- ++++ +title = "Basic Concepts" +description = "Grafana intro and concept guide" +keywords = ["grafana", "intro", "guide", "concepts"] +type = "docs" +[menu.docs] +name = "Basic Concepts" +identifier = "basic_concepts" +parent = "guides" ++++ # Basic Concepts This document is a “bottom up” introduction to basic concepts in Grafana, and can be used as a starting point to get familiar with core features. +### Data Source +Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes. -### ** Data Source ** -Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes. - -The following datasources are officially supported: [Graphite](/datasources/graphite/), [InfluxDB](/datasources/influxdb/), [OpenTSDB](/datasources/opentsdb/), and [KairosDB](/datasources/kairosdb) +The following datasources are officially supported: [Graphite](/datasources/graphite/), [InfluxDB](/datasources/influxdb/), [OpenTSDB](/datasources/opentsdb/), [Prometheus](/datasources/prometheus/), [Elasticsearch](/datasources/elasticsearch/), [CloudWatch](/datasources/cloudwatch/), and [KairosDB](/datasources/kairosdb) The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization. -### ** Organization ** +### Organization Grafana supports multiple organizations in order to support a wide variety of deployment models, including using a single Grafana instance to provide service to multiple potentially untrusted Organizations. In many cases, Grafana will be deployed with a single Organization. -Each Organization can have one or more Data Sources. +Each Organization can have one or more Data Sources. All Dashboards are owned by a particular Organization. @@ -29,20 +33,20 @@ All Dashboards are owned by a particular Organization. For more details on the user model for Grafana, please refer to [Admin](/reference/admin/) -### ** User ** -A User is a named account in Grafana. A user can belong to one or more Organizations, and can be assigned different levels of privileges through roles. +### User +A User is a named account in Grafana. A user can belong to one or more Organizations, and can be assigned different levels of privileges through roles. -Grafana supports a wide variety of internal and external ways for Users to authenticate themselves. These include from its own integrated database, from an external SQL server, or from an external LDAP server. +Grafana supports a wide variety of internal and external ways for Users to authenticate themselves. These include from its own integrated database, from an external SQL server, or from an external LDAP server. For more details please refer to [User Auth](/reference/http_api/#users) -### ** Row ** +### Row A Row is a logical divider within a Dashboard, and is used to group Panels together. Rows are always 12 “units” wide. These units are automatically scaled dependent on the horizontal resolution of your browser. You can control the relative width of Panels within a row by setting their own width. -We utilize a unit abstraction so that Grafana looks great on all screens both small and huge. +We utilize a unit abstraction so that Grafana looks great on all screens both small and huge. > Note: With MaxDataPoint functionality, Grafana can show you the perfect amount of datapoints no matter your resolution or time-range. @@ -50,7 +54,7 @@ Utilize the [Repeating Row functionality](/reference/templating/#utilizing-templ Rows can be collapsed by clicking on the Row Title. If you save a Dashboard with a Row collapsed, it will save in that state and will not preload those graphs until the row is expanded. -### ** Panel ** +### Panel The Panel is the basic visualization building block in Grafana. Each Panel provides a Query Editor (dependent on the Data Source selected in the panel) that allows you to extract the perfect visualization to display on the Panel by utilizing the Query Editor @@ -58,7 +62,7 @@ There are a wide variety of styling and formatting options that each Panel expos Panels can be dragged and dropped and rearranged on the Dashboard. They can also be resized. -There are currently four Panel types: [Graph](/reference/graph/), [Singlestat](/reference/singlestat/), [Dashlist](/reference/dashlist/), and [Text](/reference/text/). +There are currently four Panel types: [Graph](/reference/graph/), [Singlestat](/reference/singlestat/), [Dashlist](/reference/dashlist/), [Table](/reference/table_panel/),and [Text](/reference/text/). Panels like the [Graph](/reference/graph/) panel allow you to graph as many metrics and series as you want. Other panels like [Singlestat](/reference/singlestat/) require a reduction of a single query into a single number. [Dashlist](/reference/dashlist/) and [Text](/reference/text/) are special panels that do not connect to any Data Source. @@ -66,14 +70,14 @@ Panels can be made more dynamic by utilizing [Dashboard Templating](/reference/t Utilize the [Repeating Panel](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) selected. -The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-override). +The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-overrides-timeshift). Panels (or an entire Dashboard) can be [Shared](/reference/sharing/) easily in a variety of ways. You can send a link to someone who has a login to your Grafana. You can use the [Snapshot](/reference/sharing/#snapshots) feature to encode all the data currently being viewed into a static and interactive JSON document; it's so much better than emailing a screenshot! -### ** Query Editor ** +### Query Editor -The Query Editor exposes capabilities of your Data Source and allows you to query the metrics that it contains. +The Query Editor exposes capabilities of your Data Source and allows you to query the metrics that it contains. Use the Query Editor to build one or more queries (for one or more series) in your time series database. The panel will instantly update allowing you to effectively explore your data in real time and build a perfect query for that particular Panel. @@ -81,13 +85,13 @@ You can utilize [Template variables](/reference/templating/) in the Query Editor Grafana allows you to reference queries in the Query Editor by the row that they’re on. If you add a second query to graph, you can reference the first query simply by typing in #A. This provides an easy and convenient way to build compounded queries. -### ** Dashboard ** +### Dashboard The Dashboard is where it all comes together. Dashboards can be thought of as of a set of one or more Panels organized and arranged into one or more Rows. The time period for the Dashboard can be controlled by the [Dashboard time picker](/reference/timerange/) in the upper right of the Dashboard. -Dashboards can utilize [Templating](/reference/templating/) to make them more dynamic and interactive. +Dashboards can utilize [Templating](/reference/templating/) to make them more dynamic and interactive. Dashboards can utilize [Annotations](/reference/annotations/) to display event data across Panels. This can help correlate the time series data in the Panel with other events. diff --git a/docs/sources/guides/gettingstarted.md b/docs/sources/guides/getting_started.md similarity index 88% rename from docs/sources/guides/gettingstarted.md rename to docs/sources/guides/getting_started.md index 004b1fcdadb..f91ed1aae7c 100644 --- a/docs/sources/guides/gettingstarted.md +++ b/docs/sources/guides/getting_started.md @@ -1,8 +1,13 @@ ---- -page_title: Getting started -page_description: Getting started -page_keywords: grafana, guide, documentation ---- ++++ +title = "Getting Started" +description = "Getting started with Grafana guide" +keywords = ["grafana", "intro", "guide", "started"] +type = "docs" +[menu.docs] +name = "Getting Started" +identifier = "getting_started_guide" +parent = "guides" ++++ # Getting started This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running and have added at least one [Data Source](/datasources/overview). @@ -10,14 +15,14 @@ This guide will help you get started and acquainted with Grafana. It assumes you ## Beginner guides Watch the 10min [beginners guide to building dashboards](https://www.youtube.com/watch?v=sKNZMtoSHN4&index=7&list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2) to get a quick intro to setting up Dashboards and Panels. -##Basic Concepts +## Basic Concepts Read the [Basic Concepts](/guides/basic_concepts) document to get a crash course in key Grafana concepts. ### Top header Let's start with creating a new Dashboard. You can find the new Dashboard link at the bottom of the Dashboard picker. You now have a blank Dashboard. - + The image above shows you the top header for a Dashboard. @@ -29,12 +34,12 @@ The image above shows you the top header for a Dashboard. 6. Settings: Manage Dashboard settings and features such as Templating and Annotations. ## Dashboards, Panels, Rows, the building blocks of Grafana... -Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). The [Core Concepts](/guides/basic_concepts) guide explores these key ideas in detail. +Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail. ## Adding & Editing Graphs and Panels -![](/img/v2/graph_metrics_tab_graphite.png) +![](img/docs/v2/graph_metrics_tab_graphite.png) 1. You add panels via row menu. The row menu is the green icon to the left of each row. 2. To edit the graph you click on the graph title to open the panel menu, then `Edit`. @@ -43,8 +48,7 @@ Dashboards are at the core of what Grafana is all about. Dashboards are composed When you click the `Metrics` tab, you are presented with a Query Editor that is specific to the Panel Data Source. Use the Query Editor to build your queries and Grafana will visualize them in real time. - - + 1. Zoom out time range 2. Time picker dropdown. Here you can access relative time range options, auto refresh options and set custom absolute time ranges. @@ -57,7 +61,7 @@ When you click the `Metrics` tab, you are presented with a Query Editor that is You can Drag-and-Drop Panels within and between Rows. Click and hold the Panel title, and drag it to its new location. You can also easily resize panels by clicking the (-) and (+) icons. -![](/img/animated_gifs/drag_drop.gif) +![](img/docs/animated_gifs/drag_drop.gif) ## Tips and shortcuts diff --git a/docs/sources/guides/index.md b/docs/sources/guides/index.md new file mode 100644 index 00000000000..c80dd624624 --- /dev/null +++ b/docs/sources/guides/index.md @@ -0,0 +1,9 @@ ++++ +title = "Guides" +type = "docs" +[menu.docs] +name = "Getting Started" +identifier = "guides" +weight = 2 ++++ + diff --git a/docs/sources/guides/screencasts.md b/docs/sources/guides/screencasts.md deleted file mode 100644 index 500ca9f9b6a..00000000000 --- a/docs/sources/guides/screencasts.md +++ /dev/null @@ -1,69 +0,0 @@ -page_title: Screencasts -page_description: Grafana screencasts -page_keywords: grafana, screencasts, documentation, guides -no_toc: true - -# Screencasts - -
-
-

Episode 1 - Building Graphite Queries

- Learn how the Graphite Query Editor works, and how to use different graphing functions. There's also an introduction to graph display settings. -
- -
-
-
-

Episode 2 - Templated Graphite Queries

- The screencast focuses on Templating with the Graphite Data Source. Learn how to make dynamic and adaptable Dashboards for your Graphite metrics. -
- -
-
-
-
-
-
-

Episode 3 - Whats New In Grafana 2.0

- This screencast highlights many of the great new features that were included in the Grafana 2.0 release. -
- -
-
-
-

Episode 4 - Installation & Configuration on Ubuntu / Debian

- Learn how to easily install the dependencies and packages to get Grafana 2.0 up and running on Ubuntu or Debian in just a few minutes. -
- -
-
-
-
-
-
-

Episode 5 - Installation & Configuration on Red Hat / CentOS

- This screencasts shows how to get Grafana 2.0 installed and configured quickly on RPM-based Linux operating systems. -
- -
-
-
-

Episode 6 - Adding data sources, users & organizations

- Now that Grafana has been installed, learn about adding data sources and get a closer look at adding and managing Users and Organizations. -
- -
-
-
-
-
-
-

Episode 7 - Beginners guide to building dashboards

- For newer users of Grafana, this screencast will familiarize you with the general UI and teach you how to build your first Dashboard. -
- -
-
-
-
-
diff --git a/docs/sources/guides/whats-new-in-v2-1.md b/docs/sources/guides/whats-new-in-v2-1.md index 10519cb0422..efe35fd387a 100644 --- a/docs/sources/guides/whats-new-in-v2-1.md +++ b/docs/sources/guides/whats-new-in-v2-1.md @@ -1,8 +1,14 @@ ---- -page_title: What's New in Grafana v2.1 -page_description: What's new in Grafana v2.1 -page_keywords: grafana, new, changes, features, documentation ---- ++++ +title = "What's New in Grafana v2.1" +description = "Feature & improvement highlights for Grafana v2.1" +keywords = ["grafana", "new", "documentation", "2.1"] +type = "docs" +[menu.docs] +name = "Version 2.1" +identifier = "v2.1" +parent = "whatsnew" +weight = 10 ++++ # What's new in Grafana v2.1 Grafana 2.1 brings improvements in three core areas: dashboarding, authentication, and data sources. @@ -17,7 +23,7 @@ A template variable with Multi-Value enabled allows for the selection of multipl These variables can then be used in any Panel to make them more dynamic, and to give you the perfect view of your data. Multi-Value variables is also enabling the new `row repeat` and `panel repeat` feature described below. -![Multi-Value Select](/img/v2/multi-select.gif "Multi-Value Select") +![Multi-Value Select](img/docs/v2/multi-select.gif "Multi-Value Select")

### Repeating Rows and Panels @@ -25,7 +31,7 @@ It’s now possible to create a dashboard that automatically adds (or removes) b on selected variable values. Any row or any panel can be configured to repeat (duplicate itself) based on a multi-value template variable.

-![Repeating Rows and Panels](/img/v2/panel-row-repeat.gif "Repeating Rows and Panels") +![Repeating Rows and Panels](img/docs/v2/panel-row-repeat.gif "Repeating Rows and Panels")

### Dashboard Links & Navigation @@ -33,7 +39,7 @@ To support better navigation between dashboards, it's now possible to create cus panels to appropriate Dashboards. You also have the ability to create flexible top-level links on any given dashboard thanks to the new dashboard navigation bar feature. -![Dashboard Links](/img/v2/dash_links.png "Dashboard Links") +![Dashboard Links](img/docs/v2/dash_links.png "Dashboard Links") Dashboard links can be added under dashboard settings. Either defined as static URLs with a custom icon or as dynamic dashboard links or dropdowns based on custom dashboard search query. These links appear in the same @@ -82,7 +88,7 @@ The Viewer role has been modified in Grafana 2.1 so that users assigned this rol Grafana 2.1 now comes with full support for InfluxDB 0.9. There is a new query editor designed from scratch for the new features InfluxDB 0.9 enables. -![InfluxDB Editor](/img/v2/influx_09_editor_anim.gif "InfluxDB Editor") +![InfluxDB Editor](img/docs/v2/influx_09_editor_anim.gif "InfluxDB Editor")
@@ -110,15 +116,15 @@ Define series color using regex rule. This is useful when you have templated gra that change depending selected template variables. Using a regex style override rule you could for example make all series that contain the word **CPU** `red` and assigned to the second y axis. -![Define series color using regex rule](/img/v2/regex_color_override.png "Define series color using regex rule") +![Define series color using regex rule](img/docs/v2/regex_color_override.png "Define series color using regex rule") New series style override, negative-y transform and stack groups. Negative y transform is very useful if you want to plot a series on the negative y scale without affecting the legend values like min or max or the values shown in the hover tooltip. -![Negative-y Transform](/img/v2/negative-y.png "Negative-y Transform") +![Negative-y Transform](img/docs/v2/negative-y.png "Negative-y Transform") -![Negative-y Transform](/img/v2/negative-y-form.png "Negative-y Transform") +![Negative-y Transform](img/docs/v2/negative-y-form.png "Negative-y Transform") ### Singlestat Panel Now support string values. Useful for time series database like InfluxDB that supports diff --git a/docs/sources/guides/whats-new-in-v2-5.md b/docs/sources/guides/whats-new-in-v2-5.md index f02d0d43fd0..ea9666f9e90 100644 --- a/docs/sources/guides/whats-new-in-v2-5.md +++ b/docs/sources/guides/whats-new-in-v2-5.md @@ -1,8 +1,14 @@ ---- -page_title: What's New in Grafana v2.5 -page_description: What's new in Grafana v2.5 -page_keywords: grafana, new, changes, features, documentation ---- ++++ +title = "What's New in Grafana v2.5" +description = "Feature & improvement highlights for Grafana v2.5" +keywords = ["grafana", "new", "documentation", "2.5"] +type = "docs" +[menu.docs] +name = "Version 2.5" +identifier = "v2.5" +parent = "whatsnew" +weight = 9 ++++ # What's new in Grafana v2.5 @@ -12,7 +18,7 @@ fixes and enhancements to all areas of Grafana, like new Data Sources, a new and resize handles and improved InfluxDB and OpenTSDB support. ### New time range controls -New Time picker +New Time picker A new timepicker with room for more quick ranges as well as new types of relative ranges, like `Today`, `The day so far` and `This day last week`. Also an improved time & calendar picker that now works @@ -20,7 +26,7 @@ correctly in UTC mode. ### Elasticsearch -Elasticsearch example +Elasticsearch example
This release brings a fully featured query editor for Elasticsearch. You will now be able to visualize @@ -40,7 +46,7 @@ Try the new Elasticsearch query editor on the [play.grafana.org](http://play.gra ### CloudWatch -Cloudwatch editor +Cloudwatch editor Grafana 2.5 ships with a new CloudWatch datasource that will allow you to query and visualize CloudWatch metrics directly from Grafana. @@ -51,14 +57,14 @@ metrics directly from Grafana. ### Prometheus -Prometheus editor +Prometheus editor Grafana 2.5 ships with a new Prometheus datasource that will allow you to query and visualize data stored in Prometheus. ### Mix different data sources -Mix data sources in the same dashboard or in the same graph! +Mix data sources in the same dashboard or in the same graph! In previous releases you have been able to mix different data sources on the same dashboard. In v2.5 you will be able to mix then on the same graph! You can enable this by selecting the built in `-- Mixed --` data source. @@ -67,12 +73,12 @@ to plot metrics from different Graphite servers on the same Graph or plot data f data from Prometheus. Mixing different data sources on the same graph works for any data source, even custom ones. ### Panel Resize handles - + This release adds resize handles to the the bottom right corners of panels making is easy to resize both width and height. ### User invites - + This version also brings some new features for user management. diff --git a/docs/sources/guides/whats-new-in-v2-6.md b/docs/sources/guides/whats-new-in-v2-6.md index ac644ad44af..281c2b5e4be 100644 --- a/docs/sources/guides/whats-new-in-v2-6.md +++ b/docs/sources/guides/whats-new-in-v2-6.md @@ -1,8 +1,14 @@ ---- -page_title: What's New in Grafana v2.6 -page_description: What's new in Grafana v2.6 -page_keywords: grafana, new, changes, features, documentation, table ---- ++++ +title = "What's New in Grafana v2.6" +description = "Feature & improvement highlights for Grafana v2.6" +keywords = ["grafana", "new", "documentation", "2.6"] +type = "docs" +[menu.docs] +name = "Version 2.6" +identifier = "v2.6" +parent = "whatsnew" +weight = 7 ++++ # What's new in Grafana v2.6 @@ -11,7 +17,7 @@ The release includes a new Table panel, a new InfluxDB query editor, support for support for multiple Cloudwatch credentials. ## Table Panel - + The new table panel is very flexible, supporting both multiple modes for time series as well as for table, annotation and raw JSON data. It also provides date formating and value formating and coloring options. @@ -21,7 +27,7 @@ table, annotation and raw JSON data. It also provides date formating and value f In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series. - + ### Table Transform Above you see the options tab for the **Table Panel**. The most important option is the `To Table Transform`. @@ -34,7 +40,7 @@ The column styles allow you control how dates and numbers are formatted. This transform allows you to take multiple time series and group them by time. Which will result in a `Time` column and a column for each time series. - + In the screenshot above you can see how the same time series query as in the previous example can be transformed into a different table by changing the `To Table Transform` to `Time series to columns`. @@ -43,7 +49,7 @@ a different table by changing the `To Table Transform` to `Time series to colum This transform works very similar to the legend values in the Graph panel. Each series gets its own row. In the Options tab you can select which aggregations you want using the plus button the Columns section. - + You have to think about how accurate the aggregations will be. It depends on what aggregation is used in the time series query, how many data points are fetched, etc. The time series aggregations are calculated by Grafana after aggregation is performed @@ -53,38 +59,39 @@ by the time series database. If you want to show documents from Elasticsearch pick `Raw Document` as the first metric. - + This in combination with the `JSON Data` table transform will allow you to pick which fields in the document you want to show in the table. - + ### Elasticsearch aggregations You can also make Elasticsearch aggregation queries without a `Date Histogram`. This allows you to use Elasticsearch metric aggregations to get accurate aggregations for the selected time range. - + ### Annotations The table can also show any annotations you have enabled in the dashboard. - + ## The New InfluxDB Editor The new InfluxDB editor is a lot more flexible and powerful. It supports nested functions, like `derivative`. It also uses the same technique as the Graphite query editor in that it presents nested functions as chain of function transformations. It tries to simplify and unify the complicated nature of InfluxDB's query language. - + In the `SELECT` row you can specify what fields and functions you want to use. If you have a group by time you need an aggregation function. Some functions like derivative require an aggregation function. The editor tries simplify and unify this part of the query. For example: -![](/img/influxdb/select_editor.png) + +![](img/docs/influxdb/select_editor.png) The above will generate the following InfluxDB `SELECT` clause: @@ -103,7 +110,7 @@ You can remove the group by by clicking on the `tag` and then click on the x ico The new editor also allows you to remove group by time and select `raw` table data. Which is very useful in combination with the new Table panel to show raw log data stored in InfluxDB. - + ## Pipeline metrics @@ -111,7 +118,7 @@ If you have Elasticsearch 2.x and Grafana 2.6 or above then you can use pipeline **Moving Average** and **Derivative**. Elasticsearch pipeline metrics require another metric to be based on. Use the eye icon next to the metric to hide metrics from appearing in the graph. -![](/img/elasticsearch/pipeline_metrics_editor.png) +![](/img/docs/elasticsearch/pipeline_metrics_editor.png) ## Changelog For a detailed list and link to github issues for everything included in the 2.6 release please diff --git a/docs/sources/guides/whats-new-in-v2.md b/docs/sources/guides/whats-new-in-v2.md index 9e87ab55e6e..8ad24e70fd1 100644 --- a/docs/sources/guides/whats-new-in-v2.md +++ b/docs/sources/guides/whats-new-in-v2.md @@ -1,8 +1,14 @@ ---- -page_title: What's New in Grafana v2.0 -page_description: What's new in Grafana v2.0 -page_keywords: grafana, new, changes, features, documentation ---- ++++ +title = "What's New in Grafana v2.0" +description = "Feature & improvement highlights for Grafana v2.0" +keywords = ["grafana", "new", "documentation", "2.0"] +type = "docs" +[menu.docs] +name = "Version 2.0" +identifier = "v2.0" +parent = "whatsnew" +weight = 11 ++++ # What's New in Grafana v2.0 @@ -49,7 +55,7 @@ even zoom in). Also they are fast to load as they aren't actually connected to a They're a great way to communicate about a particular incident with specific people who aren't Users of your Grafana instance. You can also use them to show off your dashboards over the Internet. -![](/img/v2/dashboard_snapshot_dialog.png) +![](img/docs/v2/dashboard_snapshot_dialog.png) ### Publish snapshots @@ -61,11 +67,11 @@ Either way, anyone with the link (and access to your Grafana instance for local In Grafana v2.x you can now override the relative time range for individual panels, causing them to be different than what is selected in the Dashboard time picker in the upper right. You can also add a time shift to individual panels. This allows you to show metrics from different time periods or days at the same time. -![](/img/v2/panel_time_override.jpg) +![](img/docs/v2/panel_time_override.jpg) You control these overrides in panel editor mode and the new tab `Time Range`. -![](/img/v2/time_range_tab.jpg) +![](img/docs/v2/time_range_tab.jpg) When you zoom or change the Dashboard time to a custom absolute time range, all panel overrides will be disabled. The panel relative time override is only active when the dashboard time is also relative. The panel timeshift override however is always active, even when the dashboard time is absolute. @@ -90,7 +96,7 @@ This feature makes it easy to include interactive visualizations from your Grafa The top header has gotten a major streamlining in Grafana V2.0. - + 1. `Side menubar toggle` Toggle the side menubar on or off. This allows you to focus on the data presented on the Dashboard. The side menubar provides access to features unrelated to a Dashboard such as Users, Organizations, and Data Sources. 2. `Dashboard dropdown` The main dropdown shows you which Dashboard you are currently viewing, and allows you to easily switch to a new Dashboard. From here you can also create a new Dashboard, Import existing Dashboards, and manage the Playlist. @@ -115,7 +121,7 @@ You can easily collapse or re-open the side menubar at any time by clicking the ## New search view & starring dashboards -![](/img/v2/dashboard_search.jpg) +![](img/docs/v2/dashboard_search.jpg) The dashboard search view has gotten a big overhaul. You can now see and filter by which dashboard you have personally starred. @@ -124,11 +130,11 @@ The dashboard search view has gotten a big overhaul. You can now see and filter The Graph panel now supports 3 logarithmic scales, `log base 10`, `log base 32`, `log base 1024`. Logarithmic y-axis scales are very useful when rendering many series of different order of magnitude on the same scale (eg. latency, network traffic, and storage) -![](/img/v2/graph_logbase10_ms.png) +![](img/docs/v2/graph_logbase10_ms.png) ## Dashlist panel -![](/img/v2/dashlist_starred.png) +![](img/docs/v2/dashlist_starred.png) The dashlist is a new panel in Grafana v2.0. It allows you to show your personal starred dashboards, as well as do custom searches based on search strings or tags. @@ -147,7 +153,7 @@ In addition, connections to Data Sources can be better controlled and secured, a A commonly reported problem has been graphs dipping to zero at the the end, because metric data for the last interval has yet to be written to the Data Source. These graphs then "self correct" once the data comes in, but can look deceiving or alarming at times. You can avoid this problem by adding a `now delay` in `Dashboard Settings` > `Time Picker` tab. This new feature will cause Grafana to ignore the most recent data up to the set delay. -![](/img/v2/timepicker_now_delay.jpg) +![](img/docs/v2/timepicker_now_delay.jpg) The delay that may be necessary depends on how much latency you have in your collection pipeline. @@ -155,7 +161,7 @@ The delay that may be necessary depends on how much latency you have in your col Grafana v2.0 protects Users from accidentally overwriting each others Dashboard changes. Similar protections are in place if you try to create a new Dashboard with the same name as an existing one. -![](/img/v2/overwrite_protection.jpg) +![](img/docs/v2/overwrite_protection.jpg) These protections are only the first step; we will be building out additional capabilities around dashboard versioning and management in future versions of Grafana. @@ -171,6 +177,6 @@ Grafana now supports server-side PNG rendering. From the Panel share dialog you > **Note** This requires that your Data Source is accessible from your Grafana instance. -![](/img/v2/share_dialog_image_highlight.jpg) +![](img/docs/v2/share_dialog_image_highlight.jpg) diff --git a/docs/sources/guides/whats-new-in-v3-1.md b/docs/sources/guides/whats-new-in-v3-1.md index 9613cc1682c..65b5f8e4882 100644 --- a/docs/sources/guides/whats-new-in-v3-1.md +++ b/docs/sources/guides/whats-new-in-v3-1.md @@ -1,8 +1,14 @@ ---- -page_title: What's New in Grafana v3.1 -page_description: What's new in Grafana v3.1 -page_keywords: grafana, new, changes, features, documentation ---- ++++ +title = "What's New in Grafana v3.1" +description = "Feature & improvement highlights for Grafana v3.1" +keywords = ["grafana", "new", "documentation", "3.1"] +type = "docs" +[menu.docs] +name = "Version 3.1 (Latest)" +identifier = "v3.1" +parent = "whatsnew" +weight = 1 ++++ # What's New in Grafana v3.1 @@ -10,21 +16,21 @@ page_keywords: grafana, new, changes, features, documentation The export feature is now accessed from the share menu. - + Dashboards exported from Grafana 3.1 are now more portable and easier for others to import than before. The export process extracts information data source types used by panels and adds these to a new `inputs` section in the dashboard json. So when you or another person tries to import the dashboard they will be asked to select data source and optional metrix prefix options. - + The above screenshot shows the new import modal that gives you 3 options for how to import a dashboard. One notable new addition here is the ability to import directly from Dashboards shared on [Grafana.net](https://grafana.net). The next step in the import process: - + Here you can change the name of the dashboard and also pick what data sources you want the dashboard to use. The above screenshot shows a CollectD dashboard for Graphite that requires a metric prefix be specified. @@ -35,7 +41,7 @@ On [Grafana.net](https://grafana.net) you can now browse & search for dashboards more are being uploaded every day. To import a dashboard just copy the dashboard url and head back to Grafana, then Dashboard Search -> Import -> Paste Grafana.net Dashboard URL. - + ## Constant template variables @@ -63,6 +69,5 @@ Its now possible to configure different log levels for different modules. ## CHANGELOG For a detailed list and link to github issues for everything included -in the 3.1 release please view the -[CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) +in the 3.1 release please view the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file. diff --git a/docs/sources/guides/whats-new-in-v3.md b/docs/sources/guides/whats-new-in-v3.md index 051691e0384..f86c7133676 100644 --- a/docs/sources/guides/whats-new-in-v3.md +++ b/docs/sources/guides/whats-new-in-v3.md @@ -1,8 +1,14 @@ ---- -page_title: What's New in Grafana v3.0 -page_description: What's new in Grafana v3.0 -page_keywords: grafana, new, changes, features, documentation ---- ++++ +title = "What's New in Grafana v3.0" +description = "Feature & improvement highlights for Grafana v3.0" +keywords = ["grafana", "new", "documentation", "3.0"] +type = "docs" +[menu.docs] +name = "Version 3.0" +identifier = "v3.0" +parent = "whatsnew" +weight = 2 ++++ # What's New in Grafana v3.0 @@ -37,7 +43,7 @@ entire experience right within Grafana. ## Grafana.net - + [Grafana.net](https://grafana.net) offers a central repository where the community can come together to discover, create and share plugins (data sources, panels, apps) and dashboards. @@ -96,7 +102,7 @@ periodically and remotely. You can also make Playlists dynamic by using Dashboard **tags** to define the Playlist. - + ## Improved UI @@ -116,11 +122,11 @@ are literally hundreds of UI improvements and refinements. Here’s the new side menu in action: - + And here's the new look for Dashboard settings: - + Check out the Play Site to get a feel for some of the UI changes. @@ -132,7 +138,7 @@ over the link and click the annotation text. This feature is very useful for linking to particular commits or tickets where more detailed information can be presented to the user. - + ## Data source variables @@ -140,11 +146,11 @@ This has been a top requested feature for very long we are exited to finally pro this feature. You can now add a new `Data source` type variable. That will automatically be filled with instance names of your data sources. - + You can then use this variable as the panel data source: - + This will allow you to quickly change data source server and reuse the same dashboard for different instances of your metrics backend. For example @@ -162,7 +168,7 @@ The Prometheus Data Source now supports annotations. ### InfluxDB You can now select the InfluxDB policy from the query editor. - + Grafana 3.0 also comes with support for InfluxDB 0.11 and InfluxDB 0.12. @@ -195,23 +201,23 @@ are a couple that I incurage you try! #### [Clock Panel](https://grafana.net/plugins/grafana-clock-panel) Support's both current time and count down mode. - + #### [Pie Chart Panel](https://grafana.net/plugins/grafana-piechart-panel) A simple pie chart panel is now available as an external plugin. - + #### [WorldPing App](https://grafana.net/plugins/raintank-worldping-app) This is full blown Grafana App that adds new panels, data sources and pages to give feature rich global performance monitoring directly from your on-prem Grafana. - + #### [Zabbix App](https://grafana.net/plugins/alexanderzobnin-zabbix-app) This app contains the already very pouplar Zabbix data source plugin, 2 dashboards and a triggers panel. It is created and maintained by [Alexander Zobnin](https://github.com/alexanderzobnin/grafana-zabbix). - + Checkout the full list of plugins on [Grafana.net](https://grafana.net/plugins) diff --git a/docs/sources/http_api/auth.md b/docs/sources/http_api/auth.md index df874d1653e..503791b4879 100644 --- a/docs/sources/http_api/auth.md +++ b/docs/sources/http_api/auth.md @@ -25,7 +25,7 @@ curl example: Open the sidemenu and click the organization dropdown and select the `API Keys` option. -![](/img/v2/orgdropdown_api_keys.png) +![](img/v2/orgdropdown_api_keys.png) You use the token in all requests in the `Authorization` header, like this: diff --git a/docs/sources/http_api/index.md b/docs/sources/http_api/index.md new file mode 100644 index 00000000000..a083b150469 --- /dev/null +++ b/docs/sources/http_api/index.md @@ -0,0 +1,7 @@ ++++ +title = "HTTP API" +[menu.docs] +name = "HTTP API" +identifier = "http_api" +weight = 9 ++++ diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md index aa870031ca2..734fbb4f934 100644 --- a/docs/sources/http_api/user.md +++ b/docs/sources/http_api/user.md @@ -1,10 +1,15 @@ ----- -page_title: User API -page_description: Grafana User API Reference -page_keywords: grafana, admin, http, api, documentation, user ---- ++++ +title = "User HTTP API " +description = "Grafana User HTTP API" +keywords = ["grafana", "http", "documentation", "api", "user"] +aliases = ["/http_api/user/"] +type = "docs" +[menu.docs] +name = "Users" +parent = "http_api" ++++ -# User API +# User HTTP resources / actions ## Search Users diff --git a/docs/sources/index.md b/docs/sources/index.md index 746f2942be7..a2b19fe75da 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -1,37 +1,50 @@ ---- -page_title: Grafana Installation -page_description: Install guide for Grafana -page_keywords: grafana, installation, documentation ---- ++++ +title = "Grafana Installation" +description = "Install guide for Grafana" +keywords = ["grafana", "installation", "documentation"] +type = "docs_root" +[menu.docs] +name = "Welcome to the Docs" +identifier = "root" +weight = -1 ++++ -# Installation +# Welcome to the Grafana Documentation -Grafana is easily installed via a Debian/Ubuntu package (.deb), via -Redhat/Centos package (.rpm) or manually via a tarball that contains all -required files and binaries. If you can't find a package or binary for -your platform, you might be able to build one yourself. Read the [build -from source](../project/building_from_source) instructions for more -information. +Grafana is an open source metric analytics & visualization suite. It is most commonly used for +visualizing time series data for infrastructure and application analytics but many use it in +other domains including industrial sensors, home automation, weather, and process control. -## Platforms -- [Installing on Debian / Ubuntu](installation/debian.md) -- [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](installation/rpm.md) -- [Installing on Mac OS X](installation/mac.md) -- [Installing on Windows](installation/windows.md) -- [Installing on Docker](installation/docker.md) -- [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](installation/provisioning.md) -- [Nightly Builds](http://grafana.org/download/builds.html) +## Installing Grafana +- [Installing on Debian / Ubuntu](installation/debian) +- [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](installation/rpm) +- [Installing on Mac OS X](installation/mac) +- [Installing on Windows](installation/windows) +- [Installing on Docker](installation/docker) +- [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](installation/provisioning) +- [Nightly Builds](http://grafana.org/builds) -## Configuration +For other platforms Read the [build from source]({{< relref "project/building_from_source.md" >}}) +instructions for more information. + +## Configuring Grafana The back-end web server has a number of configuration options. Go the [Configuration](/installation/configuration) page for details on all those options. + +## Getting started + +- [Getting Started](guides/getting_started) +- [Basic Concepts](guides/basic_concepts) +- [Screencasts](tutorials/screencasts) + ## Data sources guides -- [Graphite](datasources/graphite.md) -- [Elasticsearch](datasources/elasticsearch.md) -- [InfluxDB](datasources/influxdb.md) -- [OpenTSDB](datasources/opentsdb.md) +- [Graphite](datasources/graphite) +- [Elasticsearch](datasources/elasticsearch) +- [InfluxDB](datasources/influxdb) +- [OpenTSDB](datasources/opentsdb) + diff --git a/docs/sources/installation/behind_proxy.md b/docs/sources/installation/behind_proxy.md index 0cf2507b404..46b70b5dc85 100644 --- a/docs/sources/installation/behind_proxy.md +++ b/docs/sources/installation/behind_proxy.md @@ -1,21 +1,27 @@ ---- -page_title: Running Grafana behind a reverse proxy -page_description: Guide for running Grafana behind a reverse proxy -page_keywords: Grafana, reverse proxy, nginx, haproxy ---- ++++ +title = "Running Grafana behind a reverse proxy" +description = "Guide for running Grafana behind a reverse proxy" +keywords = ["grafana", "nginx", "documentation", "haproxy", "reverse"] +type = "docs" +[menu.docs] +name = "Running Grafana behind a reverse proxy" +parent = "tutorials" +weight = 1 ++++ + # Running Grafana behind a reverse proxy -It should be straight forward to get Grafana up and running behind a reverse proxy. But here are some things that you might run into. +It should be straight forward to get Grafana up and running behind a reverse proxy. But here are some things that you might run into. -Links and redirects will not be rendered correctly unless you set the server.domain setting. +Links and redirects will not be rendered correctly unless you set the server.domain setting. ``` [server] domain = foo.bar ``` -To use sub *path* ex `http://foo.bar/grafana` make sure to include `/grafana` in the end of root_url. -Otherwise Grafana will not behave correctly. See example below. +To use sub *path* ex `http://foo.bar/grafana` make sure to include `/grafana` in the end of root_url. +Otherwise Grafana will not behave correctly. See example below. # Examples Here are some example configurations for running Grafana behind a reverse proxy. @@ -26,7 +32,7 @@ Here are some example configurations for running Grafana behind a reverse proxy. domain = foo.bar ``` -## Nginx configuration +## Nginx configuration ``` server { listen 80; diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 9a7a480f482..8ae9d4cd18c 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -1,8 +1,14 @@ ---- -page_title: Configuration -page_description: Configuration guide for Grafana. -page_keywords: grafana, configuration, documentation ---- ++++ +title = "Configuration" +description = "Configuration Docs" +keywords = ["grafana", "configuration", "documentation"] +type = "docs" +[menu.docs] +name = "Configuration" +identifier = "config" +parent = "admin" +weight = 1 ++++ # Configuration @@ -30,6 +36,9 @@ using environment variables using the syntax: Where the section name is the text within the brackets. Everything should be upper case, `.` should be replaced by `_`. For example, given these configuration settings: + # default section + instance_name = ${HOSTNAME} + [security] admin_user = admin @@ -39,6 +48,7 @@ should be upper case, `.` should be replaced by `_`. For example, given these co Then you can override them using: + export GF_DEFAULT_INSTANCE_NAME=my-instance export GF_SECURITY_ADMIN_USER=true export GF_AUTH_GOOGLE_CLIENT_SECRET=newS3cretKey @@ -382,9 +392,12 @@ browser to access Grafana, but with the prefix path of `/login/generic_oauth`. scopes = auth_url = token_url = + api_url = allowed_domains = mycompany.com mycompany.org allow_sign_up = false +Set api_url to the resource that returns basic user info. +
## [auth.basic] @@ -528,7 +541,7 @@ Use space to separate multiple modes, e.g. "console file" ### level Either "debug", "info", "warn", "error", "critical", default is "info" -### filter +### filters optional settings to set different levels for specific loggers. Ex `filters = sqlstore:debug` diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 7498cab9c48..2c37ef4d14f 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -1,13 +1,17 @@ ---- -page_title: Installing on Debian / Ubuntu -page_description: Grafana Installation guide for Debian / Ubuntu. -page_keywords: grafana, installation, debian, ubuntu, guide ---- ++++ +title = "Installing on Debian / Ubuntu" +description = "Install guide for Grafana" +keywords = ["grafana", "installation", "documentation"] +type = "docs" +[menu.docs] +name = "Installing on Ubuntu / Debian" +identifier = "debian" +parent = "installation" +weight = 1 ++++ # Installing on Debian / Ubuntu -## Download - Description | Download ------------ | ------------- Stable .deb for Debian-based Linux | [3.1.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_3.1.1-1470047149_amd64.deb) @@ -97,23 +101,24 @@ By default Grafana will log to `/var/log/grafana` The default configuration specifies a sqlite3 database located at `/var/lib/grafana/grafana.db`. Please backup this database before -upgrades. You can also use MySQL or Postgres as the Grafana database, as detailed on [the configuration page](configuration.md#database). +upgrades. You can also use MySQL or Postgres as the Grafana database, as detailed on [the configuration page]({{< relref "configuration.md#database" >}}). ## Configuration The configuration file is located at `/etc/grafana/grafana.ini`. Go the -[Configuration](/installation/configuration) page for details on all +[Configuration]({{< relref "configuration.md" >}}) page for details on all those options. ### Adding data sources -- [Graphite](../datasources/graphite.md) -- [InfluxDB](../datasources/influxdb.md) -- [OpenTSDB](../datasources/opentsdb.md) +- [Graphite]({{< relref "datasources/graphite.md" >}}) +- [InfluxDB]({{< relref "datasources/influxdb.md" >}}) +- [OpenTSDB]({{< relref "datasources/opentsdb.md" >}}) +- [Prometheus]({{< relref "datasources/prometheus.md" >}}) ## Installing from binary tar file -Download [the latest `.tar.gz` file](http://grafana.org/download/builds) and +Download [the latest `.tar.gz` file](http://grafana.org/download) and extract it. This will extract into a folder named after the version you downloaded. This folder contains all files required to run Grafana. There are no init scripts or install scripts in this package. diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index 18dcf964450..b3b286ba0a9 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -1,17 +1,18 @@ ---- -page_title: Installing using Docker -page_description: Grafana Installation guide using Docker container -page_keywords: grafana, installation, docker, container, guide ---- ++++ +title = "Installing using Docker" +description = "Installing Grafana using Docker guide" +keywords = ["grafana", "configuration", "documentation", "docker"] +type = "docs" +[menu.docs] +name = "Installing using Docker" +identifier = "docker" +parent = "installation" +weight = 4 ++++ # Installing using Docker -> **2.0.2 -> 2.1.0 Upgrade NOTICE!** -> The data and log paths were not correct in the previous image. The grafana database was placed by default in /usr/share/grafana/data instead of the correct path /var/lib/grafana. This means it was not in a dir that was marked as a volume. So if you remove the container it will remove the grafana database. So before updating make sure you copy the /usr/share/grafana/data path from inside the container to the host. - -## Install from official docker image - -Grafana has an official Docker container. +Grafana is very easy to install and run using the offical docker container. $ docker run -i -p 3000:3000 grafana/grafana @@ -36,6 +37,6 @@ an `ENV` instruction. ## Configuration The back-end web server has a number of configuration options. Go the -[Configuration](../installation/configuration.md) page for details on all +[Configuration]({{< relref "configuration.md" >}}) page for details on all those options. diff --git a/docs/sources/installation/index.md b/docs/sources/installation/index.md index 395c101a8e8..a9ebb81d853 100644 --- a/docs/sources/installation/index.md +++ b/docs/sources/installation/index.md @@ -1,37 +1,10 @@ ---- -page_title: Grafana Installation -page_description: Install guide for Grafana. -page_keywords: grafana, installation, documentation ---- - -# Installation - -Grafana is easily installed via a Debian/Ubuntu package (.deb), via -Redhat/Centos package (.rpm) or manually via a tarball that contains all -required files and binaries. If you can't find a package or binary for -your platform, you might be able to build one yourself. Read the [build -from source](../project/building_from_source) instructions for more -information. - -## Platforms -- [Installing on Debian / Ubuntu](debian.md) -- [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](rpm.md) -- [Installing on Mac OS X](mac.md) -- [Installing on Windows](windows.md) -- [Installing on Docker](docker.md) -- [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](provisioning.md) -- [Nightly Builds](http://grafana.org/download/builds.html) - -## Configuration - -The back-end web server has a number of configuration options. Go the -[Configuration](/installation/configuration) page for details on all -those options. - -## Adding data sources - -- [Graphite](../datasources/graphite.md) -- [InfluxDB](../datasources/influxdb.md) -- [OpenTSDB](../datasources/opentsdb.md) - ++++ +title = "Installation" +description = "Install guide for Grafana" +keywords = ["grafana", "installation", "documentation"] +type = "docs" +[menu.docs] +name = "Installation" +identifier = "installation" ++++ diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md index 8002c045d82..2a68ee2172e 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/installation/ldap.md @@ -1,17 +1,23 @@ ---- -page_title: LDAP Integration -page_description: LDAP Integration guide for Grafana. -page_keywords: grafana, ldap, configuration, documentation, integration ---- ++++ +title = "LDAP Authentication" +description = "Grafana LDAP Authentication Guide " +keywords = ["grafana", "configuration", "documentation", "ldap"] +type = "docs" +[menu.docs] +name = "LDAP Authentication" +identifier = "ldap" +parent = "admin" +weight = 2 ++++ -# LDAP Integration +# LDAP Authentication Grafana (2.1 and newer) ships with a strong LDAP integration feature. The LDAP integration in Grafana allows your Grafana users to login with their LDAP credentials. You can also specify mappings between LDAP group memberships and Grafana Organization user roles. ## Configuration -You turn on LDAP in the [main config file](../configuration/#authldap) as well as specify the path to the LDAP +You turn on LDAP in the [main config file]({{< relref "configuration.md#auth-ldap" >}}) as well as specify the path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`). ### Example config diff --git a/docs/sources/installation/mac.md b/docs/sources/installation/mac.md index 640db6d0d6f..912a16c66e8 100644 --- a/docs/sources/installation/mac.md +++ b/docs/sources/installation/mac.md @@ -1,8 +1,13 @@ ---- -page_title: Installing on Mac OS X -page_description: Grafana Installation guide for Mac OS X -page_keywords: grafana, installation, mac, osx, guide ---- ++++ +title = "Installing on Mac" +description = "Installing Grafana on Mac" +keywords = ["grafana", "configuration", "documentation", "mac", "homebrew", "osx"] +type = "docs" +[menu.docs] +parent = "installation" +weight = 4 ++++ + # Installing on Mac diff --git a/docs/sources/installation/migrating_to2.md b/docs/sources/installation/migrating_to2.md index 32e9c3371f3..961933681db 100644 --- a/docs/sources/installation/migrating_to2.md +++ b/docs/sources/installation/migrating_to2.md @@ -1,8 +1,17 @@ ---- -page_title: Migrating from v1.x to 2.x -page_description: Migration guide for Grafana v1.x to v2.x -page_keywords: grafana, installation, migration, documentation ---- ++++ +title = "Migrating from older versions" +description = "Upgrading & Migrating Grafana from older versions" +keywords = ["grafana", "configuration", "documentation", "migration"] +type = "docs" +[menu.docs] +parent = "installation" +weight = 10 ++++ + +# Migrating from older versions + +Normally new versions of Grafana are backward compatible. Any changes to database or dashboard schema will +be automatically migrated when Grafana-server start up without any user action required. # Migrating from v1.x to v2.x @@ -20,8 +29,7 @@ migrate to Grafana 2.0. ## Adding Data sources The `config.js` file has been deprecated. Data sources are now managed via -the UI or [HTTP API](../http_api/overview.md). Manage your -organizations data sources by clicking on the `Data Sources` menu on the +the UI or HTTP API. Manage your organizations data sources by clicking on the `Data Sources` menu on the side menu (which can be toggled via the Grafana icon in the upper left of your browser). @@ -53,7 +61,7 @@ sure your Elasticsearch data source is added. Specify the Elasticsearch index name where your existing Grafana v1.x dashboards are stored (the default is `grafana-dash`). -![](/img/v2/datasource_edit_elastic.jpg) +![](img/docs/v2/datasource_edit_elastic.jpg) ### Importing dashboards from InfluxDB @@ -66,7 +74,7 @@ your Grafana v1.x dashboards are stored, the default is `grafana`. Go to the `Dashboards` view and click on the dashboards search drop down. Click the `Import` button at the bottom of the search drop down. -![](/img/v2/dashboard_import.jpg) +![](img/docs/v2/dashboard_import.jpg) ### Import view @@ -74,7 +82,7 @@ In the Import view you find the section `Migrate dashboards`. Pick the data source you added (from Elasticsearch or InfluxDB), and click the `Import` button. -![](/img/v2/migrate_dashboards.jpg) +![](img/docs/v2/migrate_dashboards.jpg) Your dashboards should be automatically imported into the Grafana 2.0 back-end. diff --git a/docs/sources/installation/performance.md b/docs/sources/installation/performance.md deleted file mode 100644 index f09686687ca..00000000000 --- a/docs/sources/installation/performance.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -page_title: Performance Tips -page_description: Grafana performance tips -page_keywords: grafana, performance, documentation ---- - -# Performance tips - -## Graphite - -Graphite 0.9.14 adds a much needed feature to the JSON rendering API -that is very important for Grafana. If you are experiencing slow load & -rendering times for large time ranges then it is most likely caused by -running Graphite 0.9.12 or lower. - -The latest version of Graphite adds a `maxDataPoints` parameter to the -JSON render API. Without this feature Graphite can return hundreds of -thousands of data points per graph, which can hang your browser. Be sure -to upgrade to -[0.9.14](http://graphite.readthedocs.org/en/latest/releases/0_9_14.html). - - diff --git a/docs/sources/installation/provisioning.md b/docs/sources/installation/provisioning.md index 2ec79d54741..513b469d4f9 100644 --- a/docs/sources/installation/provisioning.md +++ b/docs/sources/installation/provisioning.md @@ -1,20 +1,23 @@ ---- -page_title: Provisioning -page_description: Grafana provisioning -page_keywords: grafana, provisioning, documentation ---- ++++ +title = "Installing via provisioning tools" +description = "Guide to install Grafana via provisioning tools like puppet & chef" +keywords = ["grafana", "provisioning", "documentation", "puppet", "chef", "ansible"] +type = "docs" +[menu.docs] +parent = "installation" +weight = 8 ++++ -# Provisioning + +# Installing via provisioning tools Here are links for how to install Grafana (and some include Graphite or InfluxDB as well) via a provisioning system. These are not maintained by any core Grafana team member and might be out of date. -Some of the linked cookbooks/manifests/etc. will install and configure Grafana 2.x, while some will only install the older Grafana 1.x versions. They've been broken apart below for your convenience. - ### Puppet -* [forge.puppetlabs.com/bfraser/grafana](https://forge.puppetlabs.com/bfraser/grafana) **Note:** The current version works with Grafana 2.x. To install older versions of Grafana use the 1.x series of releases. +* [forge.puppetlabs.com/bfraser/grafana](https://forge.puppetlabs.com/bfraser/grafana) ### Ansible @@ -25,6 +28,6 @@ Some of the linked cookbooks/manifests/etc. will install and configure Grafana 2 ### Chef -* [github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana) **Note:** The current version works with Grafana 2.x. To install older versions of Grafana use the 1.x series of releases. +* [github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana) * [github.com/Nordstrom/grafana2-cookbook](https://github.com/Nordstrom/grafana2-cookbook) diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index d1a402a08c9..b23e851182b 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -1,18 +1,22 @@ ---- -page_title: Installing on RPM-based Linux -page_description: Grafana Installation guide for Centos, Fedora, OpenSuse, Redhat. -page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide ---- ++++ +title = "Installing on RPM-based Linux" +description = "Grafana Installation guide for Centos, Fedora, OpenSuse, Redhat." +keywords = ["grafana", "installation", "documentation", "centos", "fedora", "opensuse", "redhat"] +type = "docs" +[menu.docs] +name = "Installing on Centos / Redhat" +identifier = "rpm" +parent = "installation" +weight = 2 ++++ # Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat) -## Download - Description | Download ------------ | ------------- Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [3.1.1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.1-1470047149.x86_64.rpm) -## Install Latest Stable +## Install Stable You can install Grafana using Yum directly. @@ -106,18 +110,34 @@ By default Grafana will log to `/var/log/grafana` The default configuration specifies a sqlite3 database located at `/var/lib/grafana/grafana.db`. Please backup this database before -upgrades. You can also use MySQL or Postgres as the Grafana database, as detailed on [the configuration page](configuration.md#database). +upgrades. You can also use MySQL or Postgres as the Grafana database, as detailed on [the configuration page]({{< relref "configuration.md#database" >}}). ## Configuration The configuration file is located at `/etc/grafana/grafana.ini`. Go the -[Configuration](/installation/configuration) page for details on all +[Configuration]({{< relref "configuration.md" >}}) page for details on all those options. ### Adding data sources -- [Graphite](../datasources/graphite.md) -- [InfluxDB](../datasources/influxdb.md) -- [OpenTSDB](../datasources/opentsdb.md) +- [Graphite]({{< relref "datasources/graphite.md" >}}) +- [InfluxDB]({{< relref "datasources/influxdb.md" >}}) +- [OpenTSDB]({{< relref "datasources/opentsdb.md" >}}) +- [Prometheus]({{< relref "datasources/prometheus.md" >}}) +## Installing from binary tar file + +Download [the latest `.tar.gz` file](http://grafana.org/download) and +extract it. This will extract into a folder named after the version you +downloaded. This folder contains all files required to run Grafana. There are +no init scripts or install scripts in this package. + +To configure Grafana add a configuration file named `custom.ini` to the +`conf` folder and override any of the settings defined in +`conf/defaults.ini`. + +Start Grafana by executing `./bin/grafana-server web`. The `grafana-server` +binary needs the working directory to be the root install directory (where the +binary and the `public` folder is located). + diff --git a/docs/sources/installation/troubleshooting.md b/docs/sources/installation/troubleshooting.md index 7a6865b9da1..c2716c6478f 100644 --- a/docs/sources/installation/troubleshooting.md +++ b/docs/sources/installation/troubleshooting.md @@ -1,6 +1,13 @@ -page_title: Troubleshooting -page_description: Troubleshooting -page_keywords: grafana, support, documentation ++++ +title = "Troubleshooting" +description = "Guide to troubleshooting Grafana problems" +keywords = ["grafana", "troubleshooting", "documentation", "guide"] +type = "docs" +[menu.docs] +parent = "admin" +weight = 8 ++++ + # Troubleshooting @@ -16,7 +23,7 @@ with Grafana being unable to query Graphite, OpenTSDB or InfluxDB. You might not be able to get metric name completion or the graph might show an error like this: -![](/img/v1/graph_timestore_error.png) +![](img/docs/v1/graph_timestore_error.png) For some types of errors, the `View details` link will show you error details. For many types of HTTP connection errors, however, there is very @@ -24,7 +31,7 @@ little information. The best way to troubleshoot these issues is use the [Chrome developer tools](https://developer.chrome.com/devtools/index). By pressing `F12` you can bring up the chrome dev tools. -![](/img/v1/toubleshooting_chrome_dev_tools.png) +![](img/docs/v1/toubleshooting_chrome_dev_tools.png) There are two important tabs in the Chrome developer tools: `Network` and `Console`. The `Console` tab will show you Javascript errors and @@ -42,32 +49,10 @@ chrome console error, request and response information from the ### Inspecting Grafana metric requests -![](/img/v1/toubleshooting_chrome_dev_tools_network.png) +![](img/docs/v1/toubleshooting_chrome_dev_tools_network.png) After opening the Chrome developer tools for the first time the `Network` tab is empty. You will need to refresh the page to get requests to show. For some type of errors, especially CORS-related, there might not be a response at all. -## Graphite connection issues - -If your Graphite web server is on another domain or IP address from your -Grafana web server you will need to [setup -CORS](../install/#graphite-server-config) (Cross Origin Resource -Sharing). - -You know if you are having CORS-related issues if you get an error like -this in the Chrome developer tools: - -![](/img/v1/toubleshooting_graphite_cors_error.png) - -If the request failed on method `OPTIONS` then you need to review your -Graphite web server configuration. - -## Only blank white page - -When you load Grafana and all you get is a blank white page then you -probably have a Javascript syntax error in `config.js`. In the Chrome -developer tools console you will quickly identify the line of the syntax -error. - diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 7dc4ce6d5d3..f75f29085c5 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -1,13 +1,16 @@ ---- -page_title: Installing on Windows -page_description: Grafana Installation guide for Windows -page_keywords: grafana, installation, windows guide ---- ++++ +title = "Installing on Windows" +description = "Installing Grafana on Windows" +keywords = ["grafana", "configuration", "documentation", "windows"] +type = "docs" +[menu.docs] +parent = "installation" +weight = 3 ++++ + # Installing on Windows -## Download - Description | Download ------------ | ------------- Stable Zip package for Windows | [grafana.3.1.1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.1.1.windows-x64.zip) @@ -29,7 +32,7 @@ command line. If you want to run Grafana as windows service, download [NSSM](https://nssm.cc/). It is very easy add Grafana as a Windows service using that tool. -Read more about the [configuration options](/installation/configuration). +Read more about the [configuration options]({{< relref "configuration.md" >}}). ## Building on Windows diff --git a/docs/sources/plugins/apps.md b/docs/sources/plugins/apps.md index 74038a9feb9..bb39d33bb9d 100644 --- a/docs/sources/plugins/apps.md +++ b/docs/sources/plugins/apps.md @@ -11,7 +11,7 @@ App plugins is a new kind of grafana plugin that can bundle datasource and panel Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu. - + ## Enabling app plugins After installing an app it have to be enabled before it show up as an datasource or panel. You can do that on the app page in the config tab. diff --git a/docs/sources/plugins/datasources.md b/docs/sources/plugins/datasources.md index 3732948d527..dbd0061cbdb 100644 --- a/docs/sources/plugins/datasources.md +++ b/docs/sources/plugins/datasources.md @@ -1,9 +1,3 @@ ---- -page_title: Plugin datasources -page_description: Datasource plugins for Grafana -page_keywords: grafana, plugins, documentation ---- - # Datasources diff --git a/docs/sources/plugins/development.md b/docs/sources/plugins/development.md index 6d2106fad01..9b2a3fb8668 100644 --- a/docs/sources/plugins/development.md +++ b/docs/sources/plugins/development.md @@ -1,10 +1,14 @@ ---- -page_title: Plugin development guide -page_description: Plugin development for Grafana -page_keywords: grafana, plugins, documentation, development ---- ++++ +title = "Developer Guide" +type = "docs" +aliases = ["/plugins/datasources/", "/plugins/apps/", "/plugins/panels/"] +[menu.docs] +name = "Developer Guide" +parent = "plugins" +weight = 5 ++++ -# Plugin development +# Developer Guide From grafana 3.0 it's very easy to develop your own plugins and share them with other grafana users. @@ -32,6 +36,7 @@ will be expected to export different things. You can find what's expected for [d and [apps](./apps.md) plugins in the documentation. ## Start developing your plugin + There are three ways that you can start developing a Grafana plugin. 1. Setup a Grafana development environment. [(described here)](http://docs.grafana.org/project/building_from_source/) and place your plugin in the ```data/plugins``` folder. @@ -45,8 +50,11 @@ the folder contains a subfolder named dist. In that case grafana will mount the This makes it possible to have both built and src content in the same plugin git repo. ## Examples + We currently have three different examples that you can fork/download to get started developing your grafana plugin. - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for querying json data from backends) - - [piechart-panel](https://github.com/grafana/piechart-panel) - [example-app](https://github.com/grafana/example-app) + - [clock-panel](https://github.com/grafana/clock-panel) + - [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts) + - [piechart-panel](https://github.com/grafana/piechart-panel) diff --git a/docs/sources/plugins/index.md b/docs/sources/plugins/index.md index bb5c8062223..217a01f9160 100644 --- a/docs/sources/plugins/index.md +++ b/docs/sources/plugins/index.md @@ -1,8 +1,11 @@ ---- -page_title: Plugin overview -page_description: Plugins for Grafana -page_keywords: grafana, plugins, documentation ---- ++++ +title = "Plugins" +[menu.docs] +name = "Plugins" +identifier = "plugins" +weight = 8 ++++ + # Plugins diff --git a/docs/sources/plugins/installation.md b/docs/sources/plugins/installation.md index e8e69af6e00..ff4f51729af 100644 --- a/docs/sources/plugins/installation.md +++ b/docs/sources/plugins/installation.md @@ -1,14 +1,30 @@ ---- -page_title: Plugin installation -page_description: Plugin installation for Grafana -page_keywords: grafana, plugins, documentation ---- ++++ +title = "Installing Plugins" +type = "docs" +[menu.docs] +parent = "plugins" +weight = 1 ++++ + +# Grafana Plugins + +From Grafana 3.0+ not only are datasource plugins supported but also panel plugins and apps. +Having panels as plugins make it easy to create and add any kind of panel, to show your data +or improve your favorite dashboards. Apps is something new in Grafana that enables +bundling of datasources, panels, dashboards and Grafana pages into a cohesive experience. + +Grafana already have a strong community of contributors and plugin developers. +By making it easier to develop and install plugins we hope that the community +can grow even stronger and develop new plugins that we would never think about. + +To discover plugins checkout the official [Plugin Repository](https://grafana.net/plugins). # Installing plugins The easiest way to install plugins is by using the CLI tool grafana-cli which is bundled with grafana. Before any modification take place after modifying plugins, grafana-server needs to be restarted. ### Grafana plugin directory + On Linux systems the grafana-cli will assume that the grafana plugin directory is `/var/lib/grafana/plugins`. It's possible to override the directory which grafana-cli will operate on by specifying the --pluginsDir flag. On Windows systems this parameter have to be specified for every call. ### Grafana-cli commands diff --git a/docs/sources/plugins/panels.md b/docs/sources/plugins/panels.md index 179af7451c5..168d1320ca5 100644 --- a/docs/sources/plugins/panels.md +++ b/docs/sources/plugins/panels.md @@ -5,6 +5,15 @@ page_keywords: grafana, plugins, documentation --- ++++ +title = "Installing Plugins" +type = "docs" +[menu.docs] +parent = "plugins" +weight = 1 ++++ + + # Panels Panels are the main building blocks of dashboards. diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index 40f963b237c..84a1fcbaa41 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -1,8 +1,10 @@ ---- -page_title: Building from source -page_description: Building from source Grafana. -page_keywords: grafana, build, contribute, documentation ---- ++++ +title = "Building from source" +type = "docs" +[menu.docs] +parent = "installation" +weight = 5 ++++ # Building Grafana from source @@ -24,7 +26,7 @@ go get github.com/grafana/grafana ## Building the backend ``` cd $GOPATH/src/github.com/grafana/grafana -go run build.go setup +go run build.go setup go run build.go build # (or 'go build ./pkg/cmd/grafana-server') ``` diff --git a/docs/sources/reference/admin.md b/docs/sources/reference/admin.md index 266fc998092..df396fed212 100644 --- a/docs/sources/reference/admin.md +++ b/docs/sources/reference/admin.md @@ -1,8 +1,13 @@ ----- -page_title: Administration -page_description: Grafana Administration -page_keywords: grafana, admin, administration, documentation ---- ++++ +title = "Admin Roles" +description = "Users & Organization permission and administration" +keywords = ["grafana", "configuration", "documentation", "admin", "users", "permissions"] +type = "docs" +[menu.docs] +name = "Admin Roles" +parent = "admin" +weight = 3 ++++ # Administration @@ -22,7 +27,7 @@ modify Organization details and options. ## Grafana Administrators - + As a Grafana Administrator, you have complete access to any Organization or User in that instance of Grafana. When performing actions as a Grafana admin, the sidebar will change it's appearance as below to indicate you are performing global server administration. @@ -32,6 +37,6 @@ From the Grafana Server Admin page, you can access the System Info page which su Organizations in Grafana are best suited for a **multi-tenant deployment**. In a multi-tenant deployment, Organizations can be used to provide a full Grafana experience to different sets of users from a single Grafana instance, -at the convenience of the Grafana Administrator. +at the convenience of the Grafana Administrator. -In most cases, a Grafana installation will only have **one** Organization. Since dashboards, data sources and other configuration items are not shared between organizations, there's no need to create multiple Organizations if you want all your users to have access to the same set of dashboards and data. +In most cases, a Grafana installation will only have **one** Organization. Since dashboards, data sources and other configuration items are not shared between organizations, there's no need to create multiple Organizations if you want all your users to have access to the same set of dashboards and data. diff --git a/docs/sources/reference/annotations.md b/docs/sources/reference/annotations.md index 668a163b672..26cb037fdfa 100644 --- a/docs/sources/reference/annotations.md +++ b/docs/sources/reference/annotations.md @@ -1,15 +1,19 @@ ----- -page_title: Annotations -page_description: Annotations user guide -page_keywords: grafana, annotations, guide, documentation ---- ++++ +title = "Annotations" +keywords = ["grafana", "annotations", "documentation", "guide"] +type = "docs" +[menu.docs] +name = "Annotations" +parent = "dashboard_features" +weight = 2 ++++ # Annotations Annotations provide a way to mark points on the graph with rich events. When you hover over an annotation you can get title, tags, and text information for the event. -![](/img/v1/annotated_graph1.png) +![](img/docs/v1/annotated_graph1.png) To add an annotation query click dashboard settings icon in top menu and select `Annotations` from the dropdown. This will open the `Annotations` edit view. Click the `Add` tab to add a new annotation query. @@ -24,7 +28,7 @@ Graphite supports two ways to query annotations. - Graphite events query, use the `Graphite event tags` text input, specify an tag or wildcard (leave empty should also work) ## Elasticsearch annotations -![](/img/v2/annotations_es.png) +![](img/docs/v2/annotations_es.png) Grafana can query any Elasticsearch index for annotation events. The index name can be the name of an alias or an index wildcard pattern. You can leave the search query blank or specify a lucene query. @@ -35,13 +39,14 @@ as the name for the fields that should be used for the annotation title, tags an > **Note** The annotation timestamp field in elasticsearch need to be in UTC format. ## InfluxDB Annotations -![](/img/v2/annotations_influxdb.png) +![](img/docs/v2/annotations_influxdb.png) For InfluxDB you need to enter a query like in the above screenshot. You need to have the ```where $timeFilter``` part. If you only select one column you will not need to enter anything in the column mapping fields. ## Prometheus Annotations -![](/img/v3/annotations_prom.png) + +![](img/docs/v3/annotations_prom.png) Prometheus supports two ways to query annotations. diff --git a/docs/sources/reference/dashboard.md b/docs/sources/reference/dashboard.md index 831dbe3abdc..0bb3fc9ed4f 100644 --- a/docs/sources/reference/dashboard.md +++ b/docs/sources/reference/dashboard.md @@ -1,13 +1,15 @@ ----- -page_title: Dashboard JSON -page_description: Dashboard JSON Reference -page_keywords: grafana, dashboard, json, documentation ---- ++++ +title = "JSON Model" +keywords = ["grafana", "dashboard", "documentation", "json", "model"] +type = "docs" +[menu.docs] +name = "JSON Model" +parent = "dashboard_features" +weight = 100 ++++ # Dashboard JSON -## Overview - A dashboard in Grafana is represented by a JSON object, which stores metadata of its dashboard. Dashboard metadata includes dashboard properties, metadata from rows, panels, template variables, panel queries, etc. To view the JSON of a dashboard, follow the steps mentioned below: diff --git a/docs/sources/reference/dashlist.md b/docs/sources/reference/dashlist.md index ea098541da2..9c5baa55b34 100644 --- a/docs/sources/reference/dashlist.md +++ b/docs/sources/reference/dashlist.md @@ -1,29 +1,32 @@ ----- -page_title: Dashlist Panel -page_description: Dashlist Panel Reference -page_keywords: grafana, dashlist, panel, documentation ---- ++++ +title = "Dashboard List" +keywords = ["grafana", "dashboard list", "documentation", "panel", "dashlist"] +type = "docs" +[menu.docs] +name = "Dashboard list" +parent = "panels" +weight = 4 ++++ -# Dashlist Panel -## Overview +# Dashboard List Panel -The dashboard list panel allows you to display dynamic links to other dashboards. The list can be configured to use starred dashboards, a search query and/or dashboard tags. +The dashboard list panel allows you to display dynamic links to other dashboards. The list can be configured to use starred dashboards, a search query and/or dashboard tags. - + > On each dashboard load, the dashlist panel will re-query the dashboard list, always providing the most up to date results. ## Mode: Starred Dashboards -The `starred` dashboard selection displays starred dashboards, up to the number specified in the `Limit Number to` field, in alphabetical order. On dashboard load, the dashlist panel will re-query the favorites to appear in dashboard list panel, always providing the most up to date results. +The `starred` dashboard selection displays starred dashboards, up to the number specified in the `Limit Number to` field, in alphabetical order. On dashboard load, the dashlist panel will re-query the favorites to appear in dashboard list panel, always providing the most up to date results. - + ## Mode: Search Dashboards -The panel may be configured to search by either string query or tag(s). On dashboard load, the dashlist panel will re-query the dashboard list, always providing the most up to date results. +The panel may be configured to search by either string query or tag(s). On dashboard load, the dashlist panel will re-query the dashboard list, always providing the most up to date results. To configure dashboard list in this manner, select `search` from the Mode select box. When selected, the Search Options section will appear. @@ -38,14 +41,14 @@ Limit number to | Specify the maximum number of dashboards ### Search by string -To search by a string, enter a search query in the `Search Options: Query` field. Queries are case-insensitive, and partial values are accepted. - +To search by a string, enter a search query in the `Search Options: Query` field. Queries are case-insensitive, and partial values are accepted. + ### Search by tag -To search by one or more tags, enter your selection in the `Search Options: Tags:` field. Note that existing tags will not appear as you type, and *are* case sensitive. To see a list of existing tags, you can always return to the dashboard, open the Dashboard Picker at the top and click `tags` link in the search bar. - +To search by one or more tags, enter your selection in the `Search Options: Tags:` field. Note that existing tags will not appear as you type, and *are* case sensitive. To see a list of existing tags, you can always return to the dashboard, open the Dashboard Picker at the top and click `tags` link in the search bar. + -> When multiple tags and strings appear, the dashboard list will display those matching ALL conditions. +> When multiple tags and strings appear, the dashboard list will display those matching ALL conditions. diff --git a/docs/sources/reference/export_import.md b/docs/sources/reference/export_import.md index 0e830db959f..a05ce2f0c6b 100644 --- a/docs/sources/reference/export_import.md +++ b/docs/sources/reference/export_import.md @@ -1,18 +1,23 @@ ---- -page_title: Export & Import Guide -page_description: Export & Import Guide for Grafana -page_keywords: grafana, export, import, documentation ---- ++++ +title = "Export & Import" +keywords = ["grafana", "dashboard", "documentation", "export", "import"] +type = "docs" +[menu.docs] +parent = "dashboard_features" +weight = 8 ++++ # Export and Import +Grafana Dashboads can easily be exported and imported, either from the UI or from the HTTP API. + ## Exporting a dashboard Dashboards are exported in Grafana JSON format, and contain everything you need (layout, variables, styles, data sources, queries, etc)to import the dashboard at a later time. The export feature is accessed from the share menu. - + ### Making a dashboard portable @@ -26,12 +31,12 @@ the dashboard, and will also be added as an required input when the dashboard is To import a dashboard open dashboard search and then hit the import button. - + From here you can upload a dashboard json file, paste a [Grafana.net](https://grafana.net) dashboard url or paste dashboard json text directly into the text area. - + In step 2 of the import process Grafana will let you change the name of the dashboard, pick what data source you want the dashboard to use and specify any metric prefixes (if the dashboard use any). @@ -40,7 +45,7 @@ data source you want the dashboard to use and specify any metric prefixes (if th Find dashboads for common server applications at [Grafana.net/dashboards](https://grafana.net/dashboards). - + ## Import & Sharing with Grafana 2.x or 3.0 diff --git a/docs/sources/reference/graph.md b/docs/sources/reference/graph.md index 9c2ab63fbaa..62c42f78b92 100644 --- a/docs/sources/reference/graph.md +++ b/docs/sources/reference/graph.md @@ -1,20 +1,24 @@ ----- -page_title: Graph Panel -page_description: Graph Panel Reference -page_keywords: grafana, graph, panel, documentation ---- ++++ +title = "Graph Panel" +keywords = ["grafana", "graph panel", "documentation", "guide", "graph"] +type = "docs" +[menu.docs] +name = "Graph" +parent = "panels" +weight = 1 ++++ # Graph Panel The main panel in Grafana is simply named Graph. It provides a very rich set of graphing options. - + Clicking the title for a panel exposes a menu. The `edit` option opens additional configuration options for the panel. ## General -![](/img/v2/graph_general.png) +![](img/docs/v2/graph_general.png) The general tab allows customization of a panel's appearance and menu options. @@ -52,7 +56,7 @@ options. ## Axes & Grid -![](/img/v2/graph_axes_grid_options.png) +![](img/docs/v2/graph_axes_grid_options.png) The Axes & Grid tab controls the display of axes, grids and legend. @@ -91,11 +95,11 @@ The legend values are calculated client side by Grafana and depend on what type aggregation or point consolidation you metric query is using. All the above legend values cannot be correct at the same time. For example if you plot a rate like requests/second, this is probably using average as aggregator, then the Total in the legend will not represent the total number of requests. -It is just the sum of all data data points received by Grafana. +It is just the sum of all data points received by Grafana. ## Display styles -![](/img/v2/graph_display_styles.png) +![](img/docs/v2/graph_display_styles.png) Display styles controls properties of the graph. @@ -140,4 +144,4 @@ a thicker line width to make it standout. ## Time range -![](/img/v2/graph_time_range.png) +![](img/docs/v2/graph_time_range.png) diff --git a/docs/sources/reference/keyboard_shortcuts.md b/docs/sources/reference/keyboard_shortcuts.md index 9947418ae5a..96709dfb725 100644 --- a/docs/sources/reference/keyboard_shortcuts.md +++ b/docs/sources/reference/keyboard_shortcuts.md @@ -1,7 +1,11 @@ -page_title: Keyboard Shortcuts -page_description: Keyboard Shortcuts for Grafana -page_keywords: grafana, export, import, documentation ---- ++++ +title = "Keyboard shortcuts" +keywords = ["grafana", "dashboard", "documentation", "shortcuts"] +type = "docs" +[menu.docs] +parent = "dashboard_features" +weight = 8 ++++ # Keyboard Shortcuts @@ -11,7 +15,7 @@ No mouse? No problem. Grafana has extensive keyboard shortcuts to allow you to n Press `Shift`+`?` to open the keyboard shortcut dialog from anywhere within the dashboard views. - + |Shortcut|Action| diff --git a/docs/sources/reference/playlist.md b/docs/sources/reference/playlist.md index f35767625c5..359a5d8f3bd 100644 --- a/docs/sources/reference/playlist.md +++ b/docs/sources/reference/playlist.md @@ -1,8 +1,12 @@ ---- -page_title: Playlist Guide -page_description: Playlist guide for Grafana -page_keywords: grafana, playlist, documentation ---- ++++ +title = "Playlist" +keywords = ["grafana", "dashboard", "documentation", "playlist"] +type = "docs" +[menu.docs] +parent = "dashboard_features" +weight = 4 ++++ + # Playlist @@ -12,9 +16,9 @@ Since Grafana automatically scales Dashboards to any resolution they're perfect ## Creating a Playlist -The Playlist feature can be accessed from Grafana's sidemenu. Click the 'Playlist' button from the sidemenu to access the Playlist functionality. When 'Playlist' button is clicked, playlist view will open up showing saved playlists and an option to create new playlists. +{{< docs-imagebox img="img/docs/v3/playlist.png" max-width="25rem" >}} - +The Playlist feature can be accessed from Grafana's sidemenu, in the Dashboard submenu. Click on "New Playlist" button to create a new playlist. Firstly, name your playlist and configure a time interval for Grafana to wait on a particular Dashboard before advancing to the next one on the Playlist. diff --git a/docs/sources/reference/scripting.md b/docs/sources/reference/scripting.md index d896a2c7650..551805b567a 100644 --- a/docs/sources/reference/scripting.md +++ b/docs/sources/reference/scripting.md @@ -1,8 +1,12 @@ ----- -page_title: Scripted dashboards -page_description: Scripted dashboards -page_keywords: grafana, scripted, guide, documentation ---- ++++ +title = "Scripted Dashboards" +keywords = ["grafana", "dashboard", "documentation", "scripted"] +type = "docs" +[menu.docs] +parent = "dashboard_features" +weight = 9 ++++ + # Scripted Dashboards diff --git a/docs/sources/reference/search.md b/docs/sources/reference/search.md index e2d6647229c..04d317d38e0 100644 --- a/docs/sources/reference/search.md +++ b/docs/sources/reference/search.md @@ -1,54 +1,58 @@ ----- -page_title: Dashboard Search -page_description: Dashboard Search in Grafana -page_keywords: grafana, search, guide, documentation ---- ++++ +title = "Search" +keywords = ["grafana", "dashboard", "documentation", "search"] +type = "docs" +[menu.docs] +parent = "dashboard_features" +weight = 5 ++++ + # Dashboard Search Dashboards can be searched by the dashboard name, filtered by one (or many) tags or filtered by starred status. The dashboard search is accessed through the dashboard picker, available in the dashboard top nav area. - + 1. `Dashboard Picker`: The Dashboard Picker is your primary navigation tool to move between dashboards. It is present on all dashboards, and open the Dashboard Search. The dashboard picker also doubles as the title of the current dashboard. 2. `Search Bar`: The search bar allows you to enter any string and search both database and file based dashboards in real-time. -3. `Starred`: The starred link allows you to filter the list to display only starred dashboards. -4. `Tags`: The tags filter allows you to filter the list by dashboard tags. +3. `Starred`: The starred link allows you to filter the list to display only starred dashboards. +4. `Tags`: The tags filter allows you to filter the list by dashboard tags. When using only a keyboard, you can use your keyboard arrow keys to navigate the results, hit enter to open the selected dashboard. ## Find by dashboard name - + -To search and load dashboards click the open folder icon in the header or use the shortcut `CTRL`+`F`. Begin typing any part of the desired dashboard names. Search will return results for for any partial string match in real-time, as you type. +To search and load dashboards click the open folder icon in the header or use the shortcut `CTRL`+`F`. Begin typing any part of the desired dashboard names. Search will return results for for any partial string match in real-time, as you type. Dashboard search is: - Real-time - *Not* case sensitive -- Functional across stored *and* file based dashboards. +- Functional across stored *and* file based dashboards. ## Filter by Tag(s) Tags are a great way to organize your dashboards, especially as the number of dashboards grow. Tags can be added and managed in the dashboard `Settings`. -To filter the dashboard list by tag, click on any tag appearing in the right column. The list may be further filtered by clicking on additional tags: +To filter the dashboard list by tag, click on any tag appearing in the right column. The list may be further filtered by clicking on additional tags: - + Alternately, to see a list of all available tags, click the tags link in the search bar. All tags will be shown, and when a tag is selected, the dashboard search will be instantly filtered: - + When using only a keyboard: `tab` to focus on the *tags* link, `▼` down arrow key to find a tag and select with the `Enter` key. -**Note**: When multiple tags are selected, Grafana will show dashboards that include **all**. +**Note**: When multiple tags are selected, Grafana will show dashboards that include **all**. ## Filter by Starred Starring is a great way to organize and find commonly used dashboards. To show only starred dashboards in the list, click the *starred* link in the search bar: - + -When using only a keyboard: `tab` to focus on the *stars* link, `▼` down arrow key to find a tag and select with the `Enter` key. \ No newline at end of file +When using only a keyboard: `tab` to focus on the *stars* link, `▼` down arrow key to find a tag and select with the `Enter` key. diff --git a/docs/sources/reference/sharing.md b/docs/sources/reference/sharing.md index c20f4e5f67c..08bfcf4c9d1 100644 --- a/docs/sources/reference/sharing.md +++ b/docs/sources/reference/sharing.md @@ -1,8 +1,11 @@ ----- -page_title: Sharing -page_description: Sharing -page_keywords: grafana, sharing, guide, documentation ---- ++++ +title = "Sharing" +keywords = ["grafana", "dashboard", "documentation", "sharing"] +type = "docs" +[menu.docs] +parent = "dashboard_features" +weight = 6 ++++ # Sharing features Grafana provides a number of ways to share a dashboard or a specific panel to other users within your @@ -19,7 +22,7 @@ A dashboard snapshot is an instant way to share an interactive dashboard publicl (metric, template and annotation) and panel links, leaving only the visible metric data and series names embedded into your dashboard. Dashboard snapshots can be accessed by anyone who has the link and can reach the URL. -![](/img/v2/dashboard_snapshot_dialog.png) +![](img/docs/v2/dashboard_snapshot_dialog.png) ### Publish snapshots You can publish snapshots to you local instance or to [snapshot.raintank.io](http://snapshot.raintank.io). The later is a free service diff --git a/docs/sources/reference/singlestat.md b/docs/sources/reference/singlestat.md index 954bfeed8b7..8c6cdc6c727 100644 --- a/docs/sources/reference/singlestat.md +++ b/docs/sources/reference/singlestat.md @@ -1,12 +1,17 @@ ----- -page_title: Singlestat Panel -page_description: Singlestat Panel Reference -page_keywords: grafana, singlestat, panel, documentation ---- ++++ +title = "Singletat Panel" +keywords = ["grafana", "dashboard", "documentation", "panels", "singlestat"] +type = "docs" +[menu.docs] +name = "Singlestat" +parent = "panels" +weight = 2 ++++ + # Singlestat Panel -![](/img/v1/singlestat_panel2.png) +![](img/docs/v1/singlestat_panel2.png) The Singlestat Panel allows you to show the one main summary stat of a SINGLE series. It reduces the series into a single number (by looking at the max, min, average, or sum of values in the series). Singlestat also provides thresholds to color the stat or the Panel background. It can also translate the single number into a text value, and show a sparkline summary of the series. @@ -14,7 +19,7 @@ The Singlestat Panel allows you to show the one main summary stat of a SINGLE se The singlestat panel has a normal query editor to allow you define your exact metric queries like many other Panels. Through the Options tab, you can access the Singlestat-specific functionality. - + 1. `Big Value`: Big Value refers to how we display the main stat for the Singlestat Panel. This is always a single value that is displayed in the Panel in between two strings, `Prefix` and `Suffix`. The single number is calculated by choosing a function (min,max,average,current,total) of your metric query. This functions reduces your query into a single numeric value. 2. `Font Size`: You can use this section to select the font size of the different texts in the Singlestat Panel, i.e. prefix, value and postfix. @@ -27,19 +32,19 @@ The singlestat panel has a normal query editor to allow you define your exact me The coloring options of the Singlestat Panel config allow you to dynamically change the colors based on the Singlestat value. - + 1. `Background`: This checkbox applies the configured thresholds and colors to the entirety of the Singlestat Panel background. 2. `Value`: This checkbox applies the configured thresholds and colors to the summary stat. 3. `Thresholds`: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90. 4. `Colors`: Select a color and opacity -5. `Invert order`: This link toggles the threshold color order.
For example: Green, Orange, Red () will become Red, Orange, Green (). +5. `Invert order`: This link toggles the threshold color order.
For example: Green, Orange, Red () will become Red, Orange, Green (). ### Spark Lines Sparklines are a great way of seeing the historical data related to the summary stat, providing valuable context at a glance. Sparklines act differently than traditional Graph Panels and do not include x or y axis, coordinates, a legend, or ability to interact with the graph. - + 1. `Show`: The show checkbox will toggle whether the spark line is shown in the Panel. When unselected, only the Singlestat value will appear. 2. `Background`: Check if you want the sparklines to take up the full panel width, or uncheck if they should be below the main Singlestat value. @@ -52,13 +57,13 @@ Sparklines are a great way of seeing the historical data related to the summary Value to text mapping allows you to translate the value of the summary stat into explicit text. The text will respect all styling, thresholds and customization defined for the value. This can be useful to translate the number of the main Singlestat value into a context-specific human-readable word or message. - + ## Troubleshooting ### Multiple Series Error - + Grafana 2.5 introduced stricter checking for multiple-series on singlestat panels. In previous versions, the panel logic did not verify that only a single series was used, and instead, displayed the first series encountered. Depending on your data source, this could have lead to inconsistent data being shown and/or a general confusion about which metric was being displayed. diff --git a/docs/sources/reference/table_panel.md b/docs/sources/reference/table_panel.md index a857be38357..616909c86e2 100644 --- a/docs/sources/reference/table_panel.md +++ b/docs/sources/reference/table_panel.md @@ -1,12 +1,17 @@ ----- -page_title: Table Panel -page_description: Table Panel Reference -page_keywords: grafana, table, panel, documentation ---- ++++ +title = "Table Panel" +keywords = ["grafana", "dashboard", "documentation", "panels", "table panel"] +type = "docs" +[menu.docs] +name = "Table" +parent = "panels" +weight = 2 ++++ + # Table Panel - + The new table panel is very flexible, supporting both multiple modes for time series as well as for table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options. @@ -17,7 +22,7 @@ To view table panels in action and test different configurations with sample dat The table panel has many ways to manipulate your data for optimal presentation. - + 1. `Data`: Control how your query is transformed into a table. 2. `Table Display`: Table display options. @@ -25,7 +30,7 @@ The table panel has many ways to manipulate your data for optimal presentation. ## Data to Table - + The data section contains the **To Table Transform (1)**. This is the primary option for how your data/metric query should be transformed into a table format. The **Columns (2)** option allows you to select what columns @@ -33,38 +38,38 @@ you want in the table. Only applicable for some transforms. ### Time series to rows - + In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series. ### Time series to columns -![](/img/v2/table_ts_to_columns2.png) +![](img/docs/v2/table_ts_to_columns2.png) This transform allows you to take multiple time series and group them by time. Which will result in the primary column being `Time` and a column for each time series. ### Time series aggregations -![](/img/v2/table_ts_to_aggregations2.png) +![](img/docs/v2/table_ts_to_aggregations2.png) This table transformation will lay out your table into rows by metric, allowing columns of `Avg`, `Min`, `Max`, `Total`, `Current` and `Count`. More than one column can be added. ### Annotations -![](/img/v2/table_annotations.png) +![](img/docs/v2/table_annotations.png) If you have annotations enabled in the dashboard you can have the table show them. If you configure this mode then any queries you have in the metrics tab will be ignored. ### JSON Data -![](/img/v2/table_json_data.png) +![](img/docs/v2/table_json_data.png) If you have an Elasticsearch **Raw Document** query or an Elasticsearch query without a `date histogram` use this transform mode and pick the columns using the **Columns** section. -![](/img/v2/elastic_raw_doc.png) +![](img/docs/v2/elastic_raw_doc.png) ## Table Display - + 1. `Pagination (Page Size)`: The table display fields allow you to control The `Pagination` (page size) is the threshold at which the table rows will be broken into pages. For example, if your table had 95 records with a pagination value of 10, your table would be split across 9 pages. 2. `Scroll`: The `scroll bar` checkbox toggles the ability to scroll within the panel, when unchecked, the panel height will grow to display all rows. @@ -75,7 +80,7 @@ transform mode and pick the columns using the **Columns** section. The column styles allow you control how dates and numbers are formatted. - + 1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values. 2. `Type`: The three supported types of types are `Number`, `String` and `Date`. diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index e605af845ea..2ca8b47b9f9 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -1,15 +1,18 @@ ----- -page_title: Dashboard Templating -page_description: Dashboard Templating -page_keywords: grafana, templating, variables, guide, documentation ---- ++++ +title = "Templating" +keywords = ["grafana", "templating", "documentation", "guide"] +type = "docs" +[menu.docs] +name = "Templating" +parent = "dashboard_features" +weight = 1 ++++ -# Dashboard Templating -![](/img/v2/templating_var_list.png) +# Templating -## Overview + -Dashboard Templating allows you to make your Dashboards more interactive and dynamic. +Dashboard Templating allows you to make your Dashboards more interactive and dynamic. They’re one of the most powerful and most used features of Grafana, and they’ve recently gotten even more attention in Grafana 2.0 and Grafana 2.1. @@ -40,13 +43,13 @@ You can utilize the special ** All ** value to allow the Dashboard user to query #### Multi-select As of Grafana 2.1, it is now possible to select a subset of Query Template variables (previously it was possible to select an individual value or 'All', not multiple values that were less than All). This is accomplished via the Multi-Select option. If enabled, the Dashboard user will be able to enable and disable individual variables. -The Multi-Select functionality is taken a step further with the introduction of Multi-Select Tagging. This functionality allows you to group individual Template variables together under a Tag or Group name. +The Multi-Select functionality is taken a step further with the introduction of Multi-Select Tagging. This functionality allows you to group individual Template variables together under a Tag or Group name. -For example, if you were using Templating to list all 20 of your applications, you could use Multi-Select Tagging to group your applications by function or region or criticality, etc. +For example, if you were using Templating to list all 20 of your applications, you could use Multi-Select Tagging to group your applications by function or region or criticality, etc. > Note: Multi-Select Tagging functionality is currently experimental but is part of Grafana 2.1. To enable this feature click the enable icon when editing Template options for a particular variable. - + Grafana gets the list of tags and the list of values in each tag by performing two queries on your metric namespace. @@ -58,20 +61,19 @@ Note: a proof of concept shim that translates the metric query into a SQL call i Once configured, Multi-Select Tagging provides a convenient way to group and your template variables, and slice your data in the exact way you want. The Tags can be seen on the right side of the template pull-down. -![](/img/v2/multi-select.gif) - +![](img/docs/v2/multi-select.gif) ### Interval Use the `Interval` type to create Template variables around time ranges (eg. `1m`,`1h`, `1d`). There is also a special `auto` option that will change depending on the current time range, you can specify how many times the current time range should be divided to calculate the current `auto` range. -![](/img/v2/templated_variable_parameter.png) +![](img/docs/v2/templated_variable_parameter.png) ### Custom -Use the `Custom` type to manually create Template variables around explicit values that are hard-coded into the Dashboard, and not dependent on any Data Source. You can specify multiple Custom Template values by separating them with a comma. +Use the `Custom` type to manually create Template variables around explicit values that are hard-coded into the Dashboard, and not dependent on any Data Source. You can specify multiple Custom Template values by separating them with a comma. -## Utilizing Template Variables with Repeating Panels and Repeating Rows +## Repeating Panels and Repeating Rows Template Variables can be very useful to dynamically change what you're visualizing on a given panel. Sometimes, you might want to create entire new Panels (or Rows) based on what Template Variables have been selected. This is now possible in Grafana 2.1. diff --git a/docs/sources/reference/timerange.md b/docs/sources/reference/timerange.md index 2ab428622bc..0063a0ee361 100644 --- a/docs/sources/reference/timerange.md +++ b/docs/sources/reference/timerange.md @@ -1,32 +1,37 @@ ----- -page_title: Time Range options -page_description: Time range user guide -page_keywords: grafana, time range, guide, documentation ---- ++++ +title = "Time Range" +keywords = ["grafana", "dashboard", "documentation", "time range"] +type = "docs" +[menu.docs] +name = "Time Range" +parent = "dashboard_features" +weight = 7 ++++ + # Time Range Controls Grafana provides numerous ways to manage the time ranges of the data being visualized, both at the Dashboard-level and the Panel-level. - + In the top right, you have the master Dashboard time picker (it's in between the 'Zoom out' and the 'Refresh' links). -1. `Current time range & refresh interval`: This shows the current dashboard time and refresh interval. It also acts as the menu button to toggle the time range controls. -2. `Quick ranges`: Quick ranges are preset values to choose a relative time. At this time, quick ranges are not configurable, and will appear on every dashboard. +1. `Current time range & refresh interval`: This shows the current dashboard time and refresh interval. It also acts as the menu button to toggle the time range controls. +2. `Quick ranges`: Quick ranges are preset values to choose a relative time. At this time, quick ranges are not configurable, and will appear on every dashboard. 3. `Time range`: The time range section allows you to mix both explicit and relative ranges. The explicit time range format is `YYYY-MM-DD HH:MM:SS` -4. `Refreshing every:` When enabled, auto-refresh will reload the dashboard at the specified time range. Auto-refresh is most commonly used with relative time ranges ending in `now`, so new data will appear when the dashboard refreshes. +4. `Refreshing every:` When enabled, auto-refresh will reload the dashboard at the specified time range. Auto-refresh is most commonly used with relative time ranges ending in `now`, so new data will appear when the dashboard refreshes. These settings apply to all Panels in the Dashboard (except those with Panel Time Overrides enabled) ## Time Units -The following time units are supported: `s (seconds)`, `m (minutes)`, `h (hours)`, `d (days)`, `w (weeks)`, `M (months)`, `y (years)`. The minus operator allows you to step back in time, relative to now. If you wish to display the full period of the unit (day, week, month, etc...), append `/$unit` to the end. +The following time units are supported: `s (seconds)`, `m (minutes)`, `h (hours)`, `d (days)`, `w (weeks)`, `M (months)`, `y (years)`. The minus operator allows you to step back in time, relative to now. If you wish to display the full period of the unit (day, week, month, etc...), append `/$unit` to the end. Take a look at some examples to seen these concepts in practice: -Example Relative Range | From: | To: --------------- | ----- | --- +Example Relative Range | From: | To: +-------------- | ----- | --- Last 5 minutes | `now-5m` | `now` The day so far | `now/d` | `now` This week | `now/w` | `now/w` @@ -38,7 +43,7 @@ Previous Month | `now-1M/M` | `now-1M/M` There are two settings available from the Dashboard Settings area, allowing customization of the auto-refresh intervals and the definition of `now`. - + ### Auto-Refresh Options @@ -54,15 +59,15 @@ Users often ask, [when will then be now](https://www.youtube.com/watch?v=VeZ9HhH You can override the relative time range for individual panels, causing them to be different than what is selected in the Dashboard time picker in the upper right. This allows you to show metrics from different time periods or days at the same time. - + You control these overrides in panel editor mode and the tab `Time Range`. - + When you zoom or change the Dashboard time to a custom absolute time range, all panel overrides will be disabled. The panel relative time override is only active when the dashboard time is also relative. The panel timeshift override is always active, even when the dashboard time is absolute. The `Hide time override info` option allows you to hide the override info text that is by default shown in the upper right of a panel when overridden time range options. -Note: You can only override the dashboard time with relative time ranges. Absolute time ranges are not available. +Note: You can only override the dashboard time with relative time ranges. Absolute time ranges are not available. diff --git a/docs/sources/tutorials/hubot_howto.md b/docs/sources/tutorials/hubot_howto.md index 85ebb3e7c85..2478e0569f0 100644 --- a/docs/sources/tutorials/hubot_howto.md +++ b/docs/sources/tutorials/hubot_howto.md @@ -1,9 +1,11 @@ ---- -page_title: How To integrate Hubot and Grafana -page_description: Hubot Grafana install guide -page_keywords: grafana, tutorials, hubot, slack, hipchat, setup, install, config -author: Torkel Ödegaard ---- ++++ +title = "How To integrate Hubot and Grafana" +type = "docs" +keywords = ["grafana", "tutorials", "hubot", "slack", "hipchat", "setup", "install", "config"] +[menu.docs] +parent = "tutorials" +weight = 10 ++++ # How to integrate Hubot with Grafana @@ -20,7 +22,7 @@ take you to the graph. > is so Hipchat and Slack can show them reliably (they require the image to be publicly available).
- +
## What is Hubot? @@ -68,7 +70,7 @@ To verify that this feature works try the `Direct link to rendered image` link i If you do not get an image when opening this link verify that the required font packages are installed for phantomjs to work. ### Grafana API Key - + You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key. You can add these from the API Keys page which you find in the Organization dropdown. @@ -113,7 +115,7 @@ Now you can add an alias like this:
Using the alias:
- +
## Summary diff --git a/docs/sources/tutorials/index.md b/docs/sources/tutorials/index.md index fd064ba5b15..cb11940c6dd 100644 --- a/docs/sources/tutorials/index.md +++ b/docs/sources/tutorials/index.md @@ -1,8 +1,9 @@ ---- -page_title: Grafana Tutorials -page_description: Tutorials -page_keywords: grafana, tutorials ---- ++++ +title = "Tutorials" +[menu.docs] +identifier = "tutorials" +weight = 6 ++++ # Tutorials diff --git a/docs/sources/tutorials/screencasts.md b/docs/sources/tutorials/screencasts.md new file mode 100644 index 00000000000..f92ead64d49 --- /dev/null +++ b/docs/sources/tutorials/screencasts.md @@ -0,0 +1,66 @@ ++++ +title = "Screencasts" +type = "docs" +[menu.docs] +identifier = "screencasts" +parent = "guides" +weight = 10 ++++ + +# Screencasts + + +{{< screencast src="https://www.youtube.com/embed/sKNZMtoSHN4?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" >}} + +### Episode 7 - Beginners guide to building dashboards + +For newer users of Grafana, this screencast will familiarize you with the general UI and teach you how to build your first Dashboard. + +
+ +{{< screencast src="https://www.youtube.com/embed/9ZCMVNxUf6s?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" >}} + +### Episode 6 - Adding data sources, users & organizations + +Now that Grafana has been installed, learn about adding data sources and get a closer look at adding and managing Users and Organizations. + +
+ +{{< screencast src="https://www.youtube.com/embed/E-gMFv85FE8?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" >}} + +### Episode 5 - Installation & Configuration on Red Hat / CentOS + +This screencasts shows how to get Grafana 2.0 installed and configured quickly on RPM-based Linux operating systems. + +
+{{< screencast src="https://www.youtube.com/embed/JY22EBOR9hQ?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" >}} + +### Episode 4 - Installation & Configuration on Ubuntu / Debian + +Learn how to easily install the dependencies and packages to get Grafana 2.0 up and running on Ubuntu or Debian in just a few minutes. + +
+ +{{< screencast src="https://www.youtube.com/embed/FC13uhFRsVw?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" >}} + +### Episode 3 - Whats New In Grafana 2.0 + +This screencast highlights many of the great new features that were included in the Grafana 2.0 release. + +
+ +{{< screencast src="//www.youtube.com/embed/FhNUrueWwOk?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" >}} +### Episode 2 - Templated Graphite Queries + +The screencast focuses on Templating with the Graphite Data Source. Learn how to make dynamic and adaptable Dashboards for your Graphite metrics. + +
+ +{{< screencast src="//www.youtube.com/embed/mgcJPREl3CU?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" >}} +### Episode 1 - Building Graphite Queries + +Learn how the Graphite Query Editor works, and how to use different graphing functions. There's also an introduction to graph display settings. + +
+ + diff --git a/emails/templates/alert_notification.html b/emails/templates/alert_notification.html index 2c9f78ce6fb..d0d69faa106 100644 --- a/emails/templates/alert_notification.html +++ b/emails/templates/alert_notification.html @@ -65,7 +65,12 @@
- + [[if ne .ImageLink "" ]] + Alerting Panel + [[end]] + [[if ne .EmbededImage "" ]] + Alerting Panel + [[end]]
diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 460ad56e1ab..e745f820aec 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -252,33 +252,30 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R return ApiSuccess("Test notification sent") } -func getAlertIdForRequest(c *middleware.Context) (int64, error) { - alertId := c.QueryInt64("alertId") - panelId := c.QueryInt64("panelId") - dashboardId := c.QueryInt64("dashboardId") - - if alertId == 0 && dashboardId == 0 && panelId == 0 { - return 0, fmt.Errorf("Missing alertId or dashboardId and panelId") +//POST /api/alerts/:alertId/pause +func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response { + cmd := models.PauseAlertCommand{ + OrgId: c.OrgId, + AlertId: c.ParamsInt64("alertId"), + Paused: dto.Paused, } - if alertId == 0 { - //fetch alertId - query := models.GetAlertsQuery{ - OrgId: c.OrgId, - DashboardId: dashboardId, - PanelId: panelId, - } - - if err := bus.Dispatch(&query); err != nil { - return 0, err - } - - if len(query.Result) != 1 { - return 0, fmt.Errorf("PanelId is not unique on dashboard") - } - - alertId = query.Result[0].Id + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "", err) } - return alertId, nil + var response models.AlertStateType = models.AlertStateNoData + pausedState := "un paused" + if cmd.Paused { + response = models.AlertStatePaused + pausedState = "paused" + } + + result := map[string]interface{}{ + "alertId": cmd.AlertId, + "state": response, + "message": "alert " + pausedState, + } + + return Json(200, result) } diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 2803aa46435..48bf6c327ad 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -44,3 +44,19 @@ func GetAnnotations(c *middleware.Context) Response { return Json(200, result) } + +func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response { + repo := annotations.GetRepository() + + err := repo.Delete(&annotations.DeleteParams{ + AlertId: cmd.PanelId, + DashboardId: cmd.DashboardId, + PanelId: cmd.PanelId, + }) + + if err != nil { + return ApiError(500, "Failed to delete annotations", err) + } + + return ApiSuccess("Annotations deleted") +} diff --git a/pkg/api/api.go b/pkg/api/api.go index deb29d730eb..ed73f2dc76d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -252,6 +252,7 @@ func Register(r *macaron.Macaron) { r.Group("/alerts", func() { r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest)) + r.Post("/:alertId/pause", bind(dtos.PauseAlertCommand{}), wrap(PauseAlert), reqEditorRole) r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert)) r.Get("/", wrap(GetAlerts)) r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard)) @@ -265,9 +266,10 @@ func Register(r *macaron.Macaron) { r.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification)) r.Get("/:notificationId", wrap(GetAlertNotificationById)) r.Delete("/:notificationId", wrap(DeleteAlertNotification)) - }, reqOrgAdmin) + }, reqEditorRole) r.Get("/annotations", wrap(GetAnnotations)) + r.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations)) // error test r.Get("/metrics/error", wrap(GenerateError)) diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go index 4d5fa1f52db..23814407182 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/pkg/api/cloudwatch/metrics.go @@ -29,7 +29,7 @@ var customMetricsDimensionsMap map[string]map[string]map[string]*CustomMetricsCa func init() { metricsMap = map[string][]string{ "AWS/ApiGateway": {"4XXError", "5XXError", "CacheHitCount", "CacheMissCount", "Count", "IntegrationLatency", "Latency"}, - "AWS/ApplicationELB": {"ActiveConnectionCount", "ClientTLSNegotiationErrorCount", "HealthyHostCount", "HTTPCode_ELB_4XX_Count", "HTTPCode_ELB_5XX_Count", "HTTPCode_Target_2XX_Count", "HTTPCode_Target_3XX_Count", "HTTPCode_Target_4XX_Count", "HTTPCode_Target_5XX_Count", "NewConnectionCount", "ProcessedBytes", "RejectedConnectionCount", "RequestCount", "TargetConnectionErrorCount", "TargetResponseTime", "TargetTLSNegotiationErrorCount", "UnhealthyHostCount"}, + "AWS/ApplicationELB": {"ActiveConnectionCount", "ClientTLSNegotiationErrorCount", "HealthyHostCount", "HTTPCode_ELB_4XX_Count", "HTTPCode_ELB_5XX_Count", "HTTPCode_Target_2XX_Count", "HTTPCode_Target_3XX_Count", "HTTPCode_Target_4XX_Count", "HTTPCode_Target_5XX_Count", "NewConnectionCount", "ProcessedBytes", "RejectedConnectionCount", "RequestCount", "TargetConnectionErrorCount", "TargetResponseTime", "TargetTLSNegotiationErrorCount", "UnHealthyHostCount"}, "AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"}, "AWS/Billing": {"EstimatedCharges"}, "AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"}, diff --git a/pkg/api/common.go b/pkg/api/common.go index 82eed0db5fe..bd1c8be477d 100644 --- a/pkg/api/common.go +++ b/pkg/api/common.go @@ -79,7 +79,7 @@ func Json(status int, body interface{}) *NormalResponse { func ApiSuccess(message string) *NormalResponse { resp := make(map[string]interface{}) resp["message"] = message - return Respond(200, resp) + return Json(200, resp) } func ApiError(status int, message string, err error) *NormalResponse { diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 1812226aa90..29a38e43bd6 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -153,16 +153,14 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { return ApiError(500, "Failed to save dashboard", err) } - if setting.AlertingEnabled { - alertCmd := alerting.UpdateDashboardAlertsCommand{ - OrgId: c.OrgId, - UserId: c.UserId, - Dashboard: cmd.Result, - } + alertCmd := alerting.UpdateDashboardAlertsCommand{ + OrgId: c.OrgId, + UserId: c.UserId, + Dashboard: cmd.Result, + } - if err := bus.Dispatch(&alertCmd); err != nil { - return ApiError(500, "Failed to save alerts", err) - } + if err := bus.Dispatch(&alertCmd); err != nil { + return ApiError(500, "Failed to save alerts", err) } c.TimeRequest(metrics.M_Api_Dashboard_Save) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 97f2529c781..34c4271ebf6 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -95,6 +95,13 @@ func ProxyDataSourceRequest(c *middleware.Context) { return } + if ds.Type == m.DS_INFLUXDB { + if c.Query("db") != ds.Database { + c.JsonApiErr(403, "Datasource is not configured to allow this database", nil) + return + } + } + targetUrl, _ := url.Parse(ds.Url) if len(setting.DataProxyWhiteList) > 0 { if _, exists := setting.DataProxyWhiteList[targetUrl.Host]; !exists { diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 18b48cd8e29..2b9964f7a71 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -92,6 +92,11 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) { cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { + if err == m.ErrDataSourceNameExists { + c.JsonApiErr(409, err.Error(), err) + return + } + c.JsonApiErr(500, "Failed to add datasource", err) return } diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index e024768cd5e..bf4d7f4353e 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -58,3 +58,8 @@ type NotificationTestCommand struct { Type string `json:"type"` Settings *simplejson.Json `json:"settings"` } + +type PauseAlertCommand struct { + AlertId int64 `json:"alertId"` + Paused bool `json:"paused"` +} diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go index a5d5823e1a4..45415978ee1 100644 --- a/pkg/api/dtos/annotations.go +++ b/pkg/api/dtos/annotations.go @@ -15,3 +15,9 @@ type Annotation struct { Data *simplejson.Json `json:"data"` } + +type DeleteAnnotationsCmd struct { + AlertId int64 `json:"alertId"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` +} diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 5a324aa1331..d599dd10735 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -145,7 +145,6 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro "hasUpdate": plugins.GrafanaHasUpdate, "env": setting.Env, }, - "alertingEnabled": setting.AlertingEnabled, } return jsonObj, nil diff --git a/pkg/api/gnetproxy.go b/pkg/api/gnetproxy.go index 8c21a0f03a7..7761729b8af 100644 --- a/pkg/api/gnetproxy.go +++ b/pkg/api/gnetproxy.go @@ -36,6 +36,7 @@ func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy { // clear cookie headers req.Header.Del("Cookie") req.Header.Del("Set-Cookie") + req.Header.Del("Authorization") } return &httputil.ReverseProxy{Director: director} diff --git a/pkg/api/index.go b/pkg/api/index.go index 385810b942e..99a5f78f9c9 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -102,7 +102,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Children: dashboardChildNavs, }) - if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) { + if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { alertChildNavs := []*dtos.NavLink{ {Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"}, {Text: "Notifications", Url: setting.AppSubUrl + "/alerting/notifications"}, diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index bc222361b25..bf280776575 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -1,9 +1,17 @@ package api import ( + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/base64" "errors" "fmt" + "io/ioutil" + "log" + "net/http" + "golang.org/x/net/context" "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/bus" @@ -14,6 +22,12 @@ import ( "github.com/grafana/grafana/pkg/social" ) +func GenStateString() string { + rnd := make([]byte, 32) + rand.Read(rnd) + return base64.StdEncoding.EncodeToString(rnd) +} + func OAuthLogin(ctx *middleware.Context) { if setting.OAuthService == nil { ctx.Handle(404, "login.OAuthLogin(oauth service not enabled)", nil) @@ -27,14 +41,63 @@ func OAuthLogin(ctx *middleware.Context) { return } + error := ctx.Query("error") + if error != "" { + errorDesc := ctx.Query("error_description") + ctx.Logger.Info("OAuthLogin Failed", "error", error, "errorDesc", errorDesc) + ctx.Redirect(setting.AppSubUrl + "/login?failCode=1003") + return + } + code := ctx.Query("code") if code == "" { - ctx.Redirect(connect.AuthCodeURL("", oauth2.AccessTypeOnline)) + state := GenStateString() + ctx.Session.Set(middleware.SESS_KEY_OAUTH_STATE, state) + ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline)) + return + } + + // verify state string + savedState := ctx.Session.Get(middleware.SESS_KEY_OAUTH_STATE).(string) + queryState := ctx.Query("state") + if savedState != queryState { + ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil) return } // handle call back - token, err := connect.Exchange(oauth2.NoContext, code) + + // initialize oauth2 context + oauthCtx := oauth2.NoContext + if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" { + cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey) + if err != nil { + log.Fatal(err) + } + + // Load CA cert + caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa) + if err != nil { + log.Fatal(err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + }, + } + sslcli := &http.Client{Transport: tr} + + oauthCtx = context.TODO() + oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli) + } + + // get token from provider + token, err := connect.Exchange(oauthCtx, code) if err != nil { ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err) return @@ -42,7 +105,11 @@ func OAuthLogin(ctx *middleware.Context) { ctx.Logger.Debug("OAuthLogin Got token") - userInfo, err := connect.UserInfo(token) + // set up oauth2 client + client := connect.Client(oauthCtx, token) + + // get user info + userInfo, err := connect.UserInfo(client) if err != nil { if err == social.ErrMissingTeamMembership { ctx.Redirect(setting.AppSubUrl + "/login?failCode=1000") @@ -63,7 +130,7 @@ func OAuthLogin(ctx *middleware.Context) { return } - userQuery := m.GetUserByLoginQuery{LoginOrEmail: userInfo.Email} + userQuery := m.GetUserByEmailQuery{Email: userInfo.Email} err = bus.Dispatch(&userQuery) // create account if missing @@ -82,7 +149,7 @@ func OAuthLogin(ctx *middleware.Context) { return } cmd := m.CreateUserCommand{ - Login: userInfo.Email, + Login: userInfo.Login, Email: userInfo.Email, Name: userInfo.Name, Company: userInfo.Company, diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 0fa6003d67a..1655d14e014 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "net/http" @@ -31,7 +32,7 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response { }) } - resp, err := tsdb.HandleRequest(request) + resp, err := tsdb.HandleRequest(context.TODO(), request) if err != nil { return ApiError(500, "Metric request error", err) } diff --git a/pkg/bus/bus.go b/pkg/bus/bus.go index 6eb4b741a27..55188b2bc73 100644 --- a/pkg/bus/bus.go +++ b/pkg/bus/bus.go @@ -1,18 +1,22 @@ package bus import ( + "context" "fmt" "reflect" ) type HandlerFunc interface{} +type CtxHandlerFunc func() type Msg interface{} type Bus interface { Dispatch(msg Msg) error + DispatchCtx(ctx context.Context, msg Msg) error Publish(msg Msg) error AddHandler(handler HandlerFunc) + AddCtxHandler(handler HandlerFunc) AddEventListener(handler HandlerFunc) AddWildcardListener(handler HandlerFunc) } @@ -34,6 +38,27 @@ func New() Bus { return bus } +func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error { + var msgName = reflect.TypeOf(msg).Elem().Name() + + var handler = b.handlers[msgName] + if handler == nil { + return fmt.Errorf("handler not found for %s", msgName) + } + + var params = make([]reflect.Value, 2) + params[0] = reflect.ValueOf(ctx) + params[1] = reflect.ValueOf(msg) + + ret := reflect.ValueOf(handler).Call(params) + err := ret[0].Interface() + if err == nil { + return nil + } else { + return err.(error) + } +} + func (b *InProcBus) Dispatch(msg Msg) error { var msgName = reflect.TypeOf(msg).Elem().Name() @@ -90,6 +115,12 @@ func (b *InProcBus) AddHandler(handler HandlerFunc) { b.handlers[queryTypeName] = handler } +func (b *InProcBus) AddCtxHandler(handler HandlerFunc) { + handlerType := reflect.TypeOf(handler) + queryTypeName := handlerType.In(1).Elem().Name() + b.handlers[queryTypeName] = handler +} + func (b *InProcBus) AddEventListener(handler HandlerFunc) { handlerType := reflect.TypeOf(handler) eventName := handlerType.In(0).Elem().Name() @@ -105,6 +136,11 @@ func AddHandler(implName string, handler HandlerFunc) { globalBus.AddHandler(handler) } +// Package level functions +func AddCtxHandler(implName string, handler HandlerFunc) { + globalBus.AddCtxHandler(handler) +} + // Package level functions func AddEventListener(handler HandlerFunc) { globalBus.AddEventListener(handler) @@ -118,6 +154,10 @@ func Dispatch(msg Msg) error { return globalBus.Dispatch(msg) } +func DispatchCtx(ctx context.Context, msg Msg) error { + return globalBus.DispatchCtx(ctx, msg) +} + func Publish(msg Msg) error { return globalBus.Publish(msg) } diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index e2ccce8418f..08a4bf4693e 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -35,11 +35,11 @@ func Init(version string) { } func ListAllPlugins(repoUrl string) (m.PluginRepo, error) { - body, err := createRequest(repoUrl, "repo") + body, err := sendRequest(repoUrl, "repo") if err != nil { - logger.Info("Failed to create request", "error", err) - return m.PluginRepo{}, fmt.Errorf("Failed to create request. error: %v", err) + logger.Info("Failed to send request", "error", err) + return m.PluginRepo{}, fmt.Errorf("Failed to send request. error: %v", err) } if err != nil { @@ -112,11 +112,11 @@ func RemoveInstalledPlugin(pluginPath, pluginName string) error { } func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) { - body, err := createRequest(repoUrl, "repo", pluginId) + body, err := sendRequest(repoUrl, "repo", pluginId) if err != nil { - logger.Info("Failed to create request", "error", err) - return m.Plugin{}, fmt.Errorf("Failed to create request. error: %v", err) + logger.Info("Failed to send request", "error", err) + return m.Plugin{}, fmt.Errorf("Failed to send request. error: %v", err) } if err != nil { @@ -133,7 +133,7 @@ func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) { return data, nil } -func createRequest(repoUrl string, subPaths ...string) ([]byte, error) { +func sendRequest(repoUrl string, subPaths ...string) ([]byte, error) { u, _ := url.Parse(repoUrl) for _, v := range subPaths { u.Path = path.Join(u.Path, v) @@ -149,6 +149,13 @@ func createRequest(repoUrl string, subPaths ...string) ([]byte, error) { } res, err := HttpClient.Do(req) + if err != nil { + return []byte{}, err + } + + if res.StatusCode/100 != 2 { + return []byte{}, fmt.Errorf("Api returned invalid status: %s", res.Status) + } body, err := ioutil.ReadAll(res.Body) defer res.Body.Close() diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 6cd063f798f..a0cb0abf886 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -20,6 +20,8 @@ import ( _ "github.com/grafana/grafana/pkg/services/alerting/conditions" _ "github.com/grafana/grafana/pkg/services/alerting/notifiers" _ "github.com/grafana/grafana/pkg/tsdb/graphite" + _ "github.com/grafana/grafana/pkg/tsdb/influxdb" + _ "github.com/grafana/grafana/pkg/tsdb/opentsdb" _ "github.com/grafana/grafana/pkg/tsdb/prometheus" _ "github.com/grafana/grafana/pkg/tsdb/testdata" ) @@ -101,8 +103,10 @@ func writePIDFile() { func listenToSystemSignals(server models.GrafanaServer) { signalChan := make(chan os.Signal, 1) + ignoreChan := make(chan os.Signal, 1) code := 0 + signal.Notify(ignoreChan, syscall.SIGHUP) signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM) select { diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 8a2ab0c2b95..2a4682cbb4b 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -7,6 +7,8 @@ import ( "os" "time" + "gopkg.in/macaron.v1" + "golang.org/x/sync/errgroup" "github.com/grafana/grafana/pkg/api" @@ -57,7 +59,7 @@ func (g *GrafanaServerImpl) Start() { plugins.Init() // init alerting - if setting.AlertingEnabled { + if setting.ExecuteAlerts { engine := alerting.NewEngine() g.childRoutines.Go(func() error { return engine.Run(g.context) }) } @@ -89,7 +91,7 @@ func (g *GrafanaServerImpl) startHttpServer() { case setting.HTTP: err = http.ListenAndServe(listenAddr, m) case setting.HTTPS: - err = http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m) + err = ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m) default: g.log.Error("Invalid protocol", "protocol", setting.Protocol) g.Shutdown(1, "Startup failed") @@ -113,6 +115,26 @@ func (g *GrafanaServerImpl) Shutdown(code int, reason string) { os.Exit(code) } +func ListenAndServeTLS(listenAddr, certfile, keyfile string, m *macaron.Macaron) error { + if certfile == "" { + return fmt.Errorf("cert_file cannot be empty when using HTTPS") + } + + if keyfile == "" { + return fmt.Errorf("cert_key cannot be empty when using HTTPS") + } + + if _, err := os.Stat(setting.CertFile); os.IsNotExist(err) { + return fmt.Errorf(`Cannot find SSL cert_file at %v`, setting.CertFile) + } + + if _, err := os.Stat(setting.KeyFile); os.IsNotExist(err) { + return fmt.Errorf(`Cannot find SSL key_file at %v`, setting.KeyFile) + } + + return http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m) +} + // implement context.Context func (g *GrafanaServerImpl) Deadline() (deadline time.Time, ok bool) { return g.context.Deadline() diff --git a/pkg/components/imguploader/imguploader.go b/pkg/components/imguploader/imguploader.go index 1de46e7fd1d..1cbe55c8572 100644 --- a/pkg/components/imguploader/imguploader.go +++ b/pkg/components/imguploader/imguploader.go @@ -10,6 +10,13 @@ type ImageUploader interface { Upload(path string) (string, error) } +type NopImageUploader struct { +} + +func (NopImageUploader) Upload(path string) (string, error) { + return "", nil +} + func NewImageUploader() (ImageUploader, error) { switch setting.ImageUploadProvider { @@ -53,5 +60,5 @@ func NewImageUploader() (ImageUploader, error) { return NewWebdavImageUploader(url, username, password) } - return nil, fmt.Errorf("could not find specified provider") + return NopImageUploader{}, nil } diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index d8c916bb765..97a34f129b1 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" ) type ldapAuther struct { @@ -29,7 +30,7 @@ func (a *ldapAuther) Dial() error { var err error var certPool *x509.CertPool if a.server.RootCACert != "" { - certPool := x509.NewCertPool() + certPool = x509.NewCertPool() for _, caCertFile := range strings.Split(a.server.RootCACert, " ") { if pem, err := ioutil.ReadFile(caCertFile); err != nil { return err @@ -132,8 +133,10 @@ func (a *ldapAuther) getGrafanaUserFor(ldapUser *ldapUserInfo) (*m.User, error) // get user from grafana db userQuery := m.GetUserByLoginQuery{LoginOrEmail: ldapUser.Username} if err := bus.Dispatch(&userQuery); err != nil { - if err == m.ErrUserNotFound { + if err == m.ErrUserNotFound && setting.LdapAllowSignup { return a.createGrafanaUser(ldapUser) + } else if err == m.ErrUserNotFound { + return nil, ErrInvalidCredentials } else { return nil, err } diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index ee6462be37a..d575189f4de 100644 --- a/pkg/middleware/session.go +++ b/pkg/middleware/session.go @@ -13,6 +13,7 @@ import ( const ( SESS_KEY_USERID = "uid" + SESS_KEY_OAUTH_STATE = "state" ) var sessionManager *session.Manager diff --git a/pkg/models/alert.go b/pkg/models/alert.go index 938be8ddbd4..7531be90e88 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -8,6 +8,7 @@ import ( type AlertStateType string type AlertSeverityType string +type NoDataOption string const ( AlertStateNoData AlertStateType = "no_data" @@ -17,10 +18,25 @@ const ( AlertStateOK AlertStateType = "ok" ) +const ( + NoDataSetNoData NoDataOption = "no_data" + NoDataSetAlerting NoDataOption = "alerting" + NoDataSetOK NoDataOption = "ok" + NoDataKeepState NoDataOption = "keep_state" +) + func (s AlertStateType) IsValid() bool { return s == AlertStateOK || s == AlertStateNoData || s == AlertStateExecError || s == AlertStatePaused } +func (s NoDataOption) IsValid() bool { + return s == NoDataSetNoData || s == NoDataSetAlerting || s == NoDataSetOK || s == NoDataKeepState +} + +func (s NoDataOption) ToAlertState() AlertStateType { + return AlertStateType(s) +} + type Alert struct { Id int64 Version int64 @@ -101,6 +117,12 @@ type SaveAlertsCommand struct { Alerts []*Alert } +type PauseAlertCommand struct { + OrgId int64 + AlertId int64 + Paused bool +} + type SetAlertStateCommand struct { AlertId int64 OrgId int64 diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 794266ba71e..883cc3a90bd 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -22,7 +22,8 @@ const ( // Typed errors var ( - ErrDataSourceNotFound = errors.New("Data source not found") + ErrDataSourceNotFound = errors.New("Data source not found") + ErrDataSourceNameExists = errors.New("Data source with same name already exists") ) type DsAccess string diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go index d357b9cf562..80803b3be08 100644 --- a/pkg/models/notifications.go +++ b/pkg/models/notifications.go @@ -5,18 +5,31 @@ import "errors" var ErrInvalidEmailCode = errors.New("Invalid or expired email code") type SendEmailCommand struct { - To []string - Template string - Data map[string]interface{} - Massive bool - Info string + To []string + Template string + Data map[string]interface{} + Info string + EmbededFiles []string +} + +type SendEmailCommandSync struct { + SendEmailCommand } type SendWebhook struct { - Url string - User string - Password string - Body string + Url string + User string + Password string + Body string + HttpMethod string +} + +type SendWebhookSync struct { + Url string + User string + Password string + Body string + HttpMethod string } type SendResetPasswordEmailCommand struct { diff --git a/pkg/models/user.go b/pkg/models/user.go index d2dcdf0a5c9..1f99f866c86 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -96,6 +96,11 @@ type GetUserByLoginQuery struct { Result *User } +type GetUserByEmailQuery struct { + Email string + Result *User +} + type GetUserByIdQuery struct { Id int64 Result *User diff --git a/pkg/services/alerting/conditions/evaluator.go b/pkg/services/alerting/conditions/evaluator.go index 1c154e17ec2..19c268f2671 100644 --- a/pkg/services/alerting/conditions/evaluator.go +++ b/pkg/services/alerting/conditions/evaluator.go @@ -28,7 +28,7 @@ type ThresholdEvaluator struct { Threshold float64 } -func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvaluator, error) { +func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvaluator, error) { params := model.Get("params").MustArray() if len(params) == 0 { return nil, alerting.ValidationError{Reason: "Evaluator missing threshold parameter"} @@ -111,7 +111,7 @@ func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) { } if inSlice(typ, defaultTypes) { - return newThresholdEvaludator(typ, model) + return newThresholdEvaluator(typ, model) } if inSlice(typ, rangedTypes) { @@ -122,7 +122,7 @@ func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) { return &NoDataEvaluator{}, nil } - return nil, alerting.ValidationError{Reason: "Evaludator invalid evaluator type"} + return nil, alerting.ValidationError{Reason: "Evaluator invalid evaluator type: " + typ} } func inSlice(a string, list []string) bool { diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index b5300a261a3..a9a99ba919e 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -82,7 +82,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange * req := c.getRequestForAlertRule(getDsInfo.Result, timeRange) result := make(tsdb.TimeSeriesSlice, 0) - resp, err := c.HandleRequest(req) + resp, err := c.HandleRequest(context.Ctx, req) if err != nil { return nil, fmt.Errorf("tsdb.HandleRequest() error %v", err) } @@ -123,6 +123,7 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRa BasicAuth: datasource.BasicAuth, BasicAuthUser: datasource.BasicAuthUser, BasicAuthPassword: datasource.BasicAuthPassword, + JsonData: datasource.JsonData, }, }, }, diff --git a/pkg/services/alerting/conditions/query_test.go b/pkg/services/alerting/conditions/query_test.go index 51c4226f81c..43e0381a80c 100644 --- a/pkg/services/alerting/conditions/query_test.go +++ b/pkg/services/alerting/conditions/query_test.go @@ -1,6 +1,7 @@ package conditions import ( + "context" "testing" null "gopkg.in/guregu/null.v3" @@ -137,7 +138,7 @@ func (ctx *queryConditionTestContext) exec() { ctx.condition = condition - condition.HandleRequest = func(req *tsdb.Request) (*tsdb.Response, error) { + condition.HandleRequest = func(context context.Context, req *tsdb.Request) (*tsdb.Response, error) { return &tsdb.Response{ Results: map[string]*tsdb.QueryResult{ "A": {Series: ctx.series}, diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index 67765f9c310..198a52b746a 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -3,6 +3,8 @@ package conditions import ( "testing" + "gopkg.in/guregu/null.v3" + "github.com/grafana/grafana/pkg/tsdb" . "github.com/smartystreets/goconvey/convey" ) @@ -43,7 +45,7 @@ func testReducer(typ string, datapoints ...float64) float64 { } for idx := range datapoints { - series.Points = append(series.Points, tsdb.NewTimePoint(datapoints[idx], 1234134)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(datapoints[idx]), 1234134)) } return reducer.Reduce(series).Float64 diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 10b8af64119..b99b7506614 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -11,7 +11,6 @@ import ( type Engine struct { execQueue chan *Job - resultQueue chan *EvalContext clock clock.Clock ticker *Ticker scheduler Scheduler @@ -25,7 +24,6 @@ func NewEngine() *Engine { e := &Engine{ ticker: NewTicker(time.Now(), time.Second*0, clock.New()), execQueue: make(chan *Job, 1000), - resultQueue: make(chan *EvalContext, 1000), scheduler: NewScheduler(), evalHandler: NewEvalHandler(), ruleReader: NewRuleReader(), @@ -39,23 +37,17 @@ func NewEngine() *Engine { func (e *Engine) Run(ctx context.Context) error { e.log.Info("Initializing Alerting") - g, ctx := errgroup.WithContext(ctx) + alertGroup, ctx := errgroup.WithContext(ctx) - g.Go(func() error { return e.alertingTicker(ctx) }) - g.Go(func() error { return e.execDispatcher(ctx) }) - g.Go(func() error { return e.resultDispatcher(ctx) }) + alertGroup.Go(func() error { return e.alertingTicker(ctx) }) + alertGroup.Go(func() error { return e.runJobDispatcher(ctx) }) - err := g.Wait() + err := alertGroup.Wait() e.log.Info("Stopped Alerting", "reason", err) return err } -func (e *Engine) Stop() { - close(e.execQueue) - close(e.resultQueue) -} - func (e *Engine) alertingTicker(grafanaCtx context.Context) error { defer func() { if err := recover(); err != nil { @@ -81,69 +73,65 @@ func (e *Engine) alertingTicker(grafanaCtx context.Context) error { } } -func (e *Engine) execDispatcher(grafanaCtx context.Context) error { +func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error { + dispatcherGroup, alertCtx := errgroup.WithContext(grafanaCtx) + for { select { case <-grafanaCtx.Done(): - close(e.resultQueue) - return grafanaCtx.Err() + return dispatcherGroup.Wait() case job := <-e.execQueue: - go e.executeJob(grafanaCtx, job) + dispatcherGroup.Go(func() error { return e.processJob(alertCtx, job) }) } } } -func (e *Engine) executeJob(grafanaCtx context.Context, job *Job) error { +var ( + unfinishedWorkTimeout time.Duration = time.Second * 5 + alertTimeout time.Duration = time.Second * 30 +) + +func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error { defer func() { if err := recover(); err != nil { - e.log.Error("Execute Alert Panic", "error", err, "stack", log.Stack(1)) + e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1)) } }() - done := make(chan *EvalContext, 1) + alertCtx, cancelFn := context.WithTimeout(context.TODO(), alertTimeout) + + job.Running = true + evalContext := NewEvalContext(alertCtx, job.Rule) + + done := make(chan struct{}) + go func() { - job.Running = true - context := NewEvalContext(job.Rule) - e.evalHandler.Eval(context) - job.Running = false - done <- context + defer func() { + if err := recover(); err != nil { + e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1)) + close(done) + } + }() + + e.evalHandler.Eval(evalContext) + e.resultHandler.Handle(evalContext) close(done) }() + var err error = nil select { - case <-grafanaCtx.Done(): - return grafanaCtx.Err() - case evalContext := <-done: - e.resultQueue <- evalContext - } - - return nil -} - -func (e *Engine) resultDispatcher(grafanaCtx context.Context) error { - for { select { - case <-grafanaCtx.Done(): - //handle all responses before shutting down. - for result := range e.resultQueue { - e.handleResponse(result) - } - - return grafanaCtx.Err() - case result := <-e.resultQueue: - e.handleResponse(result) + case <-time.After(unfinishedWorkTimeout): + cancelFn() + err = grafanaCtx.Err() + case <-done: } + case <-done: } -} -func (e *Engine) handleResponse(result *EvalContext) { - defer func() { - if err := recover(); err != nil { - e.log.Error("Panic in resultDispatcher", "error", err, "stack", log.Stack(1)) - } - }() - - e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing) - e.resultHandler.Handle(result) + e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing) + job.Running = false + cancelFn() + return err } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index a76ed8d519f..cf698d8f7a4 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -1,6 +1,7 @@ package alerting import ( + "context" "fmt" "time" @@ -20,14 +21,13 @@ type EvalContext struct { StartTime time.Time EndTime time.Time Rule *Rule - DoneChan chan bool - CancelChan chan bool log log.Logger dashboardSlug string ImagePublicUrl string ImageOnDiskPath string NoDataFound bool - RetryCount int + + Ctx context.Context } type StateDescription struct { @@ -86,6 +86,10 @@ func (c *EvalContext) GetDashboardSlug() (string, error) { } func (c *EvalContext) GetRuleUrl() (string, error) { + if c.IsTestRun { + return setting.AppUrl, nil + } + if slug, err := c.GetDashboardSlug(); err != nil { return "", err } else { @@ -94,15 +98,13 @@ func (c *EvalContext) GetRuleUrl() (string, error) { } } -func NewEvalContext(rule *Rule) *EvalContext { +func NewEvalContext(alertCtx context.Context, rule *Rule) *EvalContext { return &EvalContext{ + Ctx: alertCtx, StartTime: time.Now(), Rule: rule, Logs: make([]*ResultLogEntry, 0), EvalMatches: make([]*EvalMatch, 0), - DoneChan: make(chan bool, 1), - CancelChan: make(chan bool, 1), log: log.New("alerting.evalContext"), - RetryCount: 0, } } diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index a5599b96d2c..74054ba8191 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -1,17 +1,12 @@ package alerting import ( - "fmt" "time" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" ) -var ( - MaxRetries int = 1 -) - type DefaultEvalHandler struct { log log.Logger alertJobTimeout time.Duration @@ -20,49 +15,11 @@ type DefaultEvalHandler struct { func NewEvalHandler() *DefaultEvalHandler { return &DefaultEvalHandler{ log: log.New("alerting.evalHandler"), - alertJobTimeout: time.Second * 15, + alertJobTimeout: time.Second * 5, } } func (e *DefaultEvalHandler) Eval(context *EvalContext) { - go e.eval(context) - - select { - case <-time.After(e.alertJobTimeout): - context.Error = fmt.Errorf("Execution timed out after %v", e.alertJobTimeout) - context.EndTime = time.Now() - e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id, "timeout setting", e.alertJobTimeout) - e.retry(context) - case <-context.DoneChan: - e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing) - - if context.Error != nil { - e.retry(context) - } - } -} - -func (e *DefaultEvalHandler) retry(context *EvalContext) { - e.log.Debug("Retrying eval exeuction", "alertId", context.Rule.Id) - - if context.RetryCount < MaxRetries { - context.DoneChan = make(chan bool, 1) - context.CancelChan = make(chan bool, 1) - context.RetryCount++ - e.Eval(context) - } -} - -func (e *DefaultEvalHandler) eval(context *EvalContext) { - defer func() { - if err := recover(); err != nil { - e.log.Error("Alerting rule eval panic", "error", err, "stack", log.Stack(1)) - if panicErr, ok := err.(error); ok { - context.Error = panicErr - } - } - }() - for _, condition := range context.Rule.Conditions { condition.Eval(context) @@ -80,5 +37,4 @@ func (e *DefaultEvalHandler) eval(context *EvalContext) { context.EndTime = time.Now() elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond metrics.M_Alerting_Exeuction_Time.Update(elapsedTime) - context.DoneChan <- true } diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index ae5b4e4501d..b69e62f9622 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -1,6 +1,7 @@ package alerting import ( + "context" "testing" . "github.com/smartystreets/goconvey/convey" @@ -19,25 +20,25 @@ func TestAlertingExecutor(t *testing.T) { handler := NewEvalHandler() Convey("Show return triggered with single passing condition", func() { - context := NewEvalContext(&Rule{ + context := NewEvalContext(context.TODO(), &Rule{ Conditions: []Condition{&conditionStub{ firing: true, }}, }) - handler.eval(context) + handler.Eval(context) So(context.Firing, ShouldEqual, true) }) Convey("Show return false with not passing condition", func() { - context := NewEvalContext(&Rule{ + context := NewEvalContext(context.TODO(), &Rule{ Conditions: []Condition{ &conditionStub{firing: true}, &conditionStub{firing: false}, }, }) - handler.eval(context) + handler.Eval(context) So(context.Firing, ShouldEqual, false) }) }) diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 6a8be4f6deb..d78e84f6974 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -80,6 +80,11 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { continue } + frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString()) + if err != nil { + return nil, ValidationError{Reason: "Could not parse frequency"} + } + alert := &m.Alert{ DashboardId: e.Dash.Id, OrgId: e.OrgId, @@ -88,7 +93,7 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { Name: jsonAlert.Get("name").MustString(), Handler: jsonAlert.Get("handler").MustInt64(), Message: jsonAlert.Get("message").MustString(), - Frequency: getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString()), + Frequency: frequency, } for _, condition := range jsonAlert.Get("conditions").MustArray() { @@ -115,13 +120,17 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id) } + if interval, err := panel.Get("interval").String(); err == nil { + panelQuery.Set("interval", interval) + } + jsonQuery.Set("model", panelQuery.Interface()) } alert.Settings = jsonAlert // validate - _, err := NewRuleFromDBAlert(alert) + _, err = NewRuleFromDBAlert(alert) if err == nil && alert.ValidToSave() { alerts = append(alerts, alert) } else { diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index f82210f1b1a..e72e6d938d8 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" ) @@ -17,8 +18,35 @@ func TestAlertRuleExtraction(t *testing.T) { return &FakeCondition{}, nil }) - Convey("Parsing and validating alerts from dashboards", func() { - json := `{ + setting.NewConfigContext(&setting.CommandLineArgs{ + HomePath: "../../../", + }) + + // mock data + 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"} + + bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error { + query.Result = []*m.DataSource{defaultDs, graphite2Ds} + return nil + }) + + bus.AddHandler("test", func(query *m.GetDataSourceByNameQuery) error { + if query.Name == defaultDs.Name { + query.Result = defaultDs + } + if query.Name == graphite2Ds.Name { + query.Result = graphite2Ds + } + if query.Name == influxDBDs.Name { + query.Result = influxDBDs + } + return nil + }) + + json := ` + { "id": 57, "title": "Graphite 4", "originalTitle": "Graphite 4", @@ -80,32 +108,16 @@ func TestAlertRuleExtraction(t *testing.T) { ] } ] - }` + }` + + Convey("Parsing and validating dashboard containing graphite alerts", func() { + dashJson, err := simplejson.NewJson([]byte(json)) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) extractor := NewDashAlertExtractor(dash, 1) - // mock data - defaultDs := &m.DataSource{Id: 12, OrgId: 2, Name: "I am default", IsDefault: true} - graphite2Ds := &m.DataSource{Id: 15, OrgId: 2, Name: "graphite2"} - - bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error { - query.Result = []*m.DataSource{defaultDs, graphite2Ds} - return nil - }) - - bus.AddHandler("test", func(query *m.GetDataSourceByNameQuery) error { - if query.Name == defaultDs.Name { - query.Result = defaultDs - } - if query.Name == graphite2Ds.Name { - query.Result = graphite2Ds - } - return nil - }) - alerts, err := extractor.GetAlerts() Convey("Get rules without error", func() { @@ -119,6 +131,9 @@ func TestAlertRuleExtraction(t *testing.T) { So(v.DashboardId, ShouldEqual, 57) So(v.Name, ShouldNotBeEmpty) So(v.Message, ShouldNotBeEmpty) + + settings := simplejson.NewFromAny(v.Settings) + So(settings.Get("interval").MustString(""), ShouldEqual, "") } Convey("should extract handler property", func() { @@ -156,5 +171,317 @@ func TestAlertRuleExtraction(t *testing.T) { }) }) }) + + Convey("Parse and validate dashboard containing influxdb alert", func() { + + json2 := `{ + "id": 4, + "title": "Influxdb", + "tags": [ + "apa" + ], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "sharedCrosshair": false, + "rows": [ + { + "collapse": false, + "editable": true, + "height": "450px", + "panels": [ + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 10 + ], + "type": "gt" + }, + "query": { + "params": [ + "B", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "frequency": "3s", + "handler": 1, + "name": "Influxdb", + "noDataState": "no_data", + "notifications": [ + { + "id": 6 + } + ] + }, + "alerting": {}, + "aliasColors": { + "logins.count.count": "#890F02" + }, + "bars": false, + "datasource": "InfluxDB", + "editable": true, + "error": false, + "fill": 1, + "grid": {}, + "id": 1, + "interval": ">10s", + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$interval" + ], + "type": "time" + }, + { + "params": [ + "datacenter" + ], + "type": "tag" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "hide": false, + "measurement": "logins.count", + "policy": "default", + "query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)", + "rawQuery": true, + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "count" + } + ] + ], + "tags": [] + }, + { + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "hide": true, + "measurement": "cpu", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ], + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "sum" + } + ] + ], + "tags": [] + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 10 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel Title", + "tooltip": { + "msResolution": false, + "ordering": "alphabetical", + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "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 + } + ] + }, + { + "editable": true, + "error": false, + "id": 2, + "isNew": true, + "limit": 10, + "links": [], + "show": "current", + "span": 2, + "stateFilter": [ + "alerting" + ], + "title": "Alert status", + "type": "alertlist" + } + ], + "title": "Row" + } + ], + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": { + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "schemaVersion": 13, + "version": 120, + "links": [], + "gnetId": null + }` + + dashJson, err := simplejson.NewJson([]byte(json2)) + 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 read interval", func() { + So(len(alerts), ShouldEqual, 1) + + for _, alert := range alerts { + So(alert.DashboardId, ShouldEqual, 4) + + conditions := alert.Settings.Get("conditions").MustArray() + cond := simplejson.NewFromAny(conditions[0]) + + So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s") + } + }) + }) }) } diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 78ffc280375..583e12a120d 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -1,11 +1,9 @@ package alerting -import ( - "time" -) +import "time" type EvalHandler interface { - Eval(context *EvalContext) + Eval(evalContext *EvalContext) } type Scheduler interface { @@ -14,10 +12,13 @@ type Scheduler interface { } type Notifier interface { - Notify(alertResult *EvalContext) + Notify(evalContext *EvalContext) error GetType() string NeedsImage() bool PassesFilter(rule *Rule) bool + + GetNotifierId() int64 + GetIsDefault() bool } type Condition interface { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 39569a94bf1..2017d9d7670 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" + "golang.org/x/sync/errgroup" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" "github.com/grafana/grafana/pkg/components/renderer" @@ -33,32 +35,45 @@ func (n *RootNotifier) PassesFilter(rule *Rule) bool { return false } -func (n *RootNotifier) Notify(context *EvalContext) { - n.log.Info("Sending notifications for", "ruleId", context.Rule.Id) +func (n *RootNotifier) GetNotifierId() int64 { + return 0 +} +func (n *RootNotifier) GetIsDefault() bool { + return false +} + +func (n *RootNotifier) Notify(context *EvalContext) error { notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context) if err != nil { - n.log.Error("Failed to read notifications", "error", err) - return + return err } + n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "Amount to send", len(notifiers)) + if len(notifiers) == 0 { - return + return nil } err = n.uploadImage(context) if err != nil { n.log.Error("Failed to upload alert panel image", "error", err) + return err } - n.sendNotifications(notifiers, context) + return n.sendNotifications(context, notifiers) } -func (n *RootNotifier) sendNotifications(notifiers []Notifier, context *EvalContext) { +func (n *RootNotifier) sendNotifications(context *EvalContext, notifiers []Notifier) error { + g, _ := errgroup.WithContext(context.Ctx) + for _, notifier := range notifiers { - n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType()) - go notifier.Notify(context) + not := notifier //avoid updating scope variable in go routine + n.log.Info("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) + g.Go(func() error { return not.Notify(context) }) } + + return g.Wait() } func (n *RootNotifier) uploadImage(context *EvalContext) (err error) { diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index c854d8475b5..5e378dec890 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -22,7 +22,15 @@ func (fn *FakeNotifier) NeedsImage() bool { return true } -func (fn *FakeNotifier) Notify(alertResult *EvalContext) {} +func (n *FakeNotifier) GetNotifierId() int64 { + return 0 +} + +func (n *FakeNotifier) GetIsDefault() bool { + return false +} + +func (fn *FakeNotifier) Notify(alertResult *EvalContext) error { return nil } func (fn *FakeNotifier) PassesFilter(rule *Rule) bool { return fn.FakeMatchResult diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 27c2a625d17..f1e748207d8 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -6,13 +6,19 @@ import ( ) type NotifierBase struct { - Name string - Type string + Name string + Type string + Id int64 + IsDeault bool } -func NewNotifierBase(name, notifierType string, model *simplejson.Json) NotifierBase { - base := NotifierBase{Name: name, Type: notifierType} - return base +func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase { + return NotifierBase{ + Id: id, + Name: name, + IsDeault: isDefault, + Type: notifierType, + } } func (n *NotifierBase) PassesFilter(rule *alerting.Rule) bool { @@ -26,3 +32,11 @@ func (n *NotifierBase) GetType() string { func (n *NotifierBase) NeedsImage() bool { return true } + +func (n *NotifierBase) GetNotifierId() int64 { + return n.Id +} + +func (n *NotifierBase) GetIsDefault() bool { + return n.IsDeault +} diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go deleted file mode 100644 index 8cfc1ec3ae9..00000000000 --- a/pkg/services/alerting/notifiers/base_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package notifiers - -// import . "github.com/smartystreets/goconvey/convey" -// -// func TestBaseNotifier( t *testing.T ) { -// Convey("Parsing base notification state", t, func() { -// -// Convey("matches", func() { -// json := ` -// { -// "states": "critical" -// }` -// -// settingsJSON, _ := simplejson.NewJson([]byte(json)) -// not := NewNotifierBase("ops", "email", settingsJSON) -// So(not.MatchSeverity(m.AlertSeverityCritical), ShouldBeTrue) -// }) -// -// Convey("does not match", func() { -// json := ` -// { -// "severityFilter": "critical" -// }` -// -// settingsJSON, _ := simplejson.NewJson([]byte(json)) -// not := NewNotifierBase("ops", "email", settingsJSON) -// So(not.MatchSeverity(m.AlertSeverityWarning), ShouldBeFalse) -// }) -// }) -// } diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index eccd3ce9dfb..9b510cd3275 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -1,6 +1,7 @@ package notifiers import ( + "os" "strings" "github.com/grafana/grafana/pkg/bus" @@ -28,40 +29,67 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) { return nil, alerting.ValidationError{Reason: "Could not find addresses in settings"} } + // split addresses with a few different ways + addresses := strings.FieldsFunc(addressesString, func(r rune) bool { + switch r { + case ',', ';', '\n': + return true + } + return false + }) + return &EmailNotifier{ - NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings), - Addresses: strings.Split(addressesString, "\n"), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + Addresses: addresses, log: log.New("alerting.notifier.email"), }, nil } -func (this *EmailNotifier) Notify(context *alerting.EvalContext) { +func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Sending alert notification to", "addresses", this.Addresses) metrics.M_Alerting_Notification_Sent_Email.Inc(1) - ruleUrl, err := context.GetRuleUrl() + ruleUrl, err := evalContext.GetRuleUrl() if err != nil { this.log.Error("Failed get rule link", "error", err) - return + return err } - cmd := &m.SendEmailCommand{ - Data: map[string]interface{}{ - "Title": context.GetNotificationTitle(), - "State": context.Rule.State, - "Name": context.Rule.Name, - "StateModel": context.GetStateModel(), - "Message": context.Rule.Message, - "RuleUrl": ruleUrl, - "ImageLink": context.ImagePublicUrl, - "AlertPageUrl": setting.AppUrl + "alerting", - "EvalMatches": context.EvalMatches, + cmd := &m.SendEmailCommandSync{ + SendEmailCommand: m.SendEmailCommand{ + Data: map[string]interface{}{ + "Title": evalContext.GetNotificationTitle(), + "State": evalContext.Rule.State, + "Name": evalContext.Rule.Name, + "StateModel": evalContext.GetStateModel(), + "Message": evalContext.Rule.Message, + "RuleUrl": ruleUrl, + "ImageLink": "", + "EmbededImage": "", + "AlertPageUrl": setting.AppUrl + "alerting", + "EvalMatches": evalContext.EvalMatches, + }, + To: this.Addresses, + Template: "alert_notification.html", + EmbededFiles: []string{}, }, - To: this.Addresses, - Template: "alert_notification.html", } - if err := bus.Dispatch(cmd); err != nil { + if evalContext.ImagePublicUrl != "" { + cmd.Data["ImageLink"] = evalContext.ImagePublicUrl + } else { + file, err := os.Stat(evalContext.ImageOnDiskPath) + if err == nil { + cmd.EmbededFiles = []string{evalContext.ImageOnDiskPath} + cmd.Data["EmbededImage"] = file.Name() + } + } + + err = bus.DispatchCtx(evalContext.Ctx, cmd) + + if err != nil { this.log.Error("Failed to send alert notification email", "error", err) } + return nil + } diff --git a/pkg/services/alerting/notifiers/email_test.go b/pkg/services/alerting/notifiers/email_test.go index 19dcf23c3d2..9750cbc2833 100644 --- a/pkg/services/alerting/notifiers/email_test.go +++ b/pkg/services/alerting/notifiers/email_test.go @@ -47,6 +47,33 @@ func TestEmailNotifier(t *testing.T) { So(emailNotifier.Type, ShouldEqual, "email") So(emailNotifier.Addresses[0], ShouldEqual, "ops@grafana.org") }) + + Convey("from settings with two emails", func() { + json := ` + { + "addresses": "ops@grafana.org;dev@grafana.org" + }` + + settingsJSON, err := simplejson.NewJson([]byte(json)) + So(err, ShouldBeNil) + + model := &m.AlertNotification{ + Name: "ops", + Type: "email", + Settings: settingsJSON, + } + + not, err := NewEmailNotifier(model) + emailNotifier := not.(*EmailNotifier) + + So(err, ShouldBeNil) + So(emailNotifier.Name, ShouldEqual, "ops") + So(emailNotifier.Type, ShouldEqual, "email") + So(len(emailNotifier.Addresses), ShouldEqual, 2) + + So(emailNotifier.Addresses[0], ShouldEqual, "ops@grafana.org") + So(emailNotifier.Addresses[1], ShouldEqual, "dev@grafana.org") + }) }) }) } diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index d0d67ca5a88..7238af179d7 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -23,7 +23,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &SlackNotifier{ - NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), Url: url, log: log.New("alerting.notifier.slack"), }, nil @@ -35,19 +35,19 @@ type SlackNotifier struct { log log.Logger } -func (this *SlackNotifier) Notify(context *alerting.EvalContext) { - this.log.Info("Executing slack notification", "ruleId", context.Rule.Id, "notification", this.Name) +func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { + this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) metrics.M_Alerting_Notification_Sent_Slack.Inc(1) - ruleUrl, err := context.GetRuleUrl() + ruleUrl, err := evalContext.GetRuleUrl() if err != nil { this.log.Error("Failed get rule link", "error", err) - return + return err } fields := make([]map[string]interface{}, 0) fieldLimitCount := 4 - for index, evt := range context.EvalMatches { + for index, evt := range evalContext.EvalMatches { fields = append(fields, map[string]interface{}{ "title": evt.Metric, "value": evt.Value, @@ -58,44 +58,41 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) { } } - if context.Error != nil { + if evalContext.Error != nil { fields = append(fields, map[string]interface{}{ "title": "Error message", - "value": context.Error.Error(), + "value": evalContext.Error.Error(), "short": false, }) } message := "" - if context.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok. - message = context.Rule.Message + if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok. + message = evalContext.Rule.Message } body := map[string]interface{}{ "attachments": []map[string]interface{}{ { - "color": context.GetStateModel().Color, - "title": context.GetNotificationTitle(), + "color": evalContext.GetStateModel().Color, + "title": evalContext.GetNotificationTitle(), "title_link": ruleUrl, "text": message, "fields": fields, - "image_url": context.ImagePublicUrl, + "image_url": evalContext.ImagePublicUrl, "footer": "Grafana v" + setting.BuildVersion, "footer_icon": "http://grafana.org/assets/img/fav32.png", "ts": time.Now().Unix(), - //"pretext": "Optional text that appears above the attachment block", - // "author_name": "Bobby Tables", - // "author_link": "http://flickr.com/bobby/", - // "author_icon": "http://flickr.com/icons/bobby.jpg", - // "thumb_url": "http://example.com/path/to/thumb.png", }, }, } data, _ := json.Marshal(&body) - cmd := &m.SendWebhook{Url: this.Url, Body: string(data)} + cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)} - if err := bus.Dispatch(cmd); err != nil { + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name) } + + return nil } diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 320f273eddc..fb236c91c13 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -20,52 +20,57 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &WebhookNotifier{ - NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), Url: url, User: model.Settings.Get("user").MustString(), Password: model.Settings.Get("password").MustString(), + HttpMethod: model.Settings.Get("httpMethod").MustString("POST"), log: log.New("alerting.notifier.webhook"), }, nil } type WebhookNotifier struct { NotifierBase - Url string - User string - Password string - log log.Logger + Url string + User string + Password string + HttpMethod string + log log.Logger } -func (this *WebhookNotifier) Notify(context *alerting.EvalContext) { +func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Sending webhook") metrics.M_Alerting_Notification_Sent_Webhook.Inc(1) bodyJSON := simplejson.New() - bodyJSON.Set("title", context.GetNotificationTitle()) - bodyJSON.Set("ruleId", context.Rule.Id) - bodyJSON.Set("ruleName", context.Rule.Name) - bodyJSON.Set("state", context.Rule.State) - bodyJSON.Set("evalMatches", context.EvalMatches) + bodyJSON.Set("title", evalContext.GetNotificationTitle()) + bodyJSON.Set("ruleId", evalContext.Rule.Id) + bodyJSON.Set("ruleName", evalContext.Rule.Name) + bodyJSON.Set("state", evalContext.Rule.State) + bodyJSON.Set("evalMatches", evalContext.EvalMatches) - ruleUrl, err := context.GetRuleUrl() + ruleUrl, err := evalContext.GetRuleUrl() if err == nil { bodyJSON.Set("rule_url", ruleUrl) } - if context.ImagePublicUrl != "" { - bodyJSON.Set("image_url", context.ImagePublicUrl) + if evalContext.ImagePublicUrl != "" { + bodyJSON.Set("image_url", evalContext.ImagePublicUrl) } body, _ := bodyJSON.MarshalJSON() - cmd := &m.SendWebhook{ - Url: this.Url, - User: this.User, - Password: this.Password, - Body: string(body), + cmd := &m.SendWebhookSync{ + Url: this.Url, + User: this.User, + Password: this.Password, + Body: string(body), + HttpMethod: this.HttpMethod, } - if err := bus.Dispatch(cmd); err != nil { + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name) } + + return nil } diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index bb9f46d6084..d786e8d599d 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -12,7 +12,7 @@ import ( ) type ResultHandler interface { - Handle(ctx *EvalContext) + Handle(evalContext *EvalContext) error } type DefaultResultHandler struct { @@ -27,37 +27,38 @@ func NewResultHandler() *DefaultResultHandler { } } -func (handler *DefaultResultHandler) Handle(ctx *EvalContext) { - oldState := ctx.Rule.State +func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { + oldState := evalContext.Rule.State - exeuctionError := "" + executionError := "" annotationData := simplejson.New() - if ctx.Error != nil { - handler.log.Error("Alert Rule Result Error", "ruleId", ctx.Rule.Id, "error", ctx.Error) - ctx.Rule.State = m.AlertStateExecError - exeuctionError = ctx.Error.Error() - annotationData.Set("errorMessage", exeuctionError) - } else if ctx.Firing { - ctx.Rule.State = m.AlertStateAlerting - annotationData = simplejson.NewFromAny(ctx.EvalMatches) + if evalContext.Error != nil { + handler.log.Error("Alert Rule Result Error", "ruleId", evalContext.Rule.Id, "error", evalContext.Error) + evalContext.Rule.State = m.AlertStateExecError + executionError = evalContext.Error.Error() + annotationData.Set("errorMessage", executionError) + } else if evalContext.Firing { + evalContext.Rule.State = m.AlertStateAlerting + annotationData = simplejson.NewFromAny(evalContext.EvalMatches) } else { - // handle no data case - if ctx.NoDataFound { - ctx.Rule.State = ctx.Rule.NoDataState + if evalContext.NoDataFound { + if evalContext.Rule.NoDataState != m.NoDataKeepState { + evalContext.Rule.State = evalContext.Rule.NoDataState.ToAlertState() + } } else { - ctx.Rule.State = m.AlertStateOK + evalContext.Rule.State = m.AlertStateOK } } - countStateResult(ctx.Rule.State) - if ctx.Rule.State != oldState { - handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState) + countStateResult(evalContext.Rule.State) + if handler.shouldUpdateAlertState(evalContext, oldState) { + handler.log.Info("New state change", "alertId", evalContext.Rule.Id, "newState", evalContext.Rule.State, "oldState", oldState) cmd := &m.SetAlertStateCommand{ - AlertId: ctx.Rule.Id, - OrgId: ctx.Rule.OrgId, - State: ctx.Rule.State, - Error: exeuctionError, + AlertId: evalContext.Rule.Id, + OrgId: evalContext.Rule.OrgId, + State: evalContext.Rule.State, + Error: executionError, EvalData: annotationData, } @@ -67,14 +68,14 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) { // save annotation item := annotations.Item{ - OrgId: ctx.Rule.OrgId, - DashboardId: ctx.Rule.DashboardId, - PanelId: ctx.Rule.PanelId, + OrgId: evalContext.Rule.OrgId, + DashboardId: evalContext.Rule.DashboardId, + PanelId: evalContext.Rule.PanelId, Type: annotations.AlertType, - AlertId: ctx.Rule.Id, - Title: ctx.Rule.Name, - Text: ctx.GetStateModel().Text, - NewState: string(ctx.Rule.State), + AlertId: evalContext.Rule.Id, + Title: evalContext.Rule.Name, + Text: evalContext.GetStateModel().Text, + NewState: string(evalContext.Rule.State), PrevState: string(oldState), Epoch: time.Now().Unix(), Data: annotationData, @@ -85,8 +86,14 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) { handler.log.Error("Failed to save annotation for new alert state", "error", err) } - handler.notifier.Notify(ctx) + handler.notifier.Notify(evalContext) } + + return nil +} + +func (handler *DefaultResultHandler) shouldUpdateAlertState(evalContext *EvalContext, oldState m.AlertStateType) bool { + return evalContext.Rule.State != oldState } func countStateResult(state m.AlertStateType) { diff --git a/pkg/services/alerting/result_handler_test.go b/pkg/services/alerting/result_handler_test.go index 32589bef172..7a1abc6d1ef 100644 --- a/pkg/services/alerting/result_handler_test.go +++ b/pkg/services/alerting/result_handler_test.go @@ -1,57 +1,29 @@ package alerting // import ( +// "context" // "testing" -// "time" -// -// "github.com/grafana/grafana/pkg/bus" -// m "github.com/grafana/grafana/pkg/models" -// "github.com/grafana/grafana/pkg/services/alerting/alertstates" // +// "github.com/grafana/grafana/pkg/models" // . "github.com/smartystreets/goconvey/convey" // ) // // func TestAlertResultHandler(t *testing.T) { // Convey("Test result Handler", t, func() { -// resultHandler := ResultHandlerImpl{} -// mockResult := &AlertResultContext{ -// Triggered: false, -// Rule: &AlertRule{ -// Id: 1, -// OrgId 1, -// }, -// } -// mockAlertState := &m.AlertState{} -// bus.ClearBusHandlers() -// bus.AddHandler("test", func(query *m.GetLastAlertStateQuery) error { -// query.Result = mockAlertState -// return nil -// }) +// +// handler := NewResultHandler() +// evalContext := NewEvalContext(context.TODO(), &Rule{}) // // Convey("Should update", func() { // // Convey("when no earlier alert state", func() { -// mockAlertState = nil -// So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) -// }) +// oldState := models.AlertStateOK // -// Convey("alert state have changed", func() { -// mockAlertState = &m.AlertState{ -// State: alertstates.Critical, -// } -// mockResult.Triggered = false -// So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) -// }) +// evalContext.Rule.State = models.AlertStateAlerting +// evalContext.Rule.NoDataState = models.NoDataKeepState +// evalContext.NoDataFound = true // -// Convey("last alert state was 15min ago", func() { -// now := time.Now() -// mockAlertState = &m.AlertState{ -// State: alertstates.Critical, -// Created: now.Add(time.Minute * -30), -// } -// mockResult.Triggered = true -// mockResult.StartTime = time.Now() -// So(resultHandler.shouldUpdateState(mockResult), ShouldBeTrue) +// So(handler.shouldUpdateAlertState(evalContext, oldState), ShouldBeFalse) // }) // }) // }) diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 5f59b60b64f..2ef090717ff 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -18,7 +18,7 @@ type Rule struct { Frequency int64 Name string Message string - NoDataState m.AlertStateType + NoDataState m.NoDataOption State m.AlertStateType Conditions []Condition Notifications []int64 @@ -43,17 +43,27 @@ var unitMultiplier = map[string]int{ "h": 3600, } -func getTimeDurationStringToSeconds(str string) int64 { +func getTimeDurationStringToSeconds(str string) (int64, error) { multiplier := 1 - value, _ := strconv.Atoi(ValueFormatRegex.FindAllString(str, 1)[0]) + matches := ValueFormatRegex.FindAllString(str, 1) + + if len(matches) <= 0 { + return 0, fmt.Errorf("Frequency could not be parsed") + } + + value, err := strconv.Atoi(matches[0]) + if err != nil { + return 0, err + } + unit := UnitFormatRegex.FindAllString(str, 1)[0] if val, ok := unitMultiplier[unit]; ok { multiplier = val } - return int64(value * multiplier) + return int64(value * multiplier), nil } func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { @@ -66,7 +76,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Message = ruleDef.Message model.Frequency = ruleDef.Frequency model.State = ruleDef.State - model.NoDataState = m.AlertStateType(ruleDef.Settings.Get("noDataState").MustString("no_data")) + model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) for _, v := range ruleDef.Settings.Get("notifications").MustArray() { jsonModel := simplejson.NewFromAny(v) diff --git a/pkg/services/alerting/rule_test.go b/pkg/services/alerting/rule_test.go index 622904ec3fc..6144c01d54d 100644 --- a/pkg/services/alerting/rule_test.go +++ b/pkg/services/alerting/rule_test.go @@ -20,25 +20,30 @@ func TestAlertRuleModel(t *testing.T) { }) Convey("Can parse seconds", func() { - seconds := getTimeDurationStringToSeconds("10s") + seconds, _ := getTimeDurationStringToSeconds("10s") So(seconds, ShouldEqual, 10) }) Convey("Can parse minutes", func() { - seconds := getTimeDurationStringToSeconds("10m") + seconds, _ := getTimeDurationStringToSeconds("10m") So(seconds, ShouldEqual, 600) }) Convey("Can parse hours", func() { - seconds := getTimeDurationStringToSeconds("1h") + seconds, _ := getTimeDurationStringToSeconds("1h") So(seconds, ShouldEqual, 3600) }) Convey("defaults to seconds", func() { - seconds := getTimeDurationStringToSeconds("1o") + seconds, _ := getTimeDurationStringToSeconds("1o") So(seconds, ShouldEqual, 1) }) + Convey("should return err for empty string", func() { + _, err := getTimeDurationStringToSeconds("") + So(err, ShouldNotBeNil) + }) + Convey("can construct alert rule model", func() { json := ` { diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go index 9d20796f3dc..b6ef1a63ff8 100644 --- a/pkg/services/alerting/scheduler.go +++ b/pkg/services/alerting/scheduler.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" ) type SchedulerImpl struct { @@ -48,7 +49,7 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) { now := tickTime.Unix() for _, job := range s.jobs { - if job.Running { + if job.Running || job.Rule.State == models.AlertStatePaused { continue } diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go index de2cb981aaa..fd908d6f95d 100644 --- a/pkg/services/alerting/test_notification.go +++ b/pkg/services/alerting/test_notification.go @@ -1,6 +1,8 @@ package alerting import ( + "context" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" @@ -35,13 +37,10 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error { return err } - notifier.sendNotifications([]Notifier{notifiers}, createTestEvalContext()) - - return nil + return notifier.sendNotifications(createTestEvalContext(), []Notifier{notifiers}) } func createTestEvalContext() *EvalContext { - testRule := &Rule{ DashboardId: 1, PanelId: 1, @@ -50,9 +49,8 @@ func createTestEvalContext() *EvalContext { State: m.AlertStateAlerting, } - ctx := NewEvalContext(testRule) + ctx := NewEvalContext(context.TODO(), testRule) ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png" - ctx.IsTestRun = true ctx.Firing = true ctx.Error = nil diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go index 25a08a3b3bf..82b1a6276a1 100644 --- a/pkg/services/alerting/test_rule.go +++ b/pkg/services/alerting/test_rule.go @@ -1,6 +1,7 @@ package alerting import ( + "context" "fmt" "github.com/grafana/grafana/pkg/bus" @@ -48,7 +49,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error { func testAlertRule(rule *Rule) *EvalContext { handler := NewEvalHandler() - context := NewEvalContext(rule) + context := NewEvalContext(context.TODO(), rule) context.IsTestRun = true handler.Eval(context) diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index 189c3d823cf..3fc3bafe5c5 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -5,6 +5,7 @@ import "github.com/grafana/grafana/pkg/components/simplejson" type Repository interface { Save(item *Item) error Find(query *ItemQuery) ([]*Item, error) + Delete(params *DeleteParams) error } type ItemQuery struct { @@ -20,6 +21,12 @@ type ItemQuery struct { Limit int64 `json:"alertId"` } +type DeleteParams struct { + AlertId int64 `json:"alertId"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` +} + var repositoryInstance Repository func GetRepository() Repository { diff --git a/pkg/services/notifications/email.go b/pkg/services/notifications/email.go index f81f3e1007b..803f2096b56 100644 --- a/pkg/services/notifications/email.go +++ b/pkg/services/notifications/email.go @@ -6,19 +6,12 @@ import ( ) type Message struct { - To []string - From string - Subject string - Body string - Massive bool - Info string -} - -// create mail content -func (m *Message) Content() string { - contentType := "text/html; charset=UTF-8" - content := "From: " + m.From + "\r\nSubject: " + m.Subject + "\r\nContent-Type: " + contentType + "\r\n\r\n" + m.Body - return content + To []string + From string + Subject string + Body string + Info string + EmbededFiles []string } func setDefaultTemplateData(data map[string]interface{}, u *m.User) { diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go index 91c75a1889e..9e54e906d99 100644 --- a/pkg/services/notifications/mailer.go +++ b/pkg/services/notifications/mailer.go @@ -5,17 +5,19 @@ package notifications import ( + "bytes" "crypto/tls" + "errors" "fmt" + "html/template" "net" - "net/mail" - "net/smtp" - "os" + "strconv" "strings" - "time" "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "gopkg.in/gomail.v2" ) var mailQueue chan *Message @@ -29,7 +31,7 @@ func processMailQueue() { for { select { case msg := <-mailQueue: - num, err := buildAndSend(msg) + num, err := send(msg) tos := strings.Join(msg.To, "; ") info := "" if err != nil { @@ -48,10 +50,40 @@ var addToMailQueue = func(msg *Message) { mailQueue <- msg } -func sendToSmtpServer(recipients []string, msgContent []byte) error { - host, port, err := net.SplitHostPort(setting.Smtp.Host) +func send(msg *Message) (int, error) { + dialer, err := createDialer() if err != nil { - return err + return 0, err + } + + for _, address := range msg.To { + m := gomail.NewMessage() + m.SetHeader("From", msg.From) + m.SetHeader("To", address) + m.SetHeader("Subject", msg.Subject) + for _, file := range msg.EmbededFiles { + m.Embed(file) + } + + m.SetBody("text/html", msg.Body) + + if err := dialer.DialAndSend(m); err != nil { + return 0, err + } + } + + return len(msg.To), nil +} + +func createDialer() (*gomail.Dialer, error) { + host, port, err := net.SplitHostPort(setting.Smtp.Host) + + if err != nil { + return nil, err + } + iPort, err := strconv.Atoi(port) + if err != nil { + return nil, err } tlsconfig := &tls.Config{ @@ -62,126 +94,59 @@ func sendToSmtpServer(recipients []string, msgContent []byte) error { if setting.Smtp.CertFile != "" { cert, err := tls.LoadX509KeyPair(setting.Smtp.CertFile, setting.Smtp.KeyFile) if err != nil { - return err + return nil, err } tlsconfig.Certificates = []tls.Certificate{cert} } - conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), time.Second*10) - if err != nil { - return err - } - defer conn.Close() - - isSecureConn := false - // Start TLS directly if the port ends with 465 (SMTPS protocol) - if strings.HasSuffix(port, "465") { - conn = tls.Client(conn, tlsconfig) - isSecureConn = true - } - - client, err := smtp.NewClient(conn, host) - if err != nil { - return err - } - - hostname, err := os.Hostname() - if err != nil { - return err - } - - if err = client.Hello(hostname); err != nil { - return err - } - - // If not using SMTPS, alway use STARTTLS if available - hasStartTLS, _ := client.Extension("STARTTLS") - if !isSecureConn && hasStartTLS { - if err = client.StartTLS(tlsconfig); err != nil { - return err - } - } - - canAuth, options := client.Extension("AUTH") - - if canAuth && len(setting.Smtp.User) > 0 { - var auth smtp.Auth - - if strings.Contains(options, "CRAM-MD5") { - auth = smtp.CRAMMD5Auth(setting.Smtp.User, setting.Smtp.Password) - } else if strings.Contains(options, "PLAIN") { - auth = smtp.PlainAuth("", setting.Smtp.User, setting.Smtp.Password, host) - } - - if auth != nil { - if err = client.Auth(auth); err != nil { - return err - } - } - } - - if fromAddress, err := mail.ParseAddress(setting.Smtp.FromAddress); err != nil { - return err - } else { - if err = client.Mail(fromAddress.Address); err != nil { - return err - } - } - - for _, rec := range recipients { - if err = client.Rcpt(rec); err != nil { - return err - } - } - - w, err := client.Data() - if err != nil { - return err - } - if _, err = w.Write([]byte(msgContent)); err != nil { - return err - } - - if err = w.Close(); err != nil { - return err - } - - return client.Quit() + d := gomail.NewPlainDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password) + d.TLSConfig = tlsconfig + return d, nil } -func buildAndSend(msg *Message) (int, error) { - log.Trace("Sending mails to: %s", strings.Join(msg.To, "; ")) - - // get message body - content := msg.Content() - - if len(msg.To) == 0 { - return 0, fmt.Errorf("empty receive emails") - } else if len(msg.Body) == 0 { - return 0, fmt.Errorf("empty email body") +func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) { + if !setting.Smtp.Enabled { + return nil, errors.New("Grafana mailing/smtp options not configured, contact your Grafana admin") } - if msg.Massive { - // send mail to multiple emails one by one - num := 0 - for _, to := range msg.To { - body := []byte("To: " + to + "\r\n" + content) - err := sendToSmtpServer([]string{to}, body) - if err != nil { - return num, err - } - num++ - } - return num, nil - } else { - body := []byte("To: " + strings.Join(msg.To, ";") + "\r\n" + content) + var buffer bytes.Buffer + var err error + var subjectText interface{} - // send to multiple emails in one message - err := sendToSmtpServer(msg.To, body) - if err != nil { - return 0, err - } else { - return 1, nil - } + data := cmd.Data + if data == nil { + data = make(map[string]interface{}, 10) } + + setDefaultTemplateData(data, nil) + err = mailTemplates.ExecuteTemplate(&buffer, cmd.Template, data) + if err != nil { + return nil, err + } + + subjectData := data["Subject"].(map[string]interface{}) + subjectText, hasSubject := subjectData["value"] + + if !hasSubject { + return nil, errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template)) + } + + subjectTmpl, err := template.New("subject").Parse(subjectText.(string)) + if err != nil { + return nil, err + } + + var subjectBuffer bytes.Buffer + err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data) + if err != nil { + return nil, err + } + + return &Message{ + To: cmd.To, + From: setting.Smtp.FromAddress, + Subject: subjectBuffer.String(), + Body: buffer.String(), + EmbededFiles: cmd.EmbededFiles, + }, nil } diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index 04b11f73b84..8a13eec2dc8 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -1,7 +1,7 @@ package notifications import ( - "bytes" + "context" "errors" "fmt" "html/template" @@ -29,7 +29,10 @@ func Init() error { bus.AddHandler("email", validateResetPasswordCode) bus.AddHandler("email", sendEmailCommandHandler) + bus.AddCtxHandler("email", sendEmailCommandHandlerSync) + bus.AddHandler("webhook", sendWebhook) + bus.AddCtxHandler("webhook", SendWebhookSync) bus.AddEventListener(signUpStartedHandler) bus.AddEventListener(signUpCompletedHandler) @@ -56,12 +59,23 @@ func Init() error { return nil } +func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error { + return sendWebRequestSync(ctx, &Webhook{ + Url: cmd.Url, + User: cmd.User, + Password: cmd.Password, + Body: cmd.Body, + HttpMethod: cmd.HttpMethod, + }) +} + func sendWebhook(cmd *m.SendWebhook) error { addToWebhookQueue(&Webhook{ - Url: cmd.Url, - User: cmd.User, - Password: cmd.Password, - Body: cmd.Body, + Url: cmd.Url, + User: cmd.User, + Password: cmd.Password, + Body: cmd.Body, + HttpMethod: cmd.HttpMethod, }) return nil @@ -72,51 +86,33 @@ func subjectTemplateFunc(obj map[string]interface{}, value string) string { return "" } -func sendEmailCommandHandler(cmd *m.SendEmailCommand) error { - if !setting.Smtp.Enabled { - return errors.New("Grafana mailing/smtp options not configured, contact your Grafana admin") - } - - var buffer bytes.Buffer - var err error - var subjectText interface{} - - data := cmd.Data - if data == nil { - data = make(map[string]interface{}, 10) - } - - setDefaultTemplateData(data, nil) - err = mailTemplates.ExecuteTemplate(&buffer, cmd.Template, data) - if err != nil { - return err - } - - subjectData := data["Subject"].(map[string]interface{}) - subjectText, hasSubject := subjectData["value"] - - if !hasSubject { - return errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template)) - } - - subjectTmpl, err := template.New("subject").Parse(subjectText.(string)) - if err != nil { - return err - } - - var subjectBuffer bytes.Buffer - err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data) - if err != nil { - return err - } - - addToMailQueue(&Message{ - To: cmd.To, - From: setting.Smtp.FromAddress, - Subject: subjectBuffer.String(), - Body: buffer.String(), +func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSync) error { + message, err := buildEmailMessage(&m.SendEmailCommand{ + Data: cmd.Data, + Info: cmd.Info, + Template: cmd.Template, + To: cmd.To, + EmbededFiles: cmd.EmbededFiles, }) + if err != nil { + return err + } + + _, err = send(message) + + return err +} + +func sendEmailCommandHandler(cmd *m.SendEmailCommand) error { + message, err := buildEmailMessage(cmd) + + if err != nil { + return err + } + + addToMailQueue(message) + return nil } diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go index d4f4ee0a5fb..79db664e893 100644 --- a/pkg/services/notifications/notifications_test.go +++ b/pkg/services/notifications/notifications_test.go @@ -3,7 +3,6 @@ package notifications import ( "testing" - "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" @@ -18,7 +17,7 @@ type testTriggeredAlert struct { func TestNotifications(t *testing.T) { Convey("Given the notifications service", t, func() { - bus.ClearBusHandlers() + //bus.ClearBusHandlers() setting.StaticRootPath = "../../../public/" setting.Smtp.Enabled = true diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go index 67ffa43900a..de1303d8131 100644 --- a/pkg/services/notifications/webhook.go +++ b/pkg/services/notifications/webhook.go @@ -2,20 +2,24 @@ package notifications import ( "bytes" + "context" "fmt" "io/ioutil" "net/http" "time" + "golang.org/x/net/context/ctxhttp" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/util" ) type Webhook struct { - Url string - User string - Password string - Body string + Url string + User string + Password string + Body string + HttpMethod string } var webhookQueue chan *Webhook @@ -31,7 +35,7 @@ func processWebhookQueue() { for { select { case webhook := <-webhookQueue: - err := sendWebRequest(webhook) + err := sendWebRequestSync(context.TODO(), webhook) if err != nil { webhookLog.Error("Failed to send webrequest ", "error", err) @@ -40,14 +44,18 @@ func processWebhookQueue() { } } -func sendWebRequest(webhook *Webhook) error { - webhookLog.Debug("Sending webhook", "url", webhook.Url) +func sendWebRequestSync(ctx context.Context, webhook *Webhook) error { + webhookLog.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod) - client := http.Client{ + client := &http.Client{ Timeout: time.Duration(10 * time.Second), } - request, err := http.NewRequest("POST", webhook.Url, bytes.NewReader([]byte(webhook.Body))) + if webhook.HttpMethod == "" { + webhook.HttpMethod = http.MethodPost + } + + request, err := http.NewRequest(webhook.HttpMethod, webhook.Url, bytes.NewReader([]byte(webhook.Body))) if webhook.User != "" && webhook.Password != "" { request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password)) } @@ -56,22 +64,23 @@ func sendWebRequest(webhook *Webhook) error { return err } - resp, err := client.Do(request) + resp, err := ctxhttp.Do(ctx, client, request) if err != nil { return err } - _, err = ioutil.ReadAll(resp.Body) + if resp.StatusCode/100 == 2 { + return nil + } + + body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } - - if resp.StatusCode != 200 { - return fmt.Errorf("Webhook response code %v", resp.StatusCode) - } - defer resp.Body.Close() - return nil + + webhookLog.Debug("Webhook failed", "statuscode", resp.Status, "body", string(body)) + return fmt.Errorf("Webhook response status %v", resp.Status) } var addToWebhookQueue = func(msg *Webhook) { diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 61397017943..4824b000bcb 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -18,6 +18,7 @@ func init() { bus.AddHandler("sql", GetAllAlertQueryHandler) bus.AddHandler("sql", SetAlertState) bus.AddHandler("sql", GetAlertStatesForDashboard) + bus.AddHandler("sql", PauseAlertRule) } func GetAlertById(query *m.GetAlertByIdQuery) error { @@ -45,13 +46,23 @@ func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error { return nil } +func deleteAlertByIdInternal(alertId int64, reason string, sess *xorm.Session) error { + sqlog.Debug("Deleting alert", "id", alertId, "reason", reason) + + if _, err := sess.Exec("DELETE FROM alert WHERE id = ?", alertId); err != nil { + return err + } + + if _, err := sess.Exec("DELETE FROM annotation WHERE alert_id = ?", alertId); err != nil { + return err + } + + return nil +} + func DeleteAlertById(cmd *m.DeleteAlertCommand) error { return inTransaction(func(sess *xorm.Session) error { - if _, err := sess.Exec("DELETE FROM alert WHERE id = ?", cmd.AlertId); err != nil { - return err - } - - return nil + return deleteAlertByIdInternal(cmd.AlertId, "DeleteAlertCommand", sess) }) } @@ -109,12 +120,7 @@ func DeleteAlertDefinition(dashboardId int64, sess *xorm.Session) error { sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) for _, alert := range alerts { - _, err := sess.Exec("DELETE FROM alert WHERE id = ? ", alert.Id) - if err != nil { - return err - } - - sqlog.Debug("Alert deleted (due to dashboard deletion)", "name", alert.Name, "id", alert.Id) + deleteAlertByIdInternal(alert.Id, "Dashboard deleted", sess) } return nil @@ -194,12 +200,7 @@ func deleteMissingAlerts(alerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xorm } if missing { - _, err := sess.Exec("DELETE FROM alert WHERE id = ?", missingAlert.Id) - if err != nil { - return err - } - - sqlog.Debug("Alert deleted", "name", missingAlert.Name, "id", missingAlert.Id) + deleteAlertByIdInternal(missingAlert.Id, "Removed from dashboard", sess) } } @@ -243,6 +244,31 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error { }) } +func PauseAlertRule(cmd *m.PauseAlertCommand) error { + return inTransaction(func(sess *xorm.Session) error { + alert := m.Alert{} + + has, err := x.Where("id = ? AND org_id=?", cmd.AlertId, cmd.OrgId).Get(&alert) + + if err != nil { + return err + } else if !has { + return fmt.Errorf("Could not find alert") + } + + var newState m.AlertStateType + if cmd.Paused { + newState = m.AlertStatePaused + } else { + newState = m.AlertStateNoData + } + alert.State = newState + + sess.Id(alert.Id).Update(&alert) + return nil + }) +} + func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error { var rawSql = `SELECT id, diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index 3ea8647d3fa..e219f48d2fe 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -84,3 +84,17 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I return items, nil } + +func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error { + return inTransaction(func(sess *xorm.Session) error { + + sql := "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?" + + _, err := sess.Exec(sql, params.DashboardId, params.PanelId) + if err != nil { + return err + } + + return nil + }) +} diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 2f1b40b2d61..0e6219785bd 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -60,6 +60,13 @@ func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error { func AddDataSource(cmd *m.AddDataSourceCommand) error { return inTransaction(func(sess *xorm.Session) error { + existing := m.DataSource{OrgId: cmd.OrgId, Name: cmd.Name} + has, _ := sess.Get(&existing) + + if has { + return m.ErrDataSourceNameExists + } + ds := &m.DataSource{ OrgId: cmd.OrgId, Name: cmd.Name, diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go index b14c7ed9a24..35752eeaafc 100644 --- a/pkg/services/sqlstore/datasource_test.go +++ b/pkg/services/sqlstore/datasource_test.go @@ -41,6 +41,7 @@ func TestDataAccess(t *testing.T) { err := AddDataSource(&m.AddDataSourceCommand{ OrgId: 10, + Name: "laban", Type: m.DS_INFLUXDB, Access: m.DS_ACCESS_DIRECT, Url: "http://test", @@ -63,15 +64,19 @@ func TestDataAccess(t *testing.T) { Convey("Given a datasource", func() { - AddDataSource(&m.AddDataSourceCommand{ + err := AddDataSource(&m.AddDataSourceCommand{ OrgId: 10, + Name: "nisse", Type: m.DS_GRAPHITE, Access: m.DS_ACCESS_DIRECT, Url: "http://test", }) + So(err, ShouldBeNil) query := m.GetDataSourcesQuery{OrgId: 10} - GetDataSources(&query) + err = GetDataSources(&query) + So(err, ShouldBeNil) + ds := query.Result[0] Convey("Can delete datasource", func() { diff --git a/pkg/services/sqlstore/migrator/mysql_dialect.go b/pkg/services/sqlstore/migrator/mysql_dialect.go index fc64842bd07..c36baa3afbf 100644 --- a/pkg/services/sqlstore/migrator/mysql_dialect.go +++ b/pkg/services/sqlstore/migrator/mysql_dialect.go @@ -30,7 +30,10 @@ func (db *Mysql) AutoIncrStr() string { } func (db *Mysql) BooleanStr(value bool) string { - return strconv.FormatBool(value) + if value { + return "1" + } + return "0" } func (db *Mysql) SqlType(c *Column) string { diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index bbf21296519..bb21995f54a 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -19,6 +19,7 @@ func init() { bus.AddHandler("sql", UpdateUser) bus.AddHandler("sql", ChangeUserPassword) bus.AddHandler("sql", GetUserByLogin) + bus.AddHandler("sql", GetUserByEmail) bus.AddHandler("sql", SetUsingOrg) bus.AddHandler("sql", GetUserProfile) bus.AddHandler("sql", GetSignedInUser) @@ -193,6 +194,27 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error { return nil } +func GetUserByEmail(query *m.GetUserByEmailQuery) error { + if query.Email == "" { + return m.ErrUserNotFound + } + + user := new(m.User) + + user = &m.User{Email: query.Email} + has, err := x.Get(user) + + if err != nil { + return err + } else if has == false { + return m.ErrUserNotFound + } + + query.Result = user + + return nil +} + func UpdateUser(cmd *m.UpdateUserCommand) error { return inTransaction2(func(sess *session) error { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 46f70215c1f..94d93d3743e 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -134,8 +134,9 @@ var ( GoogleTagManagerId string // LDAP - LdapEnabled bool - LdapConfigFile string + LdapEnabled bool + LdapConfigFile string + LdapAllowSignup bool = true // SMTP email settings Smtp SmtpSettings @@ -144,7 +145,7 @@ var ( Quota QuotaSettings // Alerting - AlertingEnabled bool + ExecuteAlerts bool // logger logger log.Logger @@ -460,7 +461,7 @@ func NewConfigContext(args *CommandLineArgs) error { Env = Cfg.Section("").Key("app_mode").MustString("development") InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name") - PluginsPath = Cfg.Section("paths").Key("plugins").String() + PluginsPath = makeAbsolute(Cfg.Section("paths").Key("plugins").String(), HomePath) server := Cfg.Section("server") AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server) @@ -551,9 +552,10 @@ func NewConfigContext(args *CommandLineArgs) error { ldapSec := Cfg.Section("auth.ldap") LdapEnabled = ldapSec.Key("enabled").MustBool(false) LdapConfigFile = ldapSec.Key("config_file").String() + LdapAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true) alerting := Cfg.Section("alerting") - AlertingEnabled = alerting.Key("enabled").MustBool(false) + ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true) readSessionConfig() readSmtpSettings() diff --git a/pkg/setting/setting_oauth.go b/pkg/setting/setting_oauth.go index 8d51343e635..e652310a621 100644 --- a/pkg/setting/setting_oauth.go +++ b/pkg/setting/setting_oauth.go @@ -9,6 +9,9 @@ type OAuthInfo struct { ApiUrl string AllowSignup bool Name string + TlsClientCert string + TlsClientKey string + TlsClientCa string } type OAuther struct { diff --git a/pkg/social/generic_oauth.go b/pkg/social/generic_oauth.go index f016c87e201..4b7ec065bc3 100644 --- a/pkg/social/generic_oauth.go +++ b/pkg/social/generic_oauth.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net/http" - "strconv" "github.com/grafana/grafana/pkg/models" @@ -160,15 +159,16 @@ func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) return logins, nil } -func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { +func (s *GenericOAuth) UserInfo(client *http.Client) (*BasicUserInfo, error) { var data struct { - Id int `json:"id"` - Name string `json:"login"` - Email string `json:"email"` + Name string `json:"name"` + Login string `json:"login"` + Username string `json:"username"` + Email string `json:"email"` + Attributes map[string][]string `json:"attributes"` } var err error - client := s.Client(oauth2.NoContext, token) r, err := client.Get(s.apiUrl) if err != nil { return nil, err @@ -181,11 +181,30 @@ func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { } userInfo := &BasicUserInfo{ - Identity: strconv.Itoa(data.Id), Name: data.Name, + Login: data.Login, Email: data.Email, } + if (userInfo.Email == "" && data.Attributes["email:primary"] != nil) { + userInfo.Email = data.Attributes["email:primary"][0] + } + + if userInfo.Email == "" { + userInfo.Email, err = s.FetchPrivateEmail(client) + if err != nil { + return nil, err + } + } + + if (userInfo.Login == "" && data.Username != "") { + userInfo.Login = data.Username + } + + if (userInfo.Login == "") { + userInfo.Login = data.Email + } + if !s.IsTeamMember(client) { return nil, errors.New("User not a member of one of the required teams") } @@ -194,12 +213,5 @@ func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { return nil, errors.New("User not a member of one of the required organizations") } - if userInfo.Email == "" { - userInfo.Email, err = s.FetchPrivateEmail(client) - if err != nil { - return nil, err - } - } - return userInfo, nil } diff --git a/pkg/social/github_oauth.go b/pkg/social/github_oauth.go index 40c8f2a2f7c..f3a9d4ece0f 100644 --- a/pkg/social/github_oauth.go +++ b/pkg/social/github_oauth.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net/http" - "strconv" "github.com/grafana/grafana/pkg/models" @@ -168,15 +167,14 @@ func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) return logins, nil } -func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { +func (s *SocialGithub) UserInfo(client *http.Client) (*BasicUserInfo, error) { var data struct { Id int `json:"id"` - Name string `json:"login"` + Login string `json:"login"` Email string `json:"email"` } var err error - client := s.Client(oauth2.NoContext, token) r, err := client.Get(s.apiUrl) if err != nil { return nil, err @@ -189,8 +187,8 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { } userInfo := &BasicUserInfo{ - Identity: strconv.Itoa(data.Id), - Name: data.Name, + Name: data.Login, + Login: data.Login, Email: data.Email, } diff --git a/pkg/social/google_oauth.go b/pkg/social/google_oauth.go index 7f0fdcc250a..21543902a86 100644 --- a/pkg/social/google_oauth.go +++ b/pkg/social/google_oauth.go @@ -2,6 +2,7 @@ package social import ( "encoding/json" + "net/http" "github.com/grafana/grafana/pkg/models" @@ -27,15 +28,13 @@ func (s *SocialGoogle) IsSignupAllowed() bool { return s.allowSignup } -func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { +func (s *SocialGoogle) UserInfo(client *http.Client) (*BasicUserInfo, error) { var data struct { - Id string `json:"id"` Name string `json:"name"` Email string `json:"email"` } var err error - client := s.Client(oauth2.NoContext, token) r, err := client.Get(s.apiUrl) if err != nil { return nil, err @@ -45,8 +44,8 @@ func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { return nil, err } return &BasicUserInfo{ - Identity: data.Id, Name: data.Name, Email: data.Email, + Login: data.Email, }, nil } diff --git a/pkg/social/grafananet_oauth.go b/pkg/social/grafananet_oauth.go index 80c1aaedb45..119b7a31cfc 100644 --- a/pkg/social/grafananet_oauth.go +++ b/pkg/social/grafananet_oauth.go @@ -2,9 +2,7 @@ package social import ( "encoding/json" - "fmt" "net/http" - "strconv" "github.com/grafana/grafana/pkg/models" @@ -18,6 +16,10 @@ type SocialGrafanaNet struct { allowSignup bool } +type OrgRecord struct { + Login string `json:"login"` +} + func (s *SocialGrafanaNet) Type() int { return int(models.GRAFANANET) } @@ -30,19 +32,14 @@ func (s *SocialGrafanaNet) IsSignupAllowed() bool { return s.allowSignup } -func (s *SocialGrafanaNet) IsOrganizationMember(client *http.Client) bool { +func (s *SocialGrafanaNet) IsOrganizationMember(organizations []OrgRecord) bool { if len(s.allowedOrganizations) == 0 { return true } - organizations, err := s.FetchOrganizations(client) - if err != nil { - return false - } - for _, allowedOrganization := range s.allowedOrganizations { for _, organization := range organizations { - if organization == allowedOrganization { + if organization.Login == allowedOrganization { return true } } @@ -51,43 +48,16 @@ func (s *SocialGrafanaNet) IsOrganizationMember(client *http.Client) bool { return false } -func (s *SocialGrafanaNet) FetchOrganizations(client *http.Client) ([]string, error) { - type Record struct { - Login string `json:"login"` - } - - url := fmt.Sprintf(s.url + "/api/oauth2/user/orgs") - r, err := client.Get(url) - if err != nil { - return nil, err - } - - defer r.Body.Close() - - var records []Record - - if err = json.NewDecoder(r.Body).Decode(&records); err != nil { - return nil, err - } - - var logins = make([]string, len(records)) - for i, record := range records { - logins[i] = record.Login - } - - return logins, nil -} - -func (s *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) { +func (s *SocialGrafanaNet) UserInfo(client *http.Client) (*BasicUserInfo, error) { var data struct { - Id int `json:"id"` - Name string `json:"login"` + Name string `json:"name"` + Login string `json:"username"` Email string `json:"email"` Role string `json:"role"` + Orgs []OrgRecord `json:"orgs"` } var err error - client := s.Client(oauth2.NoContext, token) r, err := client.Get(s.url + "/api/oauth2/user") if err != nil { return nil, err @@ -100,13 +70,13 @@ func (s *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) } userInfo := &BasicUserInfo{ - Identity: strconv.Itoa(data.Id), Name: data.Name, + Login: data.Login, Email: data.Email, Role: data.Role, } - if !s.IsOrganizationMember(client) { + if !s.IsOrganizationMember(data.Orgs) { return nil, ErrMissingOrganizationMembership } diff --git a/pkg/social/social.go b/pkg/social/social.go index 4dbc70d71a9..f70bdd70843 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -1,16 +1,16 @@ package social import ( + "net/http" "strings" - "github.com/grafana/grafana/pkg/setting" "golang.org/x/net/context" - "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/setting" ) type BasicUserInfo struct { - Identity string Name string Email string Login string @@ -20,12 +20,13 @@ type BasicUserInfo struct { type SocialConnector interface { Type() int - UserInfo(token *oauth2.Token) (*BasicUserInfo, error) + UserInfo(client *http.Client) (*BasicUserInfo, error) IsEmailAllowed(email string) bool IsSignupAllowed() bool AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string Exchange(ctx context.Context, code string) (*oauth2.Token, error) + Client(ctx context.Context, t *oauth2.Token) *http.Client } var ( @@ -52,6 +53,9 @@ func NewOAuthService() { AllowedDomains: sec.Key("allowed_domains").Strings(" "), AllowSignup: sec.Key("allow_sign_up").MustBool(), Name: sec.Key("name").MustString(name), + TlsClientCert: sec.Key("tls_client_cert").String(), + TlsClientKey: sec.Key("tls_client_key").String(), + TlsClientCa: sec.Key("tls_client_ca").String(), } if !info.Enabled { @@ -59,6 +63,7 @@ func NewOAuthService() { } setting.OAuthService.OAuthInfos[name] = info + config := oauth2.Config{ ClientID: info.ClientId, ClientSecret: info.ClientSecret, @@ -85,9 +90,10 @@ func NewOAuthService() { // Google. if name == "google" { SocialMap["google"] = &SocialGoogle{ - Config: &config, allowedDomains: info.AllowedDomains, - apiUrl: info.ApiUrl, - allowSignup: info.AllowSignup, + Config: &config, + allowedDomains: info.AllowedDomains, + apiUrl: info.ApiUrl, + allowSignup: info.AllowSignup, } } @@ -104,15 +110,15 @@ func NewOAuthService() { } if name == "grafananet" { - config := oauth2.Config{ + config = oauth2.Config{ ClientID: info.ClientId, ClientSecret: info.ClientSecret, - Endpoint: oauth2.Endpoint{ - AuthURL: setting.GrafanaNetUrl + "/oauth2/authorize", - TokenURL: setting.GrafanaNetUrl + "/api/oauth2/token", + Endpoint: oauth2.Endpoint{ + AuthURL: setting.GrafanaNetUrl + "/oauth2/authorize", + TokenURL: setting.GrafanaNetUrl + "/api/oauth2/token", }, - RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name, - Scopes: info.Scopes, + RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name, + Scopes: info.Scopes, } SocialMap["grafananet"] = &SocialGrafanaNet{ diff --git a/pkg/tsdb/batch.go b/pkg/tsdb/batch.go index 4dee7b31c86..284a158bf5f 100644 --- a/pkg/tsdb/batch.go +++ b/pkg/tsdb/batch.go @@ -1,6 +1,9 @@ package tsdb -import "errors" +import ( + "context" + "errors" +) type Batch struct { DataSourceId int64 @@ -20,7 +23,7 @@ func newBatch(dsId int64, queries QuerySlice) *Batch { } } -func (bg *Batch) process(context *QueryContext) { +func (bg *Batch) process(ctx context.Context, queryContext *QueryContext) { executor := getExecutorFor(bg.Queries[0].DataSource) if executor == nil { @@ -32,13 +35,13 @@ func (bg *Batch) process(context *QueryContext) { for _, query := range bg.Queries { result.QueryResults[query.RefId] = &QueryResult{Error: result.Error} } - context.ResultsChan <- result + queryContext.ResultsChan <- result return } - res := executor.Execute(bg.Queries, context) + res := executor.Execute(ctx, bg.Queries, queryContext) bg.Done = true - context.ResultsChan <- res + queryContext.ResultsChan <- res } func (bg *Batch) addQuery(query *Query) { diff --git a/pkg/tsdb/executor.go b/pkg/tsdb/executor.go index b39c2cdaa97..cc1d592dcd5 100644 --- a/pkg/tsdb/executor.go +++ b/pkg/tsdb/executor.go @@ -1,7 +1,9 @@ package tsdb +import "context" + type Executor interface { - Execute(queries QuerySlice, context *QueryContext) *BatchResult + Execute(ctx context.Context, queries QuerySlice, query *QueryContext) *BatchResult } var registry map[string]GetExecutorFn diff --git a/pkg/tsdb/fake_test.go b/pkg/tsdb/fake_test.go index 2ba02792d6d..c403fdba4fb 100644 --- a/pkg/tsdb/fake_test.go +++ b/pkg/tsdb/fake_test.go @@ -1,5 +1,7 @@ package tsdb +import "context" + type FakeExecutor struct { results map[string]*QueryResult resultsFn map[string]ResultsFn @@ -14,7 +16,7 @@ func NewFakeExecutor(dsInfo *DataSourceInfo) *FakeExecutor { } } -func (e *FakeExecutor) Execute(queries QuerySlice, context *QueryContext) *BatchResult { +func (e *FakeExecutor) Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult { result := &BatchResult{QueryResults: make(map[string]*QueryResult)} for _, query := range queries { if results, has := e.results[query.RefId]; has { diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 78685d52371..a2783a107fe 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -1,6 +1,7 @@ package graphite import ( + "context" "crypto/tls" "encoding/json" "fmt" @@ -11,6 +12,8 @@ import ( "strings" "time" + "golang.org/x/net/context/ctxhttp" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb" @@ -26,7 +29,7 @@ func NewGraphiteExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { var ( glog log.Logger - HttpClient http.Client + HttpClient *http.Client ) func init() { @@ -37,13 +40,13 @@ func init() { TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - HttpClient = http.Client{ + HttpClient = &http.Client{ Timeout: time.Duration(15 * time.Second), Transport: tr, } } -func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { +func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { result := &tsdb.BatchResult{} formData := url.Values{ @@ -54,7 +57,11 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC } for _, query := range queries { - formData["target"] = []string{query.Model.Get("target").MustString()} + if fullTarget, err := query.Model.Get("targetFull").String(); err == nil { + formData["target"] = []string{fullTarget} + } else { + formData["target"] = []string{query.Model.Get("target").MustString()} + } } if setting.Env == setting.DEV { @@ -66,7 +73,8 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC result.Error = err return result } - res, err := HttpClient.Do(req) + + res, err := ctxhttp.Do(ctx, HttpClient, req) if err != nil { result.Error = err return result diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go new file mode 100644 index 00000000000..b546a6ee3a9 --- /dev/null +++ b/pkg/tsdb/influxdb/influxdb.go @@ -0,0 +1,138 @@ +package influxdb + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "time" + + "golang.org/x/net/context/ctxhttp" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/tsdb" +) + +type InfluxDBExecutor struct { + *tsdb.DataSourceInfo + QueryParser *InfluxdbQueryParser + QueryBuilder *QueryBuilder + ResponseParser *ResponseParser +} + +func NewInfluxDBExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { + return &InfluxDBExecutor{ + DataSourceInfo: dsInfo, + QueryParser: &InfluxdbQueryParser{}, + QueryBuilder: &QueryBuilder{}, + ResponseParser: &ResponseParser{}, + } +} + +var ( + glog log.Logger + HttpClient *http.Client +) + +func init() { + glog = log.New("tsdb.influxdb") + tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + HttpClient = &http.Client{ + Timeout: time.Duration(15 * time.Second), + Transport: tr, + } +} + +func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { + result := &tsdb.BatchResult{} + + query, err := e.getQuery(queries, context) + if err != nil { + return result.WithError(err) + } + + glog.Debug("Influxdb query", "raw query", query) + + req, err := e.createRequest(query) + if err != nil { + return result.WithError(err) + } + + resp, err := ctxhttp.Do(ctx, HttpClient, req) + if err != nil { + return result.WithError(err) + } + + if resp.StatusCode/100 != 2 { + return result.WithError(fmt.Errorf("Influxdb returned statuscode invalid status code: %v", resp.Status)) + } + + var response Response + dec := json.NewDecoder(resp.Body) + dec.UseNumber() + err = dec.Decode(&response) + if err != nil { + return result.WithError(err) + } + + result.QueryResults = make(map[string]*tsdb.QueryResult) + result.QueryResults["A"] = e.ResponseParser.Parse(&response) + + return result +} + +func (e *InfluxDBExecutor) getQuery(queries tsdb.QuerySlice, context *tsdb.QueryContext) (string, error) { + for _, v := range queries { + + query, err := e.QueryParser.Parse(v.Model, e.DataSourceInfo) + if err != nil { + return "", err + } + + rawQuery, err := e.QueryBuilder.Build(query, context) + if err != nil { + return "", err + } + + return rawQuery, nil + } + + return "", fmt.Errorf("query request contains no queries") +} + +func (e *InfluxDBExecutor) createRequest(query string) (*http.Request, error) { + u, _ := url.Parse(e.Url) + u.Path = path.Join(u.Path, "query") + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + + params := req.URL.Query() + params.Set("q", query) + params.Set("db", e.Database) + params.Set("epoch", "s") + req.URL.RawQuery = params.Encode() + + req.Header.Set("User-Agent", "Grafana") + + if e.BasicAuth { + req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword) + } + + if e.User != "" { + req.SetBasicAuth(e.User, e.Password) + } + + glog.Debug("Influxdb request", "url", req.URL.String()) + return req, nil +} diff --git a/pkg/tsdb/influxdb/model_parser.go b/pkg/tsdb/influxdb/model_parser.go new file mode 100644 index 00000000000..ff8977f925b --- /dev/null +++ b/pkg/tsdb/influxdb/model_parser.go @@ -0,0 +1,162 @@ +package influxdb + +import ( + "strconv" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" +) + +type InfluxdbQueryParser struct{} + +func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) { + policy := model.Get("policy").MustString("default") + rawQuery := model.Get("query").MustString("") + interval := model.Get("interval").MustString("") + + measurement := model.Get("measurement").MustString("") + + resultFormat, err := model.Get("resultFormat").String() + if err != nil { + return nil, err + } + + tags, err := qp.parseTags(model) + if err != nil { + return nil, err + } + + groupBys, err := qp.parseGroupBy(model) + if err != nil { + return nil, err + } + + selects, err := qp.parseSelects(model) + if err != nil { + return nil, err + } + + if interval == "" { + dsInterval := dsInfo.JsonData.Get("timeInterval").MustString("") + if dsInterval != "" { + interval = dsInterval + } + } + + return &Query{ + Measurement: measurement, + Policy: policy, + ResultFormat: resultFormat, + GroupBy: groupBys, + Tags: tags, + Selects: selects, + RawQuery: rawQuery, + Interval: interval, + }, nil +} + +func (qp *InfluxdbQueryParser) parseSelects(model *simplejson.Json) ([]*Select, error) { + var result []*Select + + for _, selectObj := range model.Get("select").MustArray() { + selectJson := simplejson.NewFromAny(selectObj) + var parts Select + + for _, partObj := range selectJson.MustArray() { + part := simplejson.NewFromAny(partObj) + queryPart, err := qp.parseQueryPart(part) + if err != nil { + return nil, err + } + + parts = append(parts, *queryPart) + } + + result = append(result, &parts) + } + + return result, nil +} + +func (*InfluxdbQueryParser) parseTags(model *simplejson.Json) ([]*Tag, error) { + var result []*Tag + for _, t := range model.Get("tags").MustArray() { + tagJson := simplejson.NewFromAny(t) + tag := &Tag{} + var err error + + tag.Key, err = tagJson.Get("key").String() + if err != nil { + return nil, err + } + + tag.Value, err = tagJson.Get("value").String() + if err != nil { + return nil, err + } + + operator, err := tagJson.Get("operator").String() + if err == nil { + tag.Operator = operator + } + + condition, err := tagJson.Get("condition").String() + if err == nil { + tag.Condition = condition + } + + result = append(result, tag) + } + + return result, nil +} + +func (*InfluxdbQueryParser) parseQueryPart(model *simplejson.Json) (*QueryPart, error) { + typ, err := model.Get("type").String() + if err != nil { + return nil, err + } + + var params []string + for _, paramObj := range model.Get("params").MustArray() { + param := simplejson.NewFromAny(paramObj) + + stringParam, err := param.String() + if err == nil { + params = append(params, stringParam) + continue + } + + intParam, err := param.Int() + if err == nil { + params = append(params, strconv.Itoa(intParam)) + continue + } + + return nil, err + + } + + qp, err := NewQueryPart(typ, params) + if err != nil { + return nil, err + } + + return qp, nil +} + +func (qp *InfluxdbQueryParser) parseGroupBy(model *simplejson.Json) ([]*QueryPart, error) { + var result []*QueryPart + + for _, groupObj := range model.Get("groupBy").MustArray() { + groupJson := simplejson.NewFromAny(groupObj) + queryPart, err := qp.parseQueryPart(groupJson) + + if err != nil { + return nil, err + } + result = append(result, queryPart) + } + + return result, nil +} diff --git a/pkg/tsdb/influxdb/model_parser_test.go b/pkg/tsdb/influxdb/model_parser_test.go new file mode 100644 index 00000000000..8f43cc7d70f --- /dev/null +++ b/pkg/tsdb/influxdb/model_parser_test.go @@ -0,0 +1,178 @@ +package influxdb + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" + . "github.com/smartystreets/goconvey/convey" +) + +func TestInfluxdbQueryParser(t *testing.T) { + Convey("Influxdb query parser", t, func() { + + parser := &InfluxdbQueryParser{} + dsInfo := &tsdb.DataSourceInfo{ + JsonData: simplejson.New(), + } + + Convey("can parse influxdb json model", func() { + json := ` + { + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$interval" + ], + "type": "time" + }, + { + "params": [ + "datacenter" + ], + "type": "tag" + }, + { + "params": [ + "none" + ], + "type": "fill" + } + ], + "measurement": "logins.count", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "type": "field", + "params": [ + "value" + ] + }, + { + "type": "count", + "params": [] + } + ], + [ + { + "type": "field", + "params": [ + "value" + ] + }, + { + "type": "bottom", + "params": [ + 3 + ] + } + ], + [ + { + "type": "field", + "params": [ + "value" + ] + }, + { + "type": "mean", + "params": [] + }, + { + "type": "math", + "params": [ + " / 100" + ] + } + ] + ], + "tags": [ + { + "key": "datacenter", + "operator": "=", + "value": "America" + }, + { + "condition": "OR", + "key": "hostname", + "operator": "=", + "value": "server1" + } + ] + } + ` + dsInfo.JsonData.Set("timeInterval", ">20s") + modelJson, err := simplejson.NewJson([]byte(json)) + So(err, ShouldBeNil) + + res, err := parser.Parse(modelJson, dsInfo) + So(err, ShouldBeNil) + So(len(res.GroupBy), ShouldEqual, 3) + So(len(res.Selects), ShouldEqual, 3) + So(len(res.Tags), ShouldEqual, 2) + So(res.Interval, ShouldEqual, ">20s") + }) + + Convey("can part raw query json model", func() { + json := ` + { + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "interval": ">10s", + "policy": "default", + "query": "RawDummieQuery", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [ + + ], + "type": "mean" + } + ] + ], + "tags": [ + + ] + } + ` + + modelJson, err := simplejson.NewJson([]byte(json)) + So(err, ShouldBeNil) + + res, err := parser.Parse(modelJson, dsInfo) + So(err, ShouldBeNil) + So(res.RawQuery, ShouldEqual, "RawDummieQuery") + So(len(res.GroupBy), ShouldEqual, 2) + So(len(res.Selects), ShouldEqual, 1) + So(len(res.Tags), ShouldEqual, 0) + So(res.Interval, ShouldEqual, ">10s") + }) + }) +} diff --git a/pkg/tsdb/influxdb/models.go b/pkg/tsdb/influxdb/models.go new file mode 100644 index 00000000000..7903616ff22 --- /dev/null +++ b/pkg/tsdb/influxdb/models.go @@ -0,0 +1,49 @@ +package influxdb + +type Query struct { + Measurement string + Policy string + ResultFormat string + Tags []*Tag + GroupBy []*QueryPart + Selects []*Select + RawQuery string + + Interval string +} + +type Tag struct { + Key string + Operator string + Value string + Condition string +} + +type Select []QueryPart + +type InfluxDbSelect struct { + Type string +} + +type Response struct { + Results []Result + Err error +} + +type Result struct { + Series []Row + Messages []*Message + Err error +} + +type Message struct { + Level string `json:"level,omitempty"` + Text string `json:"text,omitempty"` +} + +type Row struct { + Name string `json:"name,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Columns []string `json:"columns,omitempty"` + Values [][]interface{} `json:"values,omitempty"` +} diff --git a/pkg/tsdb/influxdb/query_builder.go b/pkg/tsdb/influxdb/query_builder.go new file mode 100644 index 00000000000..bf0dc084c49 --- /dev/null +++ b/pkg/tsdb/influxdb/query_builder.go @@ -0,0 +1,116 @@ +package influxdb + +import ( + "fmt" + "strings" + + "github.com/grafana/grafana/pkg/tsdb" +) + +type QueryBuilder struct{} + +func (qb *QueryBuilder) Build(query *Query, queryContext *tsdb.QueryContext) (string, error) { + if query.RawQuery != "" { + q := query.RawQuery + + q = strings.Replace(q, "$timeFilter", qb.renderTimeFilter(query, queryContext), 1) + q = strings.Replace(q, "$interval", tsdb.CalculateInterval(queryContext.TimeRange), 1) + + return q, nil + } + + res := qb.renderSelectors(query, queryContext) + res += qb.renderMeasurement(query) + res += qb.renderWhereClause(query) + res += qb.renderTimeFilter(query, queryContext) + res += qb.renderGroupBy(query, queryContext) + + return res, nil +} + +func (qb *QueryBuilder) renderTags(query *Query) []string { + var res []string + for i, tag := range query.Tags { + str := "" + + if i > 0 { + if tag.Condition == "" { + str += "AND" + } else { + str += tag.Condition + } + str += " " + } + + res = append(res, fmt.Sprintf(`%s"%s" %s '%s'`, str, tag.Key, tag.Operator, tag.Value)) + } + + return res +} + +func (qb *QueryBuilder) renderTimeFilter(query *Query, queryContext *tsdb.QueryContext) string { + from := "now() - " + queryContext.TimeRange.From + to := "" + + if queryContext.TimeRange.To != "now" && queryContext.TimeRange.To != "" { + to = " and time < now() - " + strings.Replace(queryContext.TimeRange.To, "now-", "", 1) + } + + return fmt.Sprintf("time > %s%s", from, to) +} + +func (qb *QueryBuilder) renderSelectors(query *Query, queryContext *tsdb.QueryContext) string { + res := "SELECT " + + var selectors []string + for _, sel := range query.Selects { + + stk := "" + for _, s := range *sel { + stk = s.Render(query, queryContext, stk) + } + selectors = append(selectors, stk) + } + + return res + strings.Join(selectors, ", ") +} + +func (qb *QueryBuilder) renderMeasurement(query *Query) string { + policy := "" + if query.Policy == "" || query.Policy == "default" { + policy = "" + } else { + policy = `"` + query.Policy + `".` + } + return fmt.Sprintf(` FROM %s"%s"`, policy, query.Measurement) +} + +func (qb *QueryBuilder) renderWhereClause(query *Query) string { + res := " WHERE " + conditions := qb.renderTags(query) + res += strings.Join(conditions, " ") + if len(conditions) > 0 { + res += " AND " + } + + return res +} + +func (qb *QueryBuilder) renderGroupBy(query *Query, queryContext *tsdb.QueryContext) string { + groupBy := "" + for i, group := range query.GroupBy { + if i == 0 { + groupBy += " GROUP BY" + } + + if i > 0 && group.Type != "fill" { + groupBy += ", " //fill is so very special. fill is a creep, fill is a weirdo + } else { + groupBy += " " + } + + groupBy += group.Render(query, queryContext, "") + } + + return groupBy +} diff --git a/pkg/tsdb/influxdb/query_builder_test.go b/pkg/tsdb/influxdb/query_builder_test.go new file mode 100644 index 00000000000..4552f85b28a --- /dev/null +++ b/pkg/tsdb/influxdb/query_builder_test.go @@ -0,0 +1,87 @@ +package influxdb + +import ( + "testing" + + "github.com/grafana/grafana/pkg/tsdb" + . "github.com/smartystreets/goconvey/convey" +) + +func TestInfluxdbQueryBuilder(t *testing.T) { + + Convey("Influxdb query builder", t, func() { + builder := QueryBuilder{} + + qp1, _ := NewQueryPart("field", []string{"value"}) + qp2, _ := NewQueryPart("mean", []string{}) + + groupBy1, _ := NewQueryPart("time", []string{"$interval"}) + groupBy2, _ := NewQueryPart("tag", []string{"datacenter"}) + groupBy3, _ := NewQueryPart("fill", []string{"null"}) + + tag1 := &Tag{Key: "hostname", Value: "server1", Operator: "="} + tag2 := &Tag{Key: "hostname", Value: "server2", Operator: "=", Condition: "OR"} + + queryContext := &tsdb.QueryContext{ + TimeRange: tsdb.NewTimeRange("5m", "now"), + } + + Convey("can build simple query", func() { + query := &Query{ + Selects: []*Select{{*qp1, *qp2}}, + Measurement: "cpu", + Policy: "policy", + GroupBy: []*QueryPart{groupBy1, groupBy3}, + Interval: "10s", + } + + rawQuery, err := builder.Build(query, queryContext) + So(err, ShouldBeNil) + So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "policy"."cpu" WHERE time > now() - 5m GROUP BY time(10s) fill(null)`) + }) + + Convey("can build query with group bys", func() { + query := &Query{ + Selects: []*Select{{*qp1, *qp2}}, + Measurement: "cpu", + GroupBy: []*QueryPart{groupBy1, groupBy2, groupBy3}, + Tags: []*Tag{tag1, tag2}, + Interval: "5s", + } + + rawQuery, err := builder.Build(query, queryContext) + So(err, ShouldBeNil) + So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE "hostname" = 'server1' OR "hostname" = 'server2' AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`) + }) + + Convey("can render time range", func() { + query := Query{} + builder := &QueryBuilder{} + Convey("render from: 2h to now-1h", func() { + query := Query{} + queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("2h", "now-1h")} + So(builder.renderTimeFilter(&query, queryContext), ShouldEqual, "time > now() - 2h and time < now() - 1h") + }) + + Convey("render from: 10m", func() { + queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("10m", "now")} + So(builder.renderTimeFilter(&query, queryContext), ShouldEqual, "time > now() - 10m") + }) + }) + + Convey("can build query from raw query", func() { + query := &Query{ + Selects: []*Select{{*qp1, *qp2}}, + Measurement: "cpu", + Policy: "policy", + GroupBy: []*QueryPart{groupBy1, groupBy3}, + Interval: "10s", + RawQuery: "Raw query", + } + + rawQuery, err := builder.Build(query, queryContext) + So(err, ShouldBeNil) + So(rawQuery, ShouldEqual, `Raw query`) + }) + }) +} diff --git a/pkg/tsdb/influxdb/query_part.go b/pkg/tsdb/influxdb/query_part.go new file mode 100644 index 00000000000..d634bc5c817 --- /dev/null +++ b/pkg/tsdb/influxdb/query_part.go @@ -0,0 +1,166 @@ +package influxdb + +import ( + "fmt" + "strings" + "time" + + "github.com/grafana/grafana/pkg/tsdb" +) + +var renders map[string]QueryDefinition + +type DefinitionParameters struct { + Name string + Type string +} + +type QueryDefinition struct { + Renderer func(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string + Params []DefinitionParameters +} + +func init() { + renders = make(map[string]QueryDefinition) + + renders["field"] = QueryDefinition{Renderer: fieldRenderer} + + renders["spread"] = QueryDefinition{Renderer: functionRenderer} + renders["count"] = QueryDefinition{Renderer: functionRenderer} + renders["distinct"] = QueryDefinition{Renderer: functionRenderer} + renders["integral"] = QueryDefinition{Renderer: functionRenderer} + renders["mean"] = QueryDefinition{Renderer: functionRenderer} + renders["median"] = QueryDefinition{Renderer: functionRenderer} + renders["sum"] = QueryDefinition{Renderer: functionRenderer} + + renders["derivative"] = QueryDefinition{ + Renderer: functionRenderer, + Params: []DefinitionParameters{{Name: "duration", Type: "interval"}}, + } + + renders["non_negative_derivative"] = QueryDefinition{ + Renderer: functionRenderer, + Params: []DefinitionParameters{{Name: "duration", Type: "interval"}}, + } + renders["difference"] = QueryDefinition{Renderer: functionRenderer} + renders["moving_average"] = QueryDefinition{ + Renderer: functionRenderer, + Params: []DefinitionParameters{{Name: "window", Type: "number"}}, + } + renders["stddev"] = QueryDefinition{Renderer: functionRenderer} + renders["time"] = QueryDefinition{ + Renderer: functionRenderer, + Params: []DefinitionParameters{{Name: "interval", Type: "time"}}, + } + renders["fill"] = QueryDefinition{ + Renderer: functionRenderer, + Params: []DefinitionParameters{{Name: "fill", Type: "string"}}, + } + renders["elapsed"] = QueryDefinition{ + Renderer: functionRenderer, + Params: []DefinitionParameters{{Name: "duration", Type: "interval"}}, + } + renders["bottom"] = QueryDefinition{ + Renderer: functionRenderer, + Params: []DefinitionParameters{{Name: "count", Type: "int"}}, + } + + renders["first"] = QueryDefinition{Renderer: functionRenderer} + renders["last"] = QueryDefinition{Renderer: functionRenderer} + renders["max"] = QueryDefinition{Renderer: functionRenderer} + renders["min"] = QueryDefinition{Renderer: functionRenderer} + renders["percentile"] = QueryDefinition{ + Renderer: functionRenderer, + Params: []DefinitionParameters{{Name: "nth", Type: "int"}}, + } + renders["top"] = QueryDefinition{ + Renderer: functionRenderer, + Params: []DefinitionParameters{{Name: "count", Type: "int"}}, + } + renders["tag"] = QueryDefinition{ + Renderer: fieldRenderer, + Params: []DefinitionParameters{{Name: "tag", Type: "string"}}, + } + + renders["math"] = QueryDefinition{Renderer: suffixRenderer} + renders["alias"] = QueryDefinition{Renderer: aliasRenderer} +} + +func fieldRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string { + if part.Params[0] == "*" { + return "*" + } + return fmt.Sprintf(`"%s"`, part.Params[0]) +} + +func getDefinedInterval(query *Query, queryContext *tsdb.QueryContext) string { + setInterval := strings.Replace(strings.Replace(query.Interval, "<", "", 1), ">", "", 1) + defaultInterval := tsdb.CalculateInterval(queryContext.TimeRange) + + if strings.Contains(query.Interval, ">") { + parsedDefaultInterval, err := time.ParseDuration(defaultInterval) + parsedSetInterval, err2 := time.ParseDuration(setInterval) + + if err == nil && err2 == nil && parsedDefaultInterval > parsedSetInterval { + return defaultInterval + } + } + + return setInterval +} + +func functionRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string { + for i, param := range part.Params { + if param == "$interval" { + if query.Interval != "" { + part.Params[i] = getDefinedInterval(query, queryContext) + } else { + part.Params[i] = tsdb.CalculateInterval(queryContext.TimeRange) + } + } + } + + if innerExpr != "" { + part.Params = append([]string{innerExpr}, part.Params...) + } + + params := strings.Join(part.Params, ", ") + + return fmt.Sprintf("%s(%s)", part.Type, params) +} + +func suffixRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string { + return fmt.Sprintf("%s %s", innerExpr, part.Params[0]) +} + +func aliasRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string { + return fmt.Sprintf(`%s AS "%s"`, innerExpr, part.Params[0]) +} + +func (r QueryDefinition) Render(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string { + return r.Renderer(query, queryContext, part, innerExpr) +} + +func NewQueryPart(typ string, params []string) (*QueryPart, error) { + def, exist := renders[typ] + + if !exist { + return nil, fmt.Errorf("Missing query definition for %s", typ) + } + + return &QueryPart{ + Type: typ, + Params: params, + Def: def, + }, nil +} + +type QueryPart struct { + Def QueryDefinition + Type string + Params []string +} + +func (qp *QueryPart) Render(query *Query, queryContext *tsdb.QueryContext, expr string) string { + return qp.Def.Renderer(query, queryContext, qp, expr) +} diff --git a/pkg/tsdb/influxdb/query_part_test.go b/pkg/tsdb/influxdb/query_part_test.go new file mode 100644 index 00000000000..456a7fb8d59 --- /dev/null +++ b/pkg/tsdb/influxdb/query_part_test.go @@ -0,0 +1,93 @@ +package influxdb + +import ( + "testing" + + "github.com/grafana/grafana/pkg/tsdb" + . "github.com/smartystreets/goconvey/convey" +) + +func TestInfluxdbQueryPart(t *testing.T) { + Convey("Influxdb query parts", t, func() { + + queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("5m", "now")} + query := &Query{} + + Convey("render field ", func() { + part, err := NewQueryPart("field", []string{"value"}) + So(err, ShouldBeNil) + + res := part.Render(query, queryContext, "value") + So(res, ShouldEqual, `"value"`) + }) + + Convey("render nested part", func() { + part, err := NewQueryPart("derivative", []string{"10s"}) + So(err, ShouldBeNil) + + res := part.Render(query, queryContext, "mean(value)") + So(res, ShouldEqual, "derivative(mean(value), 10s)") + }) + + Convey("render bottom", func() { + part, err := NewQueryPart("bottom", []string{"3"}) + So(err, ShouldBeNil) + + res := part.Render(query, queryContext, "value") + So(res, ShouldEqual, "bottom(value, 3)") + }) + + Convey("render time", func() { + part, err := NewQueryPart("time", []string{"$interval"}) + So(err, ShouldBeNil) + + res := part.Render(query, queryContext, "") + So(res, ShouldEqual, "time(200ms)") + }) + + Convey("render time interval >10s", func() { + part, err := NewQueryPart("time", []string{"$interval"}) + So(err, ShouldBeNil) + + query.Interval = ">10s" + + res := part.Render(query, queryContext, "") + So(res, ShouldEqual, "time(10s)") + }) + + Convey("render time interval >1s and higher interval calculation", func() { + part, err := NewQueryPart("time", []string{"$interval"}) + queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("1y", "now")} + So(err, ShouldBeNil) + + query.Interval = ">1s" + + res := part.Render(query, queryContext, "") + So(res, ShouldEqual, "time(168h)") + }) + + Convey("render spread", func() { + part, err := NewQueryPart("spread", []string{}) + So(err, ShouldBeNil) + + res := part.Render(query, queryContext, "value") + So(res, ShouldEqual, `spread(value)`) + }) + + Convey("render suffix", func() { + part, err := NewQueryPart("math", []string{"/ 100"}) + So(err, ShouldBeNil) + + res := part.Render(query, queryContext, "mean(value)") + So(res, ShouldEqual, "mean(value) / 100") + }) + + Convey("render alias", func() { + part, err := NewQueryPart("alias", []string{"test"}) + So(err, ShouldBeNil) + + res := part.Render(query, queryContext, "mean(value)") + So(res, ShouldEqual, `mean(value) AS "test"`) + }) + }) +} diff --git a/pkg/tsdb/influxdb/response_parser.go b/pkg/tsdb/influxdb/response_parser.go new file mode 100644 index 00000000000..44afa910b27 --- /dev/null +++ b/pkg/tsdb/influxdb/response_parser.go @@ -0,0 +1,94 @@ +package influxdb + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/grafana/grafana/pkg/tsdb" + "gopkg.in/guregu/null.v3" +) + +type ResponseParser struct{} + +func (rp *ResponseParser) Parse(response *Response) *tsdb.QueryResult { + queryRes := tsdb.NewQueryResult() + + for _, result := range response.Results { + queryRes.Series = append(queryRes.Series, rp.transformRows(result.Series, queryRes)...) + } + + return queryRes +} + +func (rp *ResponseParser) transformRows(rows []Row, queryResult *tsdb.QueryResult) tsdb.TimeSeriesSlice { + var result tsdb.TimeSeriesSlice + + for _, row := range rows { + for columnIndex, column := range row.Columns { + if column == "time" { + continue + } + + var points tsdb.TimeSeriesPoints + for _, valuePair := range row.Values { + point, err := rp.parseTimepoint(valuePair, columnIndex) + if err == nil { + points = append(points, point) + } + } + result = append(result, &tsdb.TimeSeries{ + Name: rp.formatSerieName(row, column), + Points: points, + }) + } + } + + return result +} + +func (rp *ResponseParser) formatSerieName(row Row, column string) string { + var tags []string + + for k, v := range row.Tags { + tags = append(tags, fmt.Sprintf("%s: %s", k, v)) + } + + tagText := "" + if len(tags) > 0 { + tagText = fmt.Sprintf(" { %s }", strings.Join(tags, " ")) + } + + return fmt.Sprintf("%s.%s%s", row.Name, column, tagText) +} + +func (rp *ResponseParser) parseTimepoint(valuePair []interface{}, valuePosition int) (tsdb.TimePoint, error) { + var value null.Float = rp.parseValue(valuePair[valuePosition]) + + timestampNumber, _ := valuePair[0].(json.Number) + timestamp, err := timestampNumber.Float64() + if err != nil { + return tsdb.TimePoint{}, err + } + + return tsdb.NewTimePoint(value, timestamp), nil +} + +func (rp *ResponseParser) parseValue(value interface{}) null.Float { + number, ok := value.(json.Number) + if !ok { + return null.FloatFromPtr(nil) + } + + fvalue, err := number.Float64() + if err == nil { + return null.FloatFrom(fvalue) + } + + ivalue, err := number.Int64() + if err == nil { + return null.FloatFrom(float64(ivalue)) + } + + return null.FloatFromPtr(nil) +} diff --git a/pkg/tsdb/influxdb/response_parser_test.go b/pkg/tsdb/influxdb/response_parser_test.go new file mode 100644 index 00000000000..b45f98a1fff --- /dev/null +++ b/pkg/tsdb/influxdb/response_parser_test.go @@ -0,0 +1,59 @@ +package influxdb + +import ( + "encoding/json" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestInfluxdbResponseParser(t *testing.T) { + Convey("Influxdb response parser", t, func() { + + parser := &ResponseParser{} + + response := &Response{ + Results: []Result{ + Result{ + Series: []Row{ + { + Name: "cpu", + Columns: []string{"time", "mean", "sum"}, + Tags: map[string]string{"datacenter": "America"}, + Values: [][]interface{}{ + {json.Number("111"), json.Number("222"), json.Number("333")}, + {json.Number("111"), json.Number("222"), json.Number("333")}, + {json.Number("111"), json.Number("null"), json.Number("333")}, + }, + }, + }, + }, + }, + } + + result := parser.Parse(response) + + Convey("can parse all series", func() { + So(len(result.Series), ShouldEqual, 2) + }) + + Convey("can parse all points", func() { + So(len(result.Series[0].Points), ShouldEqual, 3) + So(len(result.Series[1].Points), ShouldEqual, 3) + }) + + Convey("can parse multi row result", func() { + So(result.Series[0].Points[1][0].Float64, ShouldEqual, float64(222)) + So(result.Series[1].Points[1][0].Float64, ShouldEqual, float64(333)) + }) + + Convey("can parse null points", func() { + So(result.Series[0].Points[2][0].Valid, ShouldBeFalse) + }) + + Convey("can format serie names", func() { + So(result.Series[0].Name, ShouldEqual, "cpu.mean { datacenter: America }") + So(result.Series[1].Name, ShouldEqual, "cpu.sum { datacenter: America }") + }) + }) +} diff --git a/pkg/tsdb/interval.go b/pkg/tsdb/interval.go new file mode 100644 index 00000000000..71caf122c13 --- /dev/null +++ b/pkg/tsdb/interval.go @@ -0,0 +1,145 @@ +package tsdb + +import ( + "fmt" + "time" +) + +var ( + defaultRes int64 = 1500 + minInterval time.Duration = 1 * time.Millisecond + year time.Duration = time.Hour * 24 * 365 + day time.Duration = time.Hour * 24 * 365 +) + +func CalculateInterval(timerange *TimeRange) string { + interval := time.Duration((timerange.MustGetTo().UnixNano() - timerange.MustGetFrom().UnixNano()) / defaultRes) + + if interval < minInterval { + return formatDuration(minInterval) + } + + return formatDuration(roundInterval(interval)) +} + +func formatDuration(inter time.Duration) string { + if inter >= year { + return fmt.Sprintf("%dy", inter/year) + } + + if inter >= day { + return fmt.Sprintf("%dd", inter/day) + } + + if inter >= time.Hour { + return fmt.Sprintf("%dh", inter/time.Hour) + } + + if inter >= time.Minute { + return fmt.Sprintf("%dm", inter/time.Minute) + } + + if inter >= time.Second { + return fmt.Sprintf("%ds", inter/time.Second) + } + + if inter >= time.Millisecond { + return fmt.Sprintf("%dms", inter/time.Millisecond) + } + + return "1ms" +} + +func roundInterval(interval time.Duration) time.Duration { + switch true { + // 0.015s + case interval <= 15*time.Millisecond: + return time.Millisecond * 10 // 0.01s + // 0.035s + case interval <= 35*time.Millisecond: + return time.Millisecond * 20 // 0.02s + // 0.075s + case interval <= 75*time.Millisecond: + return time.Millisecond * 50 // 0.05s + // 0.15s + case interval <= 150*time.Millisecond: + return time.Millisecond * 100 // 0.1s + // 0.35s + case interval <= 350*time.Millisecond: + return time.Millisecond * 200 // 0.2s + // 0.75s + case interval <= 750*time.Millisecond: + return time.Millisecond * 500 // 0.5s + // 1.5s + case interval <= 1500*time.Millisecond: + return time.Millisecond * 1000 // 1s + // 3.5s + case interval <= 3500*time.Millisecond: + return time.Millisecond * 2000 // 2s + // 7.5s + case interval <= 7500*time.Millisecond: + return time.Millisecond * 5000 // 5s + // 12.5s + case interval <= 12500*time.Millisecond: + return time.Millisecond * 10000 // 10s + // 17.5s + case interval <= 17500*time.Millisecond: + return time.Millisecond * 15000 // 15s + // 25s + case interval <= 25000*time.Millisecond: + return time.Millisecond * 20000 // 20s + // 45s + case interval <= 45000*time.Millisecond: + return time.Millisecond * 30000 // 30s + // 1.5m + case interval <= 90000*time.Millisecond: + return time.Millisecond * 60000 // 1m + // 3.5m + case interval <= 210000*time.Millisecond: + return time.Millisecond * 120000 // 2m + // 7.5m + case interval <= 450000*time.Millisecond: + return time.Millisecond * 300000 // 5m + // 12.5m + case interval <= 750000*time.Millisecond: + return time.Millisecond * 600000 // 10m + // 12.5m + case interval <= 1050000*time.Millisecond: + return time.Millisecond * 900000 // 15m + // 25m + case interval <= 1500000*time.Millisecond: + return time.Millisecond * 1200000 // 20m + // 45m + case interval <= 2700000*time.Millisecond: + return time.Millisecond * 1800000 // 30m + // 1.5h + case interval <= 5400000*time.Millisecond: + return time.Millisecond * 3600000 // 1h + // 2.5h + case interval <= 9000000*time.Millisecond: + return time.Millisecond * 7200000 // 2h + // 4.5h + case interval <= 16200000*time.Millisecond: + return time.Millisecond * 10800000 // 3h + // 9h + case interval <= 32400000*time.Millisecond: + return time.Millisecond * 21600000 // 6h + // 24h + case interval <= 86400000*time.Millisecond: + return time.Millisecond * 43200000 // 12h + // 48h + case interval <= 172800000*time.Millisecond: + return time.Millisecond * 86400000 // 24h + // 1w + case interval <= 604800000*time.Millisecond: + return time.Millisecond * 86400000 // 24h + // 3w + case interval <= 1814400000*time.Millisecond: + return time.Millisecond * 604800000 // 1w + // 2y + case interval < 3628800000*time.Millisecond: + return time.Millisecond * 2592000000 // 30d + default: + return time.Millisecond * 31536000000 // 1y + } +} diff --git a/pkg/tsdb/interval_test.go b/pkg/tsdb/interval_test.go new file mode 100644 index 00000000000..c06e1879668 --- /dev/null +++ b/pkg/tsdb/interval_test.go @@ -0,0 +1,57 @@ +package tsdb + +import ( + "testing" + "time" + + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestInterval(t *testing.T) { + Convey("Default interval ", t, func() { + setting.NewConfigContext(&setting.CommandLineArgs{ + HomePath: "../../", + }) + + Convey("for 5min", func() { + tr := NewTimeRange("5m", "now") + + interval := CalculateInterval(tr) + So(interval, ShouldEqual, "200ms") + }) + + Convey("for 15min", func() { + tr := NewTimeRange("15m", "now") + + interval := CalculateInterval(tr) + So(interval, ShouldEqual, "500ms") + }) + + Convey("for 30min", func() { + tr := NewTimeRange("30m", "now") + + interval := CalculateInterval(tr) + So(interval, ShouldEqual, "1s") + }) + + Convey("for 1h", func() { + tr := NewTimeRange("1h", "now") + + interval := CalculateInterval(tr) + So(interval, ShouldEqual, "2s") + }) + + Convey("Round interval", func() { + So(roundInterval(time.Millisecond*30), ShouldEqual, time.Millisecond*20) + So(roundInterval(time.Millisecond*45), ShouldEqual, time.Millisecond*50) + }) + + Convey("Format value", func() { + So(formatDuration(time.Second*61), ShouldEqual, "1m") + So(formatDuration(time.Millisecond*30), ShouldEqual, "30ms") + So(formatDuration(time.Hour*23), ShouldEqual, "23h") + So(formatDuration(time.Hour*24*367), ShouldEqual, "1y") + }) + }) +} diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index bbf7bba7ac7..366709cfba7 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -39,6 +39,7 @@ type DataSourceInfo struct { BasicAuth bool BasicAuthUser string BasicAuthPassword string + JsonData *simplejson.Json } type BatchTiming struct { @@ -51,6 +52,11 @@ type BatchResult struct { Timings *BatchTiming } +func (br *BatchResult) WithError(err error) *BatchResult { + br.Error = err + return br +} + type QueryResult struct { Error error `json:"error"` RefId string `json:"refId"` @@ -72,15 +78,15 @@ func NewQueryResult() *QueryResult { } } -func NewTimePoint(value float64, timestamp float64) TimePoint { - return TimePoint{null.FloatFrom(value), null.FloatFrom(timestamp)} +func NewTimePoint(value null.Float, timestamp float64) TimePoint { + return TimePoint{value, null.FloatFrom(timestamp)} } func NewTimeSeriesPointsFromArgs(values ...float64) TimeSeriesPoints { points := make(TimeSeriesPoints, 0) for i := 0; i < len(values); i += 2 { - points = append(points, NewTimePoint(values[i], values[i+1])) + points = append(points, NewTimePoint(null.FloatFrom(values[i]), values[i+1])) } return points diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go new file mode 100644 index 00000000000..3ecd52ca723 --- /dev/null +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -0,0 +1,212 @@ +package opentsdb + +import ( + "context" + "crypto/tls" + "fmt" + "path" + "strconv" + "strings" + "time" + + "golang.org/x/net/context/ctxhttp" + + "io/ioutil" + "net/http" + "net/url" + "encoding/json" + + "gopkg.in/guregu/null.v3" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb" +) + +type OpenTsdbExecutor struct { + *tsdb.DataSourceInfo +} + +func NewOpenTsdbExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { + return &OpenTsdbExecutor{dsInfo} +} + +var ( + plog log.Logger + HttpClient *http.Client +) + +func init() { + plog = log.New("tsdb.opentsdb") + tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + HttpClient = &http.Client{ + Timeout: time.Duration(15 * time.Second), + Transport: tr, + } +} + +func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult { + result := &tsdb.BatchResult{} + + var tsdbQuery OpenTsdbQuery + + tsdbQuery.Start = queryContext.TimeRange.GetFromAsMsEpoch() + tsdbQuery.End = queryContext.TimeRange.GetToAsMsEpoch() + + for _ , query := range queries { + metric := e.buildMetric(query) + tsdbQuery.Queries = append(tsdbQuery.Queries, metric) + } + + if setting.Env == setting.DEV { + plog.Debug("OpenTsdb request", "params", tsdbQuery) + } + + req, err := e.createRequest(tsdbQuery) + if err != nil { + result.Error = err + return result + } + + res, err := ctxhttp.Do(ctx, HttpClient, req) + if err != nil { + result.Error = err + return result + } + + queryResult, err := e.parseResponse(tsdbQuery, res) + if err != nil { + return result.WithError(err) + } + + result.QueryResults = queryResult + return result +} + +func (e *OpenTsdbExecutor) createRequest(data OpenTsdbQuery) (*http.Request, error) { + u, _ := url.Parse(e.Url) + u.Path = path.Join(u.Path, "api/query") + + postData, err := json.Marshal(data) + + req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(string(postData))) + if err != nil { + plog.Info("Failed to create request", "error", err) + return nil, fmt.Errorf("Failed to create request. error: %v", err) + } + + req.Header.Set("Content-Type", "application/json") + if e.BasicAuth { + req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword) + } + + return req, err +} + +func (e *OpenTsdbExecutor) parseResponse(query OpenTsdbQuery, res *http.Response) (map[string]*tsdb.QueryResult, error) { + + queryResults := make(map[string]*tsdb.QueryResult) + queryRes := tsdb.NewQueryResult() + + body, err := ioutil.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return nil, err + } + + if res.StatusCode/100 != 2 { + plog.Info("Request failed", "status", res.Status, "body", string(body)) + return nil, fmt.Errorf("Request failed status: %v", res.Status) + } + + var data []OpenTsdbResponse + err = json.Unmarshal(body, &data) + if err != nil { + plog.Info("Failed to unmarshal opentsdb response", "error", err, "status", res.Status, "body", string(body)) + return nil, err + } + + for _, val := range data { + series := tsdb.TimeSeries{ + Name: val.Metric, + } + + for timeString, value := range val.DataPoints { + timestamp, err := strconv.ParseFloat(timeString, 64) + if err != nil { + plog.Info("Failed to unmarshal opentsdb timestamp", "timestamp", timeString) + return nil, err + } + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(value), timestamp)) + } + + queryRes.Series = append(queryRes.Series, &series) + } + + queryResults["A"] = queryRes + return queryResults, nil +} + +func (e *OpenTsdbExecutor) buildMetric(query *tsdb.Query) (map[string]interface{}) { + + metric := make(map[string]interface{}) + + // Setting metric and aggregator + metric["metric"] = query.Model.Get("metric").MustString() + metric["aggregator"] = query.Model.Get("aggregator").MustString() + + // Setting downsampling options + disableDownsampling := query.Model.Get("disableDownsampling").MustBool() + if !disableDownsampling { + downsampleInterval := query.Model.Get("downsampleInterval").MustString() + if downsampleInterval == "" { + downsampleInterval = "1m" //default value for blank + } + downsample := downsampleInterval + "-" + query.Model.Get("downsampleAggregator").MustString() + if query.Model.Get("downsampleFillPolicy").MustString() != "none" { + metric["downsample"] = downsample + "-" + query.Model.Get("downsampleFillPolicy").MustString() + } else { + metric["downsample"] = downsample + } + } + + // Setting rate options + if query.Model.Get("shouldComputeRate").MustBool() { + + metric["rate"] = true + rateOptions := make(map[string]interface{}) + rateOptions["counter"] = query.Model.Get("isCounter").MustBool() + + counterMax, counterMaxCheck := query.Model.CheckGet("counterMax") + if counterMaxCheck { + rateOptions["counterMax"] = counterMax.MustFloat64() + } + + resetValue, resetValueCheck := query.Model.CheckGet("counterResetValue") + if resetValueCheck { + rateOptions["resetValue"] = resetValue.MustFloat64() + } + + metric["rateOptions"] = rateOptions + } + + // Setting tags + tags, tagsCheck := query.Model.CheckGet("tags") + if tagsCheck && len(tags.MustMap()) > 0 { + metric["tags"] = tags.MustMap() + } + + // Setting filters + filters, filtersCheck := query.Model.CheckGet("filters") + if filtersCheck && len(filters.MustArray()) > 0 { + metric["filters"] = filters.MustArray() + } + + return metric + +} diff --git a/pkg/tsdb/opentsdb/opentsdb_test.go b/pkg/tsdb/opentsdb/opentsdb_test.go new file mode 100644 index 00000000000..905c42b0633 --- /dev/null +++ b/pkg/tsdb/opentsdb/opentsdb_test.go @@ -0,0 +1,176 @@ +package opentsdb + +import ( + "testing" + + "github.com/grafana/grafana/pkg/tsdb" + . "github.com/smartystreets/goconvey/convey" + "github.com/grafana/grafana/pkg/components/simplejson" +) + +func TestOpenTsdbExecutor(t *testing.T) { + Convey("OpenTsdb query testing", t, func() { + + exec := &OpenTsdbExecutor{} + + Convey("Build metric with downsampling enabled", func() { + + query := &tsdb.Query{ + Model: simplejson.New(), + } + + query.Model.Set("metric", "cpu.average.percent") + query.Model.Set("aggregator", "avg") + query.Model.Set("disableDownsampling", false) + query.Model.Set("downsampleInterval", "") + query.Model.Set("downsampleAggregator","avg") + query.Model.Set("downsampleFillPolicy","none") + + metric := exec.buildMetric(query) + + So(len(metric), ShouldEqual, 3) + So(metric["metric"], ShouldEqual, "cpu.average.percent") + So(metric["aggregator"], ShouldEqual, "avg") + So(metric["downsample"], ShouldEqual, "1m-avg") + + }) + + Convey("Build metric with downsampling diabled", func() { + + query := &tsdb.Query{ + Model: simplejson.New(), + } + + query.Model.Set("metric", "cpu.average.percent") + query.Model.Set("aggregator", "avg") + query.Model.Set("disableDownsampling", true) + query.Model.Set("downsampleInterval", "") + query.Model.Set("downsampleAggregator","avg") + query.Model.Set("downsampleFillPolicy","none") + + metric := exec.buildMetric(query) + + So(len(metric), ShouldEqual, 2) + So(metric["metric"], ShouldEqual, "cpu.average.percent") + So(metric["aggregator"], ShouldEqual, "avg") + + }) + + Convey("Build metric with downsampling enabled with params", func() { + + query := &tsdb.Query{ + Model: simplejson.New(), + } + + query.Model.Set("metric", "cpu.average.percent") + query.Model.Set("aggregator", "avg") + query.Model.Set("disableDownsampling", false) + query.Model.Set("downsampleInterval", "5m") + query.Model.Set("downsampleAggregator","sum") + query.Model.Set("downsampleFillPolicy","null") + + metric := exec.buildMetric(query) + + So(len(metric), ShouldEqual, 3) + So(metric["metric"], ShouldEqual, "cpu.average.percent") + So(metric["aggregator"], ShouldEqual, "avg") + So(metric["downsample"], ShouldEqual, "5m-sum-null") + }) + + Convey("Build metric with tags with downsampling disabled", func() { + + query := &tsdb.Query{ + Model: simplejson.New(), + } + + query.Model.Set("metric", "cpu.average.percent") + query.Model.Set("aggregator", "avg") + query.Model.Set("disableDownsampling", true) + query.Model.Set("downsampleInterval", "5m") + query.Model.Set("downsampleAggregator","sum") + query.Model.Set("downsampleFillPolicy","null") + + tags := simplejson.New() + tags.Set("env", "prod") + tags.Set("app", "grafana") + query.Model.Set("tags", tags.MustMap()) + + metric := exec.buildMetric(query) + + So(len(metric), ShouldEqual, 3) + So(metric["metric"], ShouldEqual, "cpu.average.percent") + So(metric["aggregator"], ShouldEqual, "avg") + So(metric["downsample"], ShouldEqual, nil) + So(len(metric["tags"].(map[string]interface{})), ShouldEqual, 2) + So(metric["tags"].(map[string]interface{})["env"], ShouldEqual, "prod") + So(metric["tags"].(map[string]interface{})["app"], ShouldEqual, "grafana") + So(metric["tags"].(map[string]interface{})["ip"], ShouldEqual, nil) + }) + + Convey("Build metric with rate enabled but counter disabled", func() { + + query := &tsdb.Query{ + Model: simplejson.New(), + } + + query.Model.Set("metric", "cpu.average.percent") + query.Model.Set("aggregator", "avg") + query.Model.Set("disableDownsampling", true) + query.Model.Set("shouldComputeRate", true) + query.Model.Set("isCounter",false) + + tags := simplejson.New() + tags.Set("env", "prod") + tags.Set("app", "grafana") + query.Model.Set("tags", tags.MustMap()) + + metric := exec.buildMetric(query) + + So(len(metric), ShouldEqual, 5) + So(metric["metric"], ShouldEqual, "cpu.average.percent") + So(metric["aggregator"], ShouldEqual, "avg") + So(len(metric["tags"].(map[string]interface{})), ShouldEqual, 2) + So(metric["tags"].(map[string]interface{})["env"], ShouldEqual, "prod") + So(metric["tags"].(map[string]interface{})["app"], ShouldEqual, "grafana") + So(metric["tags"].(map[string]interface{})["ip"], ShouldEqual, nil) + So(metric["rate"], ShouldEqual, true) + So(metric["rateOptions"].(map[string]interface{})["counter"], ShouldEqual, false) + }) + + Convey("Build metric with rate and counter enabled", func() { + + query := &tsdb.Query{ + Model: simplejson.New(), + } + + query.Model.Set("metric", "cpu.average.percent") + query.Model.Set("aggregator", "avg") + query.Model.Set("disableDownsampling", true) + query.Model.Set("shouldComputeRate", true) + query.Model.Set("isCounter",true) + query.Model.Set("counterMax",45) + query.Model.Set("counterResetValue",60) + + tags := simplejson.New() + tags.Set("env", "prod") + tags.Set("app", "grafana") + query.Model.Set("tags", tags.MustMap()) + + metric := exec.buildMetric(query) + + So(len(metric), ShouldEqual, 5) + So(metric["metric"], ShouldEqual, "cpu.average.percent") + So(metric["aggregator"], ShouldEqual, "avg") + So(len(metric["tags"].(map[string]interface{})), ShouldEqual, 2) + So(metric["tags"].(map[string]interface{})["env"], ShouldEqual, "prod") + So(metric["tags"].(map[string]interface{})["app"], ShouldEqual, "grafana") + So(metric["tags"].(map[string]interface{})["ip"], ShouldEqual, nil) + So(metric["rate"], ShouldEqual, true) + So(len(metric["rateOptions"].(map[string]interface{})), ShouldEqual, 3) + So(metric["rateOptions"].(map[string]interface{})["counter"], ShouldEqual, true) + So(metric["rateOptions"].(map[string]interface{})["counterMax"], ShouldEqual, 45) + So(metric["rateOptions"].(map[string]interface{})["resetValue"], ShouldEqual, 60) + }) + + }) +} \ No newline at end of file diff --git a/pkg/tsdb/opentsdb/types.go b/pkg/tsdb/opentsdb/types.go new file mode 100644 index 00000000000..abd216dc47d --- /dev/null +++ b/pkg/tsdb/opentsdb/types.go @@ -0,0 +1,12 @@ +package opentsdb + +type OpenTsdbQuery struct { + Start int64 `json:"start"` + End int64 `json:"end"` + Queries []map[string]interface{} `json:"queries"` +} + +type OpenTsdbResponse struct { + Metric string `json:"metric"` + DataPoints map[string]float64 `json:"dps"` +} \ No newline at end of file diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index f7e68662efa..6dc4146ad0e 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -1,17 +1,19 @@ package prometheus import ( + "context" "fmt" "net/http" "regexp" "strings" "time" + "gopkg.in/guregu/null.v3" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/tsdb" "github.com/prometheus/client_golang/api/prometheus" pmodel "github.com/prometheus/common/model" - "golang.org/x/net/context" ) type PrometheusExecutor struct { @@ -45,17 +47,17 @@ func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) { return prometheus.NewQueryAPI(client), nil } -func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult { +func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult { result := &tsdb.BatchResult{} client, err := e.getClient() if err != nil { - return resultWithError(result, err) + return result.WithError(err) } query, err := parseQuery(queries, queryContext) if err != nil { - return resultWithError(result, err) + return result.WithError(err) } timeRange := prometheus.Range{ @@ -64,15 +66,15 @@ func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb Step: query.Step, } - value, err := client.QueryRange(context.Background(), query.Expr, timeRange) + value, err := client.QueryRange(ctx, query.Expr, timeRange) if err != nil { - return resultWithError(result, err) + return result.WithError(err) } queryResult, err := parseResponse(value, query) if err != nil { - return resultWithError(result, err) + return result.WithError(err) } result.QueryResults = queryResult return result @@ -82,8 +84,10 @@ func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string { reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`) result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte { - ind := strings.Replace(strings.Replace(string(in), "{{", "", 1), "}}", "", 1) - if val, exists := metric[pmodel.LabelName(ind)]; exists { + labelName := strings.Replace(string(in), "{{", "", 1) + labelName = strings.Replace(labelName, "}}", "", 1) + labelName = strings.TrimSpace(labelName) + if val, exists := metric[pmodel.LabelName(labelName)]; exists { return []byte(val) } @@ -145,7 +149,7 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb } for _, k := range v.Values { - series.Points = append(series.Points, tsdb.NewTimePoint(float64(k.Value), float64(k.Timestamp.Unix()*1000))) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(k.Value)), float64(k.Timestamp.Unix()*1000))) } queryRes.Series = append(queryRes.Series, &series) @@ -155,7 +159,8 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb return queryResults, nil } +/* func resultWithError(result *tsdb.BatchResult, err error) *tsdb.BatchResult { result.Error = err return result -} +}*/ diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go index f7489ae9afc..a4c38cae582 100644 --- a/pkg/tsdb/prometheus/prometheus_test.go +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -17,7 +17,7 @@ func TestPrometheus(t *testing.T) { } query := &PrometheusQuery{ - LegendFormat: "legend {{app}} {{device}} {{broken}}", + LegendFormat: "legend {{app}} {{ device }} {{broken}}", } So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}") diff --git a/pkg/tsdb/request.go b/pkg/tsdb/request.go index 2e5e5eec25a..2934443bc53 100644 --- a/pkg/tsdb/request.go +++ b/pkg/tsdb/request.go @@ -1,8 +1,10 @@ package tsdb -type HandleRequestFunc func(req *Request) (*Response, error) +import "context" -func HandleRequest(req *Request) (*Response, error) { +type HandleRequestFunc func(ctx context.Context, req *Request) (*Response, error) + +func HandleRequest(ctx context.Context, req *Request) (*Response, error) { context := NewQueryContext(req.Queries, req.TimeRange) batches, err := getBatches(req) @@ -16,7 +18,7 @@ func HandleRequest(req *Request) (*Response, error) { if len(batch.Depends) == 0 { currentlyExecuting += 1 batch.Started = true - go batch.process(context) + go batch.process(ctx, context) } } @@ -46,9 +48,11 @@ func HandleRequest(req *Request) (*Response, error) { if batch.allDependenciesAreIn(context) { currentlyExecuting += 1 batch.Started = true - go batch.process(context) + go batch.process(ctx, context) } } + case <-ctx.Done(): + return nil, ctx.Err() } } diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go index e90b0d4df79..73963fc9844 100644 --- a/pkg/tsdb/testdata/scenarios.go +++ b/pkg/tsdb/testdata/scenarios.go @@ -6,6 +6,8 @@ import ( "strings" "time" + "gopkg.in/guregu/null.v3" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/tsdb" ) @@ -42,7 +44,7 @@ func init() { walker := rand.Float64() * 100 for i := int64(0); i < 10000 && timeWalkerMs < to; i++ { - points = append(points, tsdb.NewTimePoint(walker, float64(timeWalkerMs))) + points = append(points, tsdb.NewTimePoint(null.FloatFrom(walker), float64(timeWalkerMs))) walker += rand.Float64() - 0.5 timeWalkerMs += query.IntervalMs @@ -73,7 +75,7 @@ func init() { series := newSeriesForQuery(query) outsideTime := context.TimeRange.MustGetFrom().Add(-1*time.Hour).Unix() * 1000 - series.Points = append(series.Points, tsdb.NewTimePoint(10, float64(outsideTime))) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(10), float64(outsideTime))) queryRes.Series = append(queryRes.Series, series) return queryRes @@ -88,10 +90,13 @@ func init() { queryRes := tsdb.NewQueryResult() stringInput := query.Model.Get("stringInput").MustString() - values := []float64{} + values := []null.Float{} for _, strVal := range strings.Split(stringInput, ",") { + if strVal == "null" { + values = append(values, null.FloatFromPtr(nil)) + } if val, err := strconv.ParseFloat(strVal, 64); err == nil { - values = append(values, val) + values = append(values, null.FloatFrom(val)) } } @@ -105,7 +110,7 @@ func init() { step := (endTime - startTime) / int64(len(values)-1) for _, val := range values { - series.Points = append(series.Points, tsdb.NewTimePoint(val, float64(startTime))) + series.Points = append(series.Points, tsdb.TimePoint{val, null.FloatFrom(float64(startTime))}) startTime += step } diff --git a/pkg/tsdb/testdata/testdata.go b/pkg/tsdb/testdata/testdata.go index 5b40bb6de5a..cf2dcc0f898 100644 --- a/pkg/tsdb/testdata/testdata.go +++ b/pkg/tsdb/testdata/testdata.go @@ -1,6 +1,8 @@ package testdata import ( + "context" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/tsdb" ) @@ -21,7 +23,7 @@ func init() { tsdb.RegisterExecutor("grafana-testdata-datasource", NewTestDataExecutor) } -func (e *TestDataExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { +func (e *TestDataExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { result := &tsdb.BatchResult{} result.QueryResults = make(map[string]*tsdb.QueryResult) diff --git a/pkg/tsdb/tsdb_test.go b/pkg/tsdb/tsdb_test.go index 429dd01d6ba..998f59a6b9d 100644 --- a/pkg/tsdb/tsdb_test.go +++ b/pkg/tsdb/tsdb_test.go @@ -1,6 +1,7 @@ package tsdb import ( + "context" "testing" "time" @@ -62,7 +63,7 @@ func TestMetricQuery(t *testing.T) { fakeExecutor := registerFakeExecutor() fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}}) - res, err := HandleRequest(req) + res, err := HandleRequest(context.TODO(), req) So(err, ShouldBeNil) Convey("Should return query results", func() { @@ -83,7 +84,7 @@ func TestMetricQuery(t *testing.T) { fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}}) fakeExecutor.Return("B", TimeSeriesSlice{&TimeSeries{Name: "barg"}}) - res, err := HandleRequest(req) + res, err := HandleRequest(context.TODO(), req) So(err, ShouldBeNil) Convey("Should return query results", func() { @@ -106,7 +107,7 @@ func TestMetricQuery(t *testing.T) { }, } - res, err := HandleRequest(req) + res, err := HandleRequest(context.TODO(), req) So(err, ShouldBeNil) Convey("Should have been batched in two requests", func() { @@ -121,7 +122,7 @@ func TestMetricQuery(t *testing.T) { }, } - _, err := HandleRequest(req) + _, err := HandleRequest(context.TODO(), req) So(err, ShouldNotBeNil) }) @@ -152,7 +153,7 @@ func TestMetricQuery(t *testing.T) { }} }) - res, err := HandleRequest(req) + res, err := HandleRequest(context.TODO(), req) So(err, ShouldBeNil) Convey("Should have been batched in two requests", func() { diff --git a/public/app/core/components/query_part/query_part_editor.ts b/public/app/core/components/query_part/query_part_editor.ts index 7bc309d1bd2..ae53eb31001 100644 --- a/public/app/core/components/query_part/query_part_editor.ts +++ b/public/app/core/components/query_part/query_part_editor.ts @@ -128,11 +128,9 @@ export function queryPartEditorDirective($compile, templateSrv) { } $scope.showActionsMenu = function() { - if ($scope.partActions.length === 0) { - $scope.handleEvent({$event: {name: 'get-part-actions'}}).then(res => { - $scope.partActions = res; - }); - } + $scope.handleEvent({$event: {name: 'get-part-actions'}}).then(res => { + $scope.partActions = res; + }); }; $scope.triggerPartAction = function(action) { diff --git a/public/app/core/controllers/login_ctrl.js b/public/app/core/controllers/login_ctrl.js index 4b03dddf973..fa3af3d10f0 100644 --- a/public/app/core/controllers/login_ctrl.js +++ b/public/app/core/controllers/login_ctrl.js @@ -11,6 +11,7 @@ function (angular, _, coreModule, config) { "1000": "Required team membership not fulfilled", "1001": "Required organization membership not fulfilled", "1002": "Required email domain not fulfilled", + "1003": "Login provider denied login request", }; coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) { diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index cc662aa67ca..c3e51dc7a0c 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -43,7 +43,7 @@ function (_, $, coreModule) { var selected = _.find($scope.altSegments, {value: value}); if (selected) { segment.value = selected.value; - segment.html = selected.html; + segment.html = selected.html || selected.value; segment.fake = false; segment.expandable = selected.expandable; } @@ -186,25 +186,26 @@ function (_, $, coreModule) { $scope.getOptionsInternal = function() { if ($scope.options) { - var optionSegments = _.map($scope.options, function(option) { - return uiSegmentSrv.newSegment({value: option.text}); - }); - return $q.when(optionSegments); + cachedOptions = $scope.options; + return $q.when(_.map($scope.options, function(option) { + return {value: option.text}; + })); } else { return $scope.getOptions().then(function(options) { cachedOptions = options; - return _.map(options, function(option) { - return uiSegmentSrv.newSegment({value: option.text}); + return _.map(options, function(option) { + if (option.html) { + return option; + } + return {value: option.text}; }); }); } }; $scope.onSegmentChange = function() { - var options = $scope.options || cachedOptions; - - if (options) { - var option = _.find(options, {text: $scope.segment.value}); + if (cachedOptions) { + var option = _.find(cachedOptions, {text: $scope.segment.value}); if (option && option.value !== $scope.property) { $scope.property = option.value; } else if (attrs.custom !== 'false') { diff --git a/public/app/core/utils/rangeutil.ts b/public/app/core/utils/rangeutil.ts index 7f2f2fd72b1..1e564ee7ea6 100644 --- a/public/app/core/utils/rangeutil.ts +++ b/public/app/core/utils/rangeutil.ts @@ -82,8 +82,9 @@ function formatDate(date) { // now/d // if no to then to now is assumed export function describeTextRange(expr: any) { + let isLast = (expr.indexOf('+') !== 0); if (expr.indexOf('now') === -1) { - expr = 'now-' + expr; + expr = (isLast ? 'now-' : 'now') + expr; } let opt = rangeIndex[expr + ' to now']; @@ -91,15 +92,20 @@ export function describeTextRange(expr: any) { return opt; } - opt = {from: expr, to: 'now'}; + if (isLast) { + opt = {from: expr, to: 'now'}; + } else { + opt = {from: 'now', to: expr}; + } - let parts = /^now-(\d+)(\w)/.exec(expr); + let parts = /^now([-+])(\d+)(\w)/.exec(expr); if (parts) { - let unit = parts[2]; - let amount = parseInt(parts[1]); + let unit = parts[3]; + let amount = parseInt(parts[2]); let span = spans[unit]; if (span) { - opt.display = 'Last ' + amount + ' ' + span.display; + opt.display = isLast ? 'Last ' : 'Next '; + opt.display += amount + ' ' + span.display; opt.section = span.section; if (amount > 1) { opt.display += 's'; diff --git a/public/app/features/alerting/alert_def.ts b/public/app/features/alerting/alert_def.ts index 8e9a86735ad..9c567d2ebc5 100644 --- a/public/app/features/alerting/alert_def.ts +++ b/public/app/features/alerting/alert_def.ts @@ -40,6 +40,7 @@ var noDataModes = [ {text: 'OK', value: 'ok'}, {text: 'Alerting', value: 'alerting'}, {text: 'No Data', value: 'no_data'}, + {text: 'Keep Last', value: 'keep_last'}, ]; function createReducerPart(model) { diff --git a/public/app/features/alerting/alert_list_ctrl.ts b/public/app/features/alerting/alert_list_ctrl.ts index 4b429e961a7..b2287759a9b 100644 --- a/public/app/features/alerting/alert_list_ctrl.ts +++ b/public/app/features/alerting/alert_list_ctrl.ts @@ -23,7 +23,7 @@ export class AlertListCtrl { }; /** @ngInject */ - constructor(private backendSrv, private $location) { + constructor(private backendSrv, private $location, private $scope) { var params = $location.search(); this.filters.state = params.state || null; this.loadAlerts(); @@ -43,6 +43,19 @@ export class AlertListCtrl { }); } + pauseAlertRule(alertId: any) { + var alert = _.find(this.alerts, {id: alertId}); + + var payload = { + paused: alert.state !== "paused" + }; + + this.backendSrv.post(`/api/alerts/${alert.id}/pause`, payload).then(result => { + alert.state = result.state; + alert.stateModel = alertDef.getStateDisplayModel(result.state); + }); + } + openHowTo() { appEvents.emit('show-modal', { src: 'public/app/features/alerting/partials/alert_howto.html', diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 0ea5d5fd804..61c4d658ed1 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -59,7 +59,7 @@ export class AlertTabCtrl { this.panelCtrl.render(); }); - // build notification model + // build notification model this.notifications = []; this.alertNotifications = []; this.alertHistory = []; @@ -156,7 +156,7 @@ export class AlertTabCtrl { for (let addedNotification of alert.notifications) { var model = _.find(this.notifications, {id: addedNotification.id}); - if (model) { + if (model && model.isDefault === false) { model.iconClass = this.getNotificationIcon(model.type); this.alertNotifications.push(model); } @@ -231,7 +231,7 @@ export class AlertTabCtrl { this.datasourceSrv.get(datasourceName).then(ds => { if (!ds.meta.alerting) { this.error = 'The datasource does not support alerting queries'; - } else if (this.templateSrv.variableExists(foundTarget.target)) { + } else if (ds.targetContainsTemplate(foundTarget)) { this.error = 'Template variables are not supported in alert queries'; } else { this.error = ''; @@ -315,6 +315,7 @@ export class AlertTabCtrl { this.alert = null; this.panel.thresholds = []; this.conditionModels = []; + this.panelCtrl.alertState = null; this.panelCtrl.render(); } }); @@ -351,6 +352,24 @@ export class AlertTabCtrl { this.evaluatorParamsChanged(); } + clearHistory() { + appEvents.emit('confirm-modal', { + title: 'Delete Alert History', + text: 'Are you sure you want to remove all history & annotations for this alert?', + icon: 'fa-trash', + yesText: 'Yes', + onConfirm: () => { + this.backendSrv.post('/api/annotations/mass-delete', { + dashboardId: this.panelCtrl.dashboard.id, + panelId: this.panel.id, + }).then(res => { + this.alertHistory = []; + this.panelCtrl.refresh(); + }); + } + }); + } + test() { this.testing = true; diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index de5703a0631..c5f24650845 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -7,7 +7,7 @@ import config from 'app/core/config'; export class AlertNotificationEditCtrl { model: any; - showTest: boolean = false; + theForm: any; testSeverity: string = "critical"; /** @ngInject */ @@ -18,7 +18,7 @@ export class AlertNotificationEditCtrl { this.model = { type: 'email', settings: { - severityFilter: 'none' + httpMethod: 'POST' }, isDefault: false }; @@ -36,6 +36,10 @@ export class AlertNotificationEditCtrl { } save() { + if (!this.theForm.$valid) { + return; + } + if (this.model.id) { this.backendSrv.put(`/api/alert-notifications/${this.model.id}`, this.model).then(res => { this.model = res; @@ -53,11 +57,11 @@ export class AlertNotificationEditCtrl { this.model.settings = {}; } - toggleTest() { - this.showTest = !this.showTest; - } - testNotification() { + if (!this.theForm.$valid) { + return; + } + var payload = { name: this.model.name, type: this.model.type, diff --git a/public/app/features/alerting/partials/alert_list.html b/public/app/features/alerting/partials/alert_list.html index a8678bc4cdc..685aa1f2db8 100644 --- a/public/app/features/alerting/partials/alert_list.html +++ b/public/app/features/alerting/partials/alert_list.html @@ -29,7 +29,10 @@
diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index bb6fe7547b2..4dd5516945b 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -52,9 +52,9 @@
- + - +
-
State history (last 50 state changes)
+ +
+ State history (last 50 state changes) +
+ +
+
+ No state changes recorded +
+
  1. diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 65964b2c7c4..c0bbf49f77b 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -6,81 +6,86 @@
    -
    -
    +
    -
    +
    @@ -47,7 +47,7 @@
    -
    +
    diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index ba9b4ad1d2b..8d7f360aeeb 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -27,7 +27,7 @@ export class PluginEditCtrl { this.model = {}; this.pluginId = $routeParams.pluginId; this.tabIndex = 0; - this.tabs = ['Overview']; + this.tabs = ['Readme']; this.preUpdateHook = () => Promise.resolve(); this.postUpdateHook = () => Promise.resolve(); @@ -48,13 +48,13 @@ export class PluginEditCtrl { }); if (this.model.type === 'app') { - this.tabIndex = 1; - this.tabs.push('Config'); - this.hasDashboards = _.find(result.includes, {type: 'dashboard'}); if (this.hasDashboards) { - this.tabs.push('Dashboards'); + this.tabs.unshift('Dashboards'); } + + this.tabs.unshift('Config'); + this.tabIndex = 0; } return this.initReadme(); diff --git a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json index 757dd48e50f..c64ab84f338 100644 --- a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json +++ b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json @@ -1,5 +1,5 @@ { - "revision": 4, + "revision": 5, "title": "TestData - Graph Panel Last 1h", "tags": [ "grafana-test" @@ -320,124 +320,376 @@ ] }, { - "title": "", - "error": false, - "span": 4, - "editable": true, - "type": "text", - "isNew": true, - "id": 6, - "mode": "markdown", "content": "Just verify that the tooltip time has millisecond resolution ", - "links": [] + "editable": true, + "error": false, + "id": 6, + "isNew": true, + "links": [], + "mode": "markdown", + "span": 4, + "title": "", + "type": "text" } ], "title": "New row" }, { - "title": "New row", - "height": 336, - "editable": true, "collapse": false, + "editable": true, + "height": 336, "panels": [ { - "title": "2 yaxis and axis lables", - "error": false, - "span": 7.99561403508772, - "editable": true, - "type": "graph", - "isNew": true, - "id": 5, - "targets": [ - { - "target": "", - "refId": "A", - "scenarioId": "csv_metric_values", - "stringInput": "1,20,90,30,5,0" - }, - { - "target": "", - "refId": "B", - "scenarioId": "csv_metric_values", - "stringInput": "2000,3000,4000,1000,3000,10000" - } - ], + "aliasColors": {}, + "bars": false, "datasource": "Grafana TestData", - "renderer": "flot", - "yaxes": [ - { - "label": "Perecent", - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "percent" - }, - { - "label": "Pressure", - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - } - ], - "xaxis": { + "editable": true, + "error": false, + "fill": 1, + "id": 5, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, "show": true, - "mode": "time", - "name": null, - "values": [] + "total": false, + "values": false }, "lines": true, - "fill": 1, "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, + "links": [], "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true, - "sort": 0, - "msResolution": false - }, - "timeFrom": null, - "timeShift": null, - "aliasColors": {}, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", "seriesOverrides": [ { "alias": "B-series", "yaxis": 2 } ], + "span": 7.99561403508772, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "2000,3000,4000,1000,3000,10000", + "target": "" + } + ], "thresholds": [], - "links": [] + "timeFrom": null, + "timeShift": null, + "title": "2 yaxis and axis lables", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": "Perecent", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": "Pressure", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] }, { - "title": "", - "error": false, - "span": 4.00438596491228, - "editable": true, - "type": "text", - "isNew": true, - "id": 7, - "mode": "markdown", "content": "Verify that axis labels look ok", - "links": [] + "editable": true, + "error": false, + "id": 7, + "isNew": true, + "links": [], + "mode": "markdown", + "span": 4.00438596491228, + "title": "", + "type": "text" } - ] + ], + "title": "New row" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": "Grafana TestData", + "editable": true, + "error": false, + "fill": 1, + "id": 8, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "null value connected", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "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, + "datasource": "Grafana TestData", + "editable": true, + "error": false, + "fill": 1, + "id": 10, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 3, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "null value null as zero", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "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, + "datasource": "Grafana TestData", + "editable": true, + "error": false, + "fill": 1, + "id": 9, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "zindex": -3 + } + ], + "span": 5, + "stack": true, + "steppedLine": false, + "targets": [ + { + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Stacking value ontop of nulls", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "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": "New row" } ], "time": { @@ -477,7 +729,7 @@ }, "refresh": false, "schemaVersion": 13, - "version": 3, + "version": 13, "links": [], "gnetId": null } diff --git a/public/app/plugins/app/testdata/plugin.json b/public/app/plugins/app/testdata/plugin.json index 6742ad04ecb..63f88df8140 100644 --- a/public/app/plugins/app/testdata/plugin.json +++ b/public/app/plugins/app/testdata/plugin.json @@ -9,7 +9,7 @@ "name": "Grafana Project", "url": "http://grafana.org" }, - "version": "1.0.13", + "version": "1.0.14", "updated": "2016-09-26" }, diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 4365c2e2596..fde9e8f09ea 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -3,9 +3,10 @@ define([ 'lodash', 'moment', 'app/core/utils/datemath', + 'app/core/utils/kbn', './annotation_query', ], -function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { +function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) { 'use strict'; /** @ngInject */ @@ -36,12 +37,9 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars); query.statistics = target.statistics; - var range = end - start; - query.period = parseInt(target.period, 10) || (query.namespace === 'AWS/EC2' ? 300 : 60); - if (range / query.period >= 1440) { - query.period = Math.ceil(range / 1440 / 60) * 60; - } - target.period = query.period; + var period = this._getPeriod(target, query, options, start, end); + target.period = period; + query.period = period; queries.push(query); }.bind(this)); @@ -69,6 +67,27 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { }); }; + this._getPeriod = function(target, query, options, start, end) { + var period; + var range = end - start; + + if (!target.period) { + period = (query.namespace === 'AWS/EC2') ? 300 : 60; + } else if (/^\d+$/.test(target.period)) { + period = parseInt(target.period, 10); + } else { + period = kbn.interval_to_seconds(templateSrv.replace(target.period, options.scopedVars)); + } + if (query.period < 60) { + period = 60; + } + if (range / query.period >= 1440) { + period = Math.ceil(range / 1440 / 60) * 60; + } + + return period; + }; + this.performTimeSeriesQuery = function(query, start, end) { return this.awsRequest({ region: query.region, @@ -339,9 +358,15 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { } this.getExpandedVariables = function(target, dimensionKey, variable) { + /* if the all checkbox is marked we should add all values to the targets */ + var allSelected = _.find(variable.options, {'selected': true, 'text': 'All'}); return _.chain(variable.options) .filter(function(v) { - return v.selected; + if (allSelected) { + return v.text !== 'All'; + } else { + return v.selected; + } }) .map(function(v) { var t = angular.copy(target); @@ -350,6 +375,10 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { }).value(); }; + this.containsVariable = function (str, variableName) { + return str.indexOf('$' + variableName) !== -1; + }; + this.expandTemplateVariable = function(targets, templateSrv) { var self = this; return _.chain(targets) @@ -360,7 +389,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { if (dimensionKey) { var variable = _.find(templateSrv.variables, function(variable) { - return templateSrv.containsVariable(target.dimensions[dimensionKey], variable.name); + return self.containsVariable(target.dimensions[dimensionKey], variable.name); }); return self.getExpandedVariables(target, dimensionKey, variable); } else { diff --git a/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png b/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png index f11f4034751..7f9687e839c 100644 Binary files a/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png and b/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png differ diff --git a/public/app/plugins/datasource/cloudwatch/partials/config.html b/public/app/plugins/datasource/cloudwatch/partials/config.html index fb34a9fb92b..6f119964512 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/config.html +++ b/public/app/plugins/datasource/cloudwatch/partials/config.html @@ -11,7 +11,7 @@
    - + Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region. diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts index 0b9b3b53fb6..0e7ae5081e6 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts @@ -82,6 +82,35 @@ describe('CloudWatchDatasource', function() { ctx.$rootScope.$apply(); }); + it('should generate the correct query with interval variable', function(done) { + ctx.templateSrv.data = { + period: '10m' + }; + + var query = { + range: { from: 'now-1h', to: 'now' }, + targets: [ + { + region: 'us-east-1', + namespace: 'AWS/EC2', + metricName: 'CPUUtilization', + dimensions: { + InstanceId: 'i-12345678' + }, + statistics: ['Average'], + period: '[[period]]' + } + ] + }; + + ctx.ds.query(query).then(function() { + var params = requestParams.data.parameters; + expect(params.period).to.be(600); + done(); + }); + ctx.$rootScope.$apply(); + }); + it('should return series list', function(done) { ctx.ds.query(query).then(function(result) { expect(result.data[0].target).to.be('CPUUtilization_Average'); diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.js b/public/app/plugins/datasource/elasticsearch/elastic_response.js index a099aa1a012..4e82a280024 100644 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.js +++ b/public/app/plugins/datasource/elasticsearch/elastic_response.js @@ -171,6 +171,9 @@ function (_, queryDef) { } else { props["filter"] = nameIndex; } + if (bucket.key_as_string) { + props[aggDef.field] = bucket.key_as_string; + } this.processBuckets(bucket, target, seriesList, docs, props, depth+1); } } diff --git a/public/app/plugins/datasource/elasticsearch/img/logo_large.png b/public/app/plugins/datasource/elasticsearch/img/logo_large.png index b76d01e5d74..5ded1d8f438 100644 Binary files a/public/app/plugins/datasource/elasticsearch/img/logo_large.png and b/public/app/plugins/datasource/elasticsearch/img/logo_large.png differ diff --git a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html index 36e914d06e0..3790d18857e 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -23,7 +23,7 @@ -
    diff --git a/public/app/plugins/datasource/elasticsearch/query_def.js b/public/app/plugins/datasource/elasticsearch/query_def.js index 9a45b138b21..187e5c2f4fa 100644 --- a/public/app/plugins/datasource/elasticsearch/query_def.js +++ b/public/app/plugins/datasource/elasticsearch/query_def.js @@ -22,7 +22,7 @@ function (_) { bucketAggTypes: [ {text: "Terms", value: 'terms', requiresField: true}, {text: "Filters", value: 'filters' }, - {text: "Geo Hash Grid", value: 'geohash_grid', requiresField: true}, + {text: "Geo Hash Grid", value: 'geohash_grid', requiresField: true}, {text: "Date Histogram", value: 'date_histogram', requiresField: true}, ], diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index ac3c6581522..5d0b3de0630 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -126,6 +126,10 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv } }; + this.targetContainsTemplate = function(target) { + return templateSrv.variableExists(target.target); + }; + this.translateTime = function(date, roundUp) { if (_.isString(date)) { if (date === 'now') { diff --git a/public/app/plugins/datasource/graphite/img/graphite_logo.png b/public/app/plugins/datasource/graphite/img/graphite_logo.png index e70b40720c4..7b6b85d2eb5 100644 Binary files a/public/app/plugins/datasource/graphite/img/graphite_logo.png and b/public/app/plugins/datasource/graphite/img/graphite_logo.png differ diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index 93977dc143d..30f17554d2b 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -209,17 +209,40 @@ export class GraphiteQueryCtrl extends QueryCtrl { this.panelCtrl.refresh(); } + updateModelTarget() { + // render query + var metricPath = this.getSegmentPathUpTo(this.segments.length); + this.target.target = _.reduce(this.functions, this.wrapFunction, metricPath); + + // render nested query + var targetsByRefId = _.keyBy(this.panelCtrl.panel.targets, 'refId'); + var nestedSeriesRefRegex = /\#([A-Z])/g; + var targetWithNestedQueries = this.target.target.replace(nestedSeriesRefRegex, (match, g1) => { + var target = targetsByRefId[g1]; + if (!target) { + return match; + } + + return target.targetFull || target.target; + }); + + delete this.target.targetFull; + if (this.target.target !== targetWithNestedQueries) { + this.target.targetFull = targetWithNestedQueries; + } + } + targetChanged() { if (this.error) { return; } var oldTarget = this.target.target; - var target = this.getSegmentPathUpTo(this.segments.length); - this.target.target = _.reduce(this.functions, this.wrapFunction, target); + this.updateModelTarget(); if (this.target.target !== oldTarget) { - if (this.segments[this.segments.length - 1].value !== 'select metric') { + var lastSegment = this.segments.length > 0 ? this.segments[this.segments.length - 1] : {}; + if (lastSegment.value !== 'select metric') { this.panelCtrl.refresh(); } } diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 76a66d18e3c..8e7175b0cdf 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -139,6 +139,24 @@ export default class InfluxDatasource { }); }; + targetContainsTemplate(target) { + for (let group of target.groupBy) { + for (let param of group.params) { + if (this.templateSrv.variableExists(param)) { + return true; + } + } + } + + for (let i in target.tags) { + if (this.templateSrv.variableExists(target.tags[i].value)) { + return true; + } + } + + return false; + }; + metricFindQuery(query) { var interpolated = this.templateSrv.replace(query, null, 'regex'); diff --git a/public/app/plugins/datasource/influxdb/plugin.json b/public/app/plugins/datasource/influxdb/plugin.json index 605ce168831..635309d1610 100644 --- a/public/app/plugins/datasource/influxdb/plugin.json +++ b/public/app/plugins/datasource/influxdb/plugin.json @@ -6,6 +6,7 @@ "defaultMatchFormat": "regex values", "metrics": true, "annotations": true, + "alerting": true, "info": { "author": { diff --git a/public/app/plugins/datasource/opentsdb/datasource.js b/public/app/plugins/datasource/opentsdb/datasource.js index 54605a4f7e2..4ef7c9761fe 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.js +++ b/public/app/plugins/datasource/opentsdb/datasource.js @@ -102,6 +102,26 @@ function (angular, _, dateMath) { }.bind(this)); }; + this.targetContainsTemplate = function(target) { + if (target.filters && target.filters.length > 0) { + for (var i = 0; i < target.filters.length; i++) { + if (templateSrv.variableExists(target.filters[i].filter)) { + return true; + } + } + } + + if (target.tags && Object.keys(target.tags).length > 0) { + for (var tagKey in target.tags) { + if (templateSrv.variableExists(target.tags[tagKey])) { + return true; + } + } + } + + return false; + }; + this.performTimeSeriesQuery = function(queries, start, end) { var msResolution = false; if (this.tsdbResolution === 2) { @@ -332,7 +352,7 @@ function (angular, _, dateMath) { var tagData = []; if (!_.isEmpty(md.tags)) { - _.each(_.pairs(md.tags), function(tag) { + _.each(_.toPairs(md.tags), function(tag) { if (_.has(groupByTags, tag[0])) { tagData.push(tag[0] + "=" + tag[1]); } @@ -405,6 +425,10 @@ function (angular, _, dateMath) { } } + if (target.explicitTags) { + query.explicitTags = true; + } + return query; } @@ -419,7 +443,7 @@ function (angular, _, dateMath) { return target.metric === metricData.metric; } else { return target.metric === metricData.metric && - _.all(target.tags, function(tagV, tagK) { + _.every(target.tags, function(tagV, tagK) { interpolatedTagValue = templateSrv.replace(tagV, options.scopedVars, 'pipe'); return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*"; }); diff --git a/public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png b/public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png index 519d56a476c..081459abe0c 100644 Binary files a/public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png and b/public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png differ diff --git a/public/app/plugins/datasource/opentsdb/partials/query.editor.html b/public/app/plugins/datasource/opentsdb/partials/query.editor.html index 255e23c5701..a2db8a68840 100644 --- a/public/app/plugins/datasource/opentsdb/partials/query.editor.html +++ b/public/app/plugins/datasource/opentsdb/partials/query.editor.html @@ -249,6 +249,11 @@
    +
    + + +
    +
    diff --git a/public/app/plugins/datasource/opentsdb/plugin.json b/public/app/plugins/datasource/opentsdb/plugin.json index 02ba02e6e89..7de072c6948 100644 --- a/public/app/plugins/datasource/opentsdb/plugin.json +++ b/public/app/plugins/datasource/opentsdb/plugin.json @@ -6,6 +6,7 @@ "metrics": true, "defaultMatchFormat": "pipe", "annotations": true, + "alerting": true, "info": { "author": { diff --git a/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json b/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json index 0c2e0e85728..3b5b73ea6d4 100644 --- a/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json +++ b/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json @@ -478,7 +478,7 @@ "steppedLine": false, "targets": [ { - "expr": "prometheus_evaluator_duration_milliseconds{quantile!=\"0.01\", quantile!=\"0.05\"}", + "expr": "prometheus_evaluator_duration_seconds{quantile!=\"0.01\", quantile!=\"0.05\"}", "interval": "", "intervalFactor": 2, "legendFormat": "{{quantile}}", diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 941591791ef..972fa2c7c55 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -58,6 +58,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS return escapedValues.join('|'); }; + this.targetContainsTemplate = function(target) { + return templateSrv.variableExists(target.expr); + }; + // Called once per panel (graph) this.query = function(options) { var self = this; diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index 6dbbc80c4cd..38083a80a56 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -39,6 +39,7 @@ export class AxesEditorCtrl { {text: 'Max', value: 'min'}, {text: 'Total', value: 'total'}, {text: 'Count', value: 'count'}, + {text: 'Current', value: 'current'}, ]; if (this.panel.xaxis.mode === 'custom') { diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index a07701cef65..6a3e49b8455 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -384,10 +384,36 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { if (!annotations || annotations.length === 0) { return; } + console.log(annotations); var types = {}; + types['$__alerting'] = { + color: 'rgba(237, 46, 24, 1)', + position: 'BOTTOM', + markerSize: 5, + }; + + types['$__ok'] = { + color: 'rgba(11, 237, 50, 1)', + position: 'BOTTOM', + markerSize: 5, + }; + + types['$__no_data'] = { + color: 'rgba(150, 150, 150, 1)', + position: 'BOTTOM', + markerSize: 5, + }; + + types['$__execution_error'] = ['$__no_data']; + for (var i = 0; i < annotations.length; i++) { var item = annotations[i]; + if (item.newState) { + console.log(item.newState); + item.eventType = '$__' + item.newState; + continue; + } if (!types[item.source.name]) { types[item.source.name] = { diff --git a/public/app/plugins/panel/graph/graph_tooltip.js b/public/app/plugins/panel/graph/graph_tooltip.js index cd3bddf41ef..5ae03ccf813 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.js +++ b/public/app/plugins/panel/graph/graph_tooltip.js @@ -41,7 +41,7 @@ function ($, _) { }; this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) { - var value, i, series, hoverIndex, hoverDistance, pointTime; + var value, i, series, hoverIndex, hoverDistance, pointTime, yaxis; var results = []; //now we know the current X (j) position for X and Y values @@ -51,12 +51,12 @@ function ($, _) { series = seriesList[i]; if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) { - results.push({ hidden: true }); + results.push({ hidden: true, value: 0, yaxis: 0 }); continue; } if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) { - results.push({ hidden: true }); + results.push({ hidden: true, value: 0, yaxis: 0 }); continue; } @@ -85,13 +85,20 @@ function ($, _) { hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex); } + yaxis = 0; + if (series.yaxis) { + yaxis = series.yaxis.n; + } + results.push({ value: value, hoverIndex: hoverIndex, color: series.color, label: series.label, time: pointTime, - distance: hoverDistance + distance: hoverDistance, + yaxis: yaxis, + index: i }); } @@ -142,10 +149,8 @@ function ($, _) { seriesHtml = ''; - absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat); - // Dynamically reorder the hovercard for the current time point if the - // option is enabled. + // option is enabled, sort by yaxis by default. if (panel.tooltip.sort === 2) { seriesHoverInfo.sort(function(a, b) { return b.value - a.value; @@ -154,8 +159,14 @@ function ($, _) { seriesHoverInfo.sort(function(a, b) { return a.value - b.value; }); + } else { + seriesHoverInfo.sort(function(a, b) { + return a.yaxis - b.yaxis; + }); } + var distance, time; + for (i = 0; i < seriesHoverInfo.length; i++) { hoverInfo = seriesHoverInfo[i]; @@ -163,21 +174,27 @@ function ($, _) { continue; } + if (! distance || hoverInfo.distance < distance) { + distance = hoverInfo.distance; + time = hoverInfo.time; + } + var highlightClass = ''; if (item && i === item.seriesIndex) { highlightClass = 'graph-tooltip-list-item--highlight'; } - series = seriesList[i]; + series = seriesList[hoverInfo.index]; value = series.formatValue(hoverInfo.value); seriesHtml += '
    '; seriesHtml += ' ' + hoverInfo.label + ':
    '; seriesHtml += '
    ' + value + '
    '; - plot.highlight(i, hoverInfo.hoverIndex); + plot.highlight(hoverInfo.index, hoverInfo.hoverIndex); } + absoluteTime = dashboard.formatDate(time, tooltipFormat); self.showTooltip(absoluteTime, seriesHtml, pos); } // single series tooltip diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 8d392c0d5c1..c4b1bd89044 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -93,7 +93,7 @@ class GraphCtrl extends MetricsPanelCtrl { steppedLine: false, // tooltip options tooltip : { - value_type: 'cumulative', + value_type: 'individual', shared: true, sort: 0, msResolution: false, @@ -133,10 +133,8 @@ class GraphCtrl extends MetricsPanelCtrl { this.addEditorTab('Axes', axesEditorComponent, 2); this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); + this.addEditorTab('Alert', alertTab, 5); - if (config.alertingEnabled) { - this.addEditorTab('Alert', alertTab, 5); - } this.subTabIndex = 0; } diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html index 14c8c4cf6fc..6845181b8b2 100644 --- a/public/app/plugins/panel/graph/tab_display.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -71,13 +71,9 @@
    Stacking & Null value
    - + - +
    diff --git a/public/app/plugins/panel/text/module.ts b/public/app/plugins/panel/text/module.ts index 7228f220d46..c817c184380 100644 --- a/public/app/plugins/panel/text/module.ts +++ b/public/app/plugins/panel/text/module.ts @@ -13,14 +13,15 @@ export class TextPanelCtrl extends PanelCtrl { mode : "markdown", // 'html', 'markdown', 'text' content : "# title", }; - /** @ngInject */ + + /** @ngInject **/ constructor($scope, $injector, private templateSrv, private $sce) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); - this.events.on('refresh', this.onRender.bind(this)); + this.events.on('refresh', this.onRefresh.bind(this)); this.events.on('render', this.onRender.bind(this)); } @@ -29,6 +30,10 @@ export class TextPanelCtrl extends PanelCtrl { this.editorTabIndex = 1; } + onRefresh() { + this.render(); + } + onRender() { if (this.panel.mode === 'markdown') { this.renderMarkdown(this.panel.content); diff --git a/public/emails/alert_notification.html b/public/emails/alert_notification.html index e91ab6450fe..15668c1924c 100644 --- a/public/emails/alert_notification.html +++ b/public/emails/alert_notification.html @@ -5,7 +5,7 @@ - - - -
    -
    + + +
    +
    - - -
    -
    + + +
    +
    - - -
    + + +
    - - -
    - + + + - +
    +
    @@ -200,21 +200,21 @@ text-decoration: underline;
    - - - +
    + + - - +
    +
    {{Subject .Subject "{{.Title}}"}} - - -
    - - -
    -

    {{.Title}}

    + + +
    + + +
    +

    {{.Title}}

    @@ -222,13 +222,13 @@ text-decoration: underline;
    - - -
    - - -
    -

    {{.Message}}

    + + +
    + + +
    +

    {{.Message}}

    @@ -237,26 +237,26 @@ text-decoration: underline;
    {{if ne .State "ok" }} - - -
    -
    - - -
    -
    Metric name
    + + +
    +
    + + + - {{range .EvalMatches}} - - + - {{end}} @@ -267,13 +267,18 @@ text-decoration: underline;
    +
    Metric name
    -
    Value
    +
    +
    Value
    -
    {{.Metric}}
    +
    +
    {{.Metric}}
    -
    {{.Value}}
    +
    +
    {{.Value}}
    {{end}} - - -
    - - -
    - + + +
    + + +
    + {{if ne .ImageLink "" }} + Alerting Panel + {{end}} + {{if ne .EmbededImage "" }} + Alerting Panel + {{end}}
    @@ -282,25 +287,25 @@ text-decoration: underline;
    - - -
    - - -
    - - -
    - View your Alert rule + + +
    + + + -
    + + +
    + View your Alert rule
    - - -
    - Go to the Alerts page + + + +
    + Go to the Alerts page
    @@ -318,20 +323,20 @@ text-decoration: underline;
    - - -