Browse Source

Merge branch 'master' into websocket

Torkel Ödegaard 9 năm trước cách đây
mục cha
commit
8a95c563bb
100 tập tin đã thay đổi với 1767 bổ sung338 xóa
  1. 51 1
      CHANGELOG.md
  2. 1 1
      Makefile
  3. 5 3
      README.md
  4. 3 2
      appveyor.yml
  5. 6 5
      bower.json
  6. 30 32
      build.go
  7. 32 7
      circle.yml
  8. 10 1
      conf/defaults.ini
  9. 1 0
      conf/ldap.toml
  10. 12 2
      conf/sample.ini
  11. 30 0
      docs/sources/administration/cli.md
  12. 0 0
      docs/sources/features/datasources/cloudwatch.md
  13. 0 0
      docs/sources/features/datasources/elasticsearch.md
  14. 0 0
      docs/sources/features/datasources/graphite.md
  15. 0 0
      docs/sources/features/datasources/index.md
  16. 0 0
      docs/sources/features/datasources/influxdb.md
  17. 0 0
      docs/sources/features/datasources/kairosdb.md
  18. 0 0
      docs/sources/features/datasources/opentsdb.md
  19. 0 0
      docs/sources/features/datasources/plugin_api.md
  20. 5 0
      docs/sources/features/datasources/prometheus.md
  21. 54 0
      docs/sources/features/datasources/testdata.md
  22. 0 0
      docs/sources/features/panels/dashlist.md
  23. 0 0
      docs/sources/features/panels/graph.md
  24. 2 1
      docs/sources/features/panels/singlestat.md
  25. 0 0
      docs/sources/features/panels/table_panel.md
  26. 1 1
      docs/sources/guides/whats-new-in-v3-1.md
  27. 1 1
      docs/sources/guides/whats-new-in-v3.md
  28. 70 0
      docs/sources/guides/whats-new-in-v4-1.md
  29. 2 2
      docs/sources/guides/whats-new-in-v4.md
  30. 18 0
      docs/sources/http_api/admin.md
  31. 216 0
      docs/sources/http_api/alerting.md
  32. 1 1
      docs/sources/http_api/dashboard.md
  33. 2 2
      docs/sources/http_api/data_source.md
  34. 34 0
      docs/sources/http_api/user.md
  35. 13 1
      docs/sources/installation/configuration.md
  36. 3 3
      docs/sources/installation/debian.md
  37. 1 0
      docs/sources/installation/ldap.md
  38. 4 4
      docs/sources/installation/rpm.md
  39. 1 1
      docs/sources/installation/windows.md
  40. 3 3
      docs/sources/plugins/datasources.md
  41. 4 1
      docs/sources/project/building_from_source.md
  42. 2 2
      latest.json
  43. 5 8
      package.json
  44. 6 0
      packaging/deb/control/postinst
  45. 1 1
      packaging/deb/default/grafana-server
  46. 2 2
      packaging/publish/publish_both.sh
  47. 2 2
      packaging/publish/publish_testing.sh
  48. 7 0
      packaging/rpm/control/postinst
  49. 1 1
      packaging/rpm/sysconfig/grafana-server
  50. 6 2
      pkg/api/alerting.go
  51. 3 0
      pkg/api/api.go
  52. 44 17
      pkg/api/cloudwatch/cloudwatch.go
  53. 9 5
      pkg/api/cloudwatch/metrics.go
  54. 33 0
      pkg/api/dataproxy.go
  55. 2 2
      pkg/api/datasources.go
  56. 4 1
      pkg/api/dtos/alerting.go
  57. 1 1
      pkg/api/dtos/user.go
  58. 1 0
      pkg/api/frontendsettings.go
  59. 2 2
      pkg/api/index.go
  60. 6 5
      pkg/api/render.go
  61. 30 6
      pkg/api/user.go
  62. 1 1
      pkg/cmd/grafana-server/server.go
  63. 13 10
      pkg/components/imguploader/imguploader.go
  64. 3 2
      pkg/components/imguploader/imguploader_test.go
  65. 38 32
      pkg/components/imguploader/s3uploader.go
  66. 10 0
      pkg/components/null/float.go
  67. 35 5
      pkg/components/renderer/renderer.go
  68. 6 0
      pkg/metrics/metrics.go
  69. 0 1
      pkg/models/alert.go
  70. 1 0
      pkg/models/notifications.go
  71. 1 1
      pkg/services/alerting/conditions/evaluator.go
  72. 2 2
      pkg/services/alerting/conditions/evaluator_test.go
  73. 21 3
      pkg/services/alerting/conditions/query.go
  74. 32 2
      pkg/services/alerting/conditions/query_test.go
  75. 1 1
      pkg/services/alerting/conditions/reducer.go
  76. 12 2
      pkg/services/alerting/conditions/reducer_test.go
  77. 35 0
      pkg/services/alerting/eval_handler.go
  78. 73 3
      pkg/services/alerting/eval_handler_test.go
  79. 15 2
      pkg/services/alerting/extractor.go
  80. 28 0
      pkg/services/alerting/extractor_test.go
  81. 3 1
      pkg/services/alerting/models.go
  82. 23 5
      pkg/services/alerting/notifier.go
  83. 15 1
      pkg/services/alerting/notifiers/email.go
  84. 94 0
      pkg/services/alerting/notifiers/line.go
  85. 49 0
      pkg/services/alerting/notifiers/line_test.go
  86. 22 1
      pkg/services/alerting/notifiers/opsgenie.go
  87. 22 1
      pkg/services/alerting/notifiers/pagerduty.go
  88. 115 0
      pkg/services/alerting/notifiers/sensu.go
  89. 52 0
      pkg/services/alerting/notifiers/sensu_test.go
  90. 36 1
      pkg/services/alerting/notifiers/slack.go
  91. 113 0
      pkg/services/alerting/notifiers/telegram.go
  92. 55 0
      pkg/services/alerting/notifiers/telegram_test.go
  93. 13 1
      pkg/services/alerting/notifiers/victorops.go
  94. 30 2
      pkg/services/alerting/notifiers/webhook.go
  95. 5 34
      pkg/services/alerting/result_handler.go
  96. 0 90
      pkg/services/alerting/result_handler_test.go
  97. 3 2
      pkg/services/alerting/test_notification.go
  98. 1 0
      pkg/services/notifications/mailer.go
  99. 1 0
      pkg/services/notifications/notifications.go
  100. 5 0
      pkg/services/notifications/webhook.go

+ 51 - 1
CHANGELOG.md

@@ -1,4 +1,53 @@
-# 4.1-beta (unreleased)
+# 4.2.0 (unreleased)
+
+## Enhancements
+* **Alerting**: Added Telegram alert notifier [#7098](https://github.com/grafana/grafana/pull/7098), thx [@leonoff](https://github.com/leonoff)
+* **Templating**: Make $__interval and $__interval_ms global built in variables that can be used in by any datasource (in panel queries), closes [#7190](https://github.com/grafana/grafana/issues/7190), closes [#6582](https://github.com/grafana/grafana/issues/6582)
+* **S3 Image Store**: External s3 image store (used in alert notifications) now support AWS IAM Roles, closes [#6985](https://github.com/grafana/grafana/issues/6985), [#7058](https://github.com/grafana/grafana/issues/7058) thx [@mtanda](https://github.com/mtanda)
+* **Optimzation**: Never issue refresh event when Grafana tab is not visible [#7218](https://github.com/grafana/grafana/issues/7218), thx [@mtanda](https://github.com/mtanda)
+* **Browser History**: Browser back/forward now works time ranges / zoom, [#7259](https://github.com/grafana/grafana/issues/7259)
+* **SingleStat**: Implements diff aggregation method for singlestat [#7234](https://github.com/grafana/grafana/issues/7234), thx [@oliverpool](https://github.com/oliverpool)
+* **Dataproxy**: Added setting to enable more verbose logging in dataproxy [#7209](https://github.com/grafana/grafana/pull/7209), thx [@Ricky-N](https://github.com/Ricky-N)
+* **Alerting**: Better information about why an alert triggered [#7035](https://github.com/grafana/grafana/issues/7035)
+* **LINE**: Add LINE as alerting notification channel [#7301](https://github.com/grafana/grafana/pull/7301), thx [#huydx](https://github.com/huydx)
+* **Elasticsearch**: Support for Min Doc Count options in Terms aggregation [#7324](https://github.com/grafana/grafana/pull/7324), thx [#lpic10](https://github.com/lpic10)
+* **Elasticsearch**: Term aggregation limit can now be changed in template queries [#7112](https://github.com/grafana/grafana/issues/7112), thx [#FFalcon](https://github.com/FFalcon)
+
+## Tech
+
+* **Library Upgrade**: Upgraded angularjs from 1.5.8 to 1.6.1 [#7274](https://github.com/grafana/grafana/issues/7274)
+
+## Bugfixes
+* **Alerting**: Fixes missing support for no_data and execution error when testing alerts [#7149](https://github.com/grafana/grafana/issues/7149)
+* **Dashboard**: Avoid duplicate data in dashboard json for panels with alerts [#7256](https://github.com/grafana/grafana/pull/7256)
+* **Alertlist**: Only show scrollbar when required [#7269](https://github.com/grafana/grafana/issues/7269)
+* **SMTP**: Set LocalName to hostname [#7223](https://github.com/grafana/grafana/issues/7223)
+* **Sidemenu**: Disable sign out in sidemenu for AuthProxyEnabled [#7377](https://github.com/grafana/grafana/pull/7377), thx [@solugebefola](https://github.com/solugebefola)
+
+# 4.1.2 (unreleased)
+
+### Bugfixes
+* **Table**: Fixes broken annotation rendering mode in the table panel [#7268](https://github.com/grafana/grafana/issues/7268)
+
+# 4.1.1 (2017-01-11)
+
+### Bugfixes
+* **Graph Panel**: Fixed issue with legend height in table mode [#7221](https://github.com/grafana/grafana/issues/7221)
+
+# 4.1.0 (2017-01-11)
+
+### Bugfixes
+* **Server side PNG rendering**: Fixed issue with y-axis label rotation in phantomjs rendered images [#6924](https://github.com/grafana/grafana/issues/6924)
+* **Graph**: Fixed centering of y-axis label [#7099](https://github.com/grafana/grafana/issues/7099)
+* **Graph**: Fixed graph legend table mode and always visible scrollbar [#6828](https://github.com/grafana/grafana/issues/6828)
+* **Templating**: Fixed template variable value groups/tags feature [#6752](https://github.com/grafana/grafana/issues/6752)
+* **Webhook**: Fixed webhook username mismatch [#7195](https://github.com/grafana/grafana/pull/7195), thx [@theisenmark](https://github.com/theisenmark)
+* **Influxdb**: Handles time(auto) the same way as time($interval) [#6997](https://github.com/grafana/grafana/issues/6997)
+
+## Enhancements
+* **Elasticsearch**: Added support for all moving average options [#7154](https://github.com/grafana/grafana/pull/7154), thx [@vaibhavinbayarea](https://github.com/vaibhavinbayarea)
+
+# 4.1-beta1 (2016-12-21)
 
 ### Enhancements
 * **Postgres**: Add support for Certs for Postgres database [#6655](https://github.com/grafana/grafana/issues/6655)
@@ -17,6 +66,7 @@
 * **Alerting**: Adds OK as no data option. [#6866](https://github.com/grafana/grafana/issues/6866)
 * **Alert list**: Order alerts based on state. [#6676](https://github.com/grafana/grafana/issues/6676)
 * **Alerting**: Add api endpoint for pausing all alerts. [#6589](https://github.com/grafana/grafana/issues/6589)
+* **Panel**: Added help text for panels. [#4079](https://github.com/grafana/grafana/issues/4079), thx [@utkarshcmu](https://github.com/utkarshcmu)
 
 ### Bugfixes
 * **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679)

+ 1 - 1
Makefile

@@ -4,7 +4,7 @@ deps-go:
 	go run build.go setup
 
 deps-js:
-	npm install
+	yarn install --pure-lockfile
 
 deps: deps-go deps-js
 

+ 5 - 3
README.md

@@ -1,4 +1,4 @@
-[Grafana](http://grafana.org) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Coverage Status](https://coveralls.io/repos/grafana/grafana/badge.png)](https://coveralls.io/r/grafana/grafana)
+[Grafana](http://grafana.org) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) 
 ================
 [Website](http://grafana.org) |
 [Twitter](https://twitter.com/grafana) |
@@ -10,7 +10,7 @@
 Grafana is an open source, feature rich metrics dashboard and graph editor for
 Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
 
-![](http://grafana.org/assets/img/start_page_bg.png)
+![](http://grafana.org/assets/img/features/dashboard_ex1.png)
 
 - [Install instructions](http://docs.grafana.org/installation/)
 - [What's New in Grafana 2.0](http://docs.grafana.org/guides/whats-new-in-v2/)
@@ -18,6 +18,7 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
 - [What's New in Grafana 2.5](http://docs.grafana.org/guides/whats-new-in-v2-5/)
 - [What's New in Grafana 3.0](http://docs.grafana.org/guides/whats-new-in-v3/)
 - [What's New in Grafana 4.0](http://docs.grafana.org/guides/whats-new-in-v4/)
+- [What's New in Grafana 4.1](http://docs.grafana.org/guides/whats-new-in-v4-1/)
 
 ## Features
 ### Graphite Target Editor
@@ -113,7 +114,8 @@ To build less to css for the frontend you will need a recent version of **node (
 npm (v2.5.0) and grunt (v0.4.5). Run the following:
 
 ```bash
-npm install
+npm install -g yarn
+yarn install --pure-lockfile
 npm run build
 ```
 

+ 3 - 2
appveyor.yml

@@ -5,13 +5,14 @@ os: Windows Server 2012 R2
 clone_folder: c:\gopath\src\github.com\grafana\grafana
 
 environment:
-  nodejs_version: "5"
+  nodejs_version: "6"
   GOPATH: c:\gopath
 
 install:
   # install nodejs and npm
   - ps: Install-Product node $env:nodejs_version
-  - npm install
+  - npm install -g yarn
+  - yarn install --pure-lockfile
   - npm install -g grunt-cli
   # install gcc (needed for sqlite3)
   - choco install -y --limit-output mingw

+ 6 - 5
bower.json

@@ -15,11 +15,12 @@
   "dependencies": {
     "jquery": "3.1.0",
     "lodash": "4.15.0",
-    "angular": "1.5.8",
-    "angular-route": "1.5.8",
-    "angular-mocks": "1.5.8",
-    "angular-sanitize": "1.5.8",
+    "angular": "1.6.1",
+    "angular-route": "1.6.1",
+    "angular-mocks": "1.6.1",
+    "angular-sanitize": "1.6.1",
     "angular-native-dragdrop": "1.2.2",
-    "angular-bindonce": "0.3.3"
+    "angular-bindonce": "0.3.3",
+    "clipboard": "^1.5.16"
   }
 }

+ 30 - 32
build.go

@@ -37,6 +37,7 @@ var (
 	race                  bool
 	phjsToRelease         string
 	workingDir            string
+	includeBuildNumber    bool     = true
 	binaries              []string = []string{"grafana-server", "grafana-cli"}
 )
 
@@ -47,9 +48,6 @@ func main() {
 	log.SetFlags(0)
 
 	ensureGoPath()
-	readVersionFromPackageJson()
-
-	log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration)
 
 	flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
 	flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
@@ -59,8 +57,13 @@ func main() {
 	flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
 	flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
 	flag.BoolVar(&race, "race", race, "Use race detector")
+	flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
 	flag.Parse()
 
+	readVersionFromPackageJson()
+
+	log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration)
+
 	if flag.NArg() == 0 {
 		log.Println("Usage: go run build.go build")
 		return
@@ -73,9 +76,9 @@ func main() {
 		case "setup":
 			setup()
 
-    case "build-cli":
-      clean()
-      build("grafana-cli", "./pkg/cmd/grafana-cli", []string{})
+		case "build-cli":
+			clean()
+			build("grafana-cli", "./pkg/cmd/grafana-cli", []string{})
 
 		case "build":
 			clean()
@@ -90,24 +93,20 @@ 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 "sha1-dist":
+			sha1FilesInDist()
 
 		case "latest":
 			makeLatestDistCopies()
-			sha1FilesInDist()
 
 		case "clean":
 			clean()
@@ -157,7 +156,9 @@ func readVersionFromPackageJson() {
 	}
 
 	// add timestamp to iteration
-	linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration)
+	if includeBuildNumber {
+		linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration)
+	}
 }
 
 type linuxPackageOptions struct {
@@ -167,7 +168,6 @@ type linuxPackageOptions struct {
 	serverBinPath          string
 	cliBinPath             string
 	configDir              string
-	configFilePath         string
 	ldapFilePath           string
 	etcDefaultPath         string
 	etcDefaultFilePath     string
@@ -188,8 +188,6 @@ func createDebPackages() {
 		homeDir:                "/usr/share/grafana",
 		binPath:                "/usr/sbin",
 		configDir:              "/etc/grafana",
-		configFilePath:         "/etc/grafana/grafana.ini",
-		ldapFilePath:           "/etc/grafana/ldap.toml",
 		etcDefaultPath:         "/etc/default",
 		etcDefaultFilePath:     "/etc/default/grafana-server",
 		initdScriptFilePath:    "/etc/init.d/grafana-server",
@@ -210,8 +208,6 @@ func createRpmPackages() {
 		homeDir:                "/usr/share/grafana",
 		binPath:                "/usr/sbin",
 		configDir:              "/etc/grafana",
-		configFilePath:         "/etc/grafana/grafana.ini",
-		ldapFilePath:           "/etc/grafana/ldap.toml",
 		etcDefaultPath:         "/etc/sysconfig",
 		etcDefaultFilePath:     "/etc/sysconfig/grafana-server",
 		initdScriptFilePath:    "/etc/init.d/grafana-server",
@@ -222,7 +218,7 @@ func createRpmPackages() {
 		defaultFileSrc: "packaging/rpm/sysconfig/grafana-server",
 		systemdFileSrc: "packaging/rpm/systemd/grafana-server.service",
 
-		depends: []string{"initscripts", "fontconfig"},
+		depends: []string{"/sbin/service", "fontconfig"},
 	})
 }
 
@@ -256,10 +252,6 @@ func createPackage(options linuxPackageOptions) {
 	runPrint("cp", "-a", filepath.Join(workingDir, "tmp")+"/.", filepath.Join(packageRoot, options.homeDir))
 	// remove bin path
 	runPrint("rm", "-rf", filepath.Join(packageRoot, options.homeDir, "bin"))
-	// copy sample ini file to /etc/grafana
-	runPrint("cp", "conf/sample.ini", filepath.Join(packageRoot, options.configFilePath))
-	// copy sample ldap toml config file to /etc/grafana/ldap.toml
-	runPrint("cp", "conf/ldap.toml", filepath.Join(packageRoot, options.ldapFilePath))
 
 	args := []string{
 		"-s", "dir",
@@ -269,8 +261,6 @@ func createPackage(options linuxPackageOptions) {
 		"--url", "http://grafana.org",
 		"--license", "\"Apache 2.0\"",
 		"--maintainer", "contact@grafana.org",
-		"--config-files", options.configFilePath,
-		"--config-files", options.ldapFilePath,
 		"--config-files", options.initdScriptFilePath,
 		"--config-files", options.etcDefaultFilePath,
 		"--config-files", options.systemdServiceFilePath,
@@ -334,7 +324,12 @@ func grunt(params ...string) {
 }
 
 func gruntBuildArg(task string) []string {
-	args := []string{task, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration)}
+	var args []string
+	if includeBuildNumber {
+		args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
+	} else {
+		args = append(args, fmt.Sprintf("--pkgVer=%v", linuxPackageVersion))
+	}
 	if pkgArch != "" {
 		args = append(args, fmt.Sprintf("--arch=%v", pkgArch))
 	}
@@ -429,14 +424,10 @@ func setBuildEnv() {
 }
 
 func getGitSha() string {
-	v, err := runError("git", "describe", "--always", "--dirty")
+	v, err := runError("git", "rev-parse", "--short", "HEAD")
 	if err != nil {
 		return "unknown-dev"
 	}
-	v = versionRe.ReplaceAllFunc(v, func(s []byte) []byte {
-		s[0] = '+'
-		return s
-	})
 	return string(v)
 }
 
@@ -516,8 +507,15 @@ func md5File(file string) error {
 
 func sha1FilesInDist() {
 	filepath.Walk("./dist", func(path string, f os.FileInfo, err error) error {
+		if path == "./dist" {
+			return nil
+		}
+
 		if strings.Contains(path, ".sha1") == false {
-			sha1File(path)
+			err := sha1File(path)
+			if err != nil {
+				log.Printf("Failed to create sha file. error: %v\n", err)
+			}
 		}
 		return nil
 	})

+ 32 - 7
circle.yml

@@ -1,18 +1,26 @@
 machine:
   node:
-    version: 5.11.1
+    version: 6.9.2
+  python:
+    version: 2.7.3
+  services:
+    - docker
   environment:
     GOPATH: "/home/ubuntu/.go_workspace"
     ORG_PATH: "github.com/grafana"
     REPO_PATH: "${ORG_PATH}/grafana"
     GODIST: "go1.7.4.linux-amd64.tar.gz"
   post:
-    - mkdir -p download
+    - mkdir -p ~/download
+    - mkdir -p ~/docker
     - test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST
     - sudo rm -rf /usr/local/go
     - sudo tar -C /usr/local -xzf download/$GODIST
 
 dependencies:
+  cache_directories:
+    - "~/docker"
+    - "~/download"
   override:
     - rm -rf ${GOPATH}/src/${REPO_PATH}
     - mkdir -p ${GOPATH}/src/${ORG_PATH}
@@ -23,9 +31,26 @@ test:
      - bash scripts/circle-test.sh
 
 deployment:
-  master:
-    branch: master
-    owner: grafana
+  gh_branch:
+    branch: new_master
     commands:
-      - ./scripts/trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
-      - ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN}
+      - pip install awscli
+      - sudo apt-get update; sudo apt-get install rpm; sudo apt-get install expect
+      - ./scripts/build/build_container.sh
+      - ./scripts/build/deploy.sh
+      - ./scripts/build/sign_packages.sh
+      - go run build.go sha1-dist
+      - aws s3 sync ./dist s3://$BUCKET_NAME/master
+      #- ./scripts/trigger_grafana_docker_build.sh ${TRIGGER_GRAFANA_DOCKER_CIRCLECI_TOKEN}
+  gh_tag:
+    tag: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
+    commands:
+      - pip install awscli
+      - sudo apt-get update; sudo apt-get install rpm; sudo apt-get install expect
+      - ./scripts/build/build_container.sh
+      - ./scripts/build/deploy.sh
+      - ./scripts/build/sign_packages.sh
+      - go run build.go sha1-dist
+      - aws s3 sync ./dist s3://$BUCKET_NAME/release
+      #- ./scripts/trigger_grafana_docker_build.sh ${TRIGGER_GRAFANA_DOCKER_CIRCLECI_TOKEN}
+

+ 10 - 1
conf/defaults.ini

@@ -113,6 +113,12 @@ cookie_secure = false
 session_life_time = 86400
 gc_interval_time = 86400
 
+#################################### Data proxy ###########################
+[dataproxy]
+
+# This enables data proxy logging, default is false
+logging = false
+
 #################################### Analytics ###########################
 [analytics]
 # Server reporting, sends usage counters to stats.grafana.org every 24 hours.
@@ -279,6 +285,7 @@ allow_sign_up = true
 enabled = false
 host = localhost:25
 user =
+# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
 password =
 cert_file =
 key_file =
@@ -395,7 +402,9 @@ global_session = -1
 
 #################################### Alerting ############################
 [alerting]
-# Makes it possible to turn off alert rule execution.
+# Disable alerting engine & UI features
+enabled = true
+# Makes it possible to turn off alert rule execution but alerting UI is visible
 execute_alerts = true
 
 #################################### Internal Grafana Metrics ############

+ 1 - 0
conf/ldap.toml

@@ -19,6 +19,7 @@ ssl_skip_verify = false
 # Search user bind dn
 bind_dn = "cn=admin,dc=grafana,dc=org"
 # Search user bind password
+# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
 bind_password = 'grafana'
 
 # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"

+ 12 - 2
conf/sample.ini

@@ -104,6 +104,13 @@
 # Session life time, default is 86400
 ;session_life_time = 86400
 
+#################################### Data proxy ###########################
+[dataproxy]
+
+# This enables data proxy logging, default is false
+;logging = false
+
+
 #################################### Analytics ####################################
 [analytics]
 # Server reporting, sends usage counters to stats.grafana.org every 24 hours.
@@ -263,6 +270,7 @@
 ;enabled = false
 ;host = localhost:25
 ;user =
+# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
 ;password =
 ;cert_file =
 ;key_file =
@@ -342,9 +350,11 @@
 ;enabled = false
 ;path = /var/lib/grafana/dashboards
 
-#################################### Alerting ######################################
+#################################### Alerting ############################
 [alerting]
-# Makes it possible to turn off alert rule execution.
+# Disable alerting engine & UI features
+;enabled = true
+# Makes it possible to turn off alert rule execution but alerting UI is visible
 ;execute_alerts = true
 
 #################################### Internal Grafana Metrics ##########################

+ 30 - 0
docs/sources/administration/cli.md

@@ -0,0 +1,30 @@
++++
+title = "Grafana CLI"
+description = "Guide to using grafana-cli"
+keywords = ["grafana", "cli", "grafana-cli", "command line interface"]
+type = "docs"
+[menu.docs]
+parent = "admin"
+weight = 8
++++
+
+# Grafana CLI
+
+Grafana cli is a small executable that is bundled with grafana server and is suppose to be executed on the same machine as grafana runs. 
+
+## Plugins
+
+The CLI helps you install, upgrade and manage your plugins on the same machine it CLI is running. You can find more information about how to install and manage your plugins at the [plugin page] ({{< relref "/installation.md" >}})
+
+## Admin
+
+> This feature is only available in grafana 4.1 and above.
+
+To show all admin commands:
+`grafana-cli admin`
+
+### Reset admin password
+
+You can reset the password for the admin user using the CLI.
+
+`grafana-cli admin reset-admin-password ...`

+ 0 - 0
docs/sources/datasources/cloudwatch.md → docs/sources/features/datasources/cloudwatch.md


+ 0 - 0
docs/sources/datasources/elasticsearch.md → docs/sources/features/datasources/elasticsearch.md


+ 0 - 0
docs/sources/datasources/graphite.md → docs/sources/features/datasources/graphite.md


+ 0 - 0
docs/sources/datasources/index.md → docs/sources/features/datasources/index.md


+ 0 - 0
docs/sources/datasources/influxdb.md → docs/sources/features/datasources/influxdb.md


+ 0 - 0
docs/sources/datasources/kairosdb.md → docs/sources/features/datasources/kairosdb.md


+ 0 - 0
docs/sources/datasources/opentsdb.md → docs/sources/features/datasources/opentsdb.md


+ 0 - 0
docs/sources/datasources/plugin_api.md → docs/sources/features/datasources/plugin_api.md


+ 5 - 0
docs/sources/datasources/prometheus.md → docs/sources/features/datasources/prometheus.md

@@ -72,4 +72,9 @@ label_values(hostname)
 
 You can also use raw queries & regular expressions to extract anything you might need.
 
+### Using templated variables in queries
+
+When the `Include All` option or `Multi-Value` option is enabled, Grafana converts the labels from plain text to a regex compatible string. 
+Which means you have to use `=~` instead of `=` in your Prometheus queries. For example `ALERTS{instance=~$instance}` instead of `ALERTS{instance=$instance}`.
+
 ![](/img/v2/prometheus_templating.png)

+ 54 - 0
docs/sources/features/datasources/testdata.md

@@ -0,0 +1,54 @@
++++
+title = "Grafana TestData"
+keywords = ["grafana", "dashboard", "documentation", "panels", "testdata"]
+type = "docs"
+[menu.docs]
+name = "Grafana TestData"
+parent = "datasources"
+weight = 2
++++
+
+
+# Grafana TestData 
+
+    > NOTE: This plugin is disable by default. 
+
+The purpose of this data sources is to make it easier to create fake data for any panel. 
+Using `Grafana TestData` you can build your own time series and have any panel render it. 
+This make is much easier to verify functionally since the data can be shared very 
+
+## Enable 
+
+`Grafana TestData` is not enabled by default. To enable it you have to go to `/plugins/testdata/edit` and click the enable button to enable it for each server.
+
+## Create mock data.
+
+Once `Grafana TestData` is enabled you use it as a datasource in the metric panel.
+
+![](/img/docs/v41/test_data_add.png)
+
+## Scenarios
+
+You can now choose different scenario that you want rendered in the drop down menu. If you have scenarios that you think should be added, please add them to `` and submit a pull request.
+
+## CSV
+
+The comma separated values scenario is the most powerful one since it lets you create any kind of graph you like. 
+Once you provided the numbers `Grafana TestData` will distribute them evenly based on the time range of your query. 
+
+![](/img/docs/v41/test_data_csv_example.png)
+
+
+## Dashboards
+
+`Grafana TestData` also contains some dashboards with example. `/plugins/testdata/edit`
+
+### Commit updates to the dashboards
+
+If you want to submit a change to one of the current dashboards bundled with `Grafana TestData` you have to update the revision property. 
+Otherwise the dashboard will not be updated automatically for other Grafana users.
+
+## Using test data in issues
+
+If you post an issue on github regarding time series data or rendering of time series data we strongly advice you to use this data source to replicate the data. 
+That makes it much easier for the developers to replicate and solve the issue you have. 

+ 0 - 0
docs/sources/reference/dashlist.md → docs/sources/features/panels/dashlist.md


+ 0 - 0
docs/sources/reference/graph.md → docs/sources/features/panels/graph.md


+ 2 - 1
docs/sources/reference/singlestat.md → docs/sources/features/panels/singlestat.md

@@ -26,11 +26,12 @@ The singlestat panel has a normal query editor to allow you define your exact me
 3. `Values`: The Value fields let you set the function (min, max, average, current, total, first, delta, range) that your entire query is reduced into a single value with. You can also set the font size of the Value field and font-size (as a %) of the metric query that the Panel is configured with. This reduces the entire query into a single summary value that is displayed.
    * `min` - The smallest value in the series
    * `max` - The largest value in the series
-   * `average` - The average of all the non-null values in the series
+   * `avg` - The average of all the non-null values in the series
    * `current` - The last value in the series. If the series ends on null the previous value will be used.
    * `total` - The sum of all the non-null values in the series
    * `first` - The first value in the series
    * `delta` - The total incremental increase (of a counter) in the series. An attempt is made to account for counter resets, but this will only be accurate for single instance metrics. Used to show total counter increase in time series.
+   * `diff` - The difference betwen 'current' (last value) and 'first'.
    * `range` - The difference between 'min' and 'max'. Useful the show the range of change for a gauge.
 4. `Postfixes`: The Postfix fields let you define a custom label and font-size (as a %) to appear *after* the value
 5. `Units`: Units are appended to the the Singlestat  within the panel, and will respect the color and threshold settings for the value.

+ 0 - 0
docs/sources/reference/table_panel.md → docs/sources/features/panels/table_panel.md


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

@@ -7,7 +7,7 @@ type = "docs"
 name = "Version 3.1"
 identifier = "v3.1"
 parent = "whatsnew"
-weight = 1
+weight = 5
 +++
 
 # What's New in Grafana v3.1

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

@@ -7,7 +7,7 @@ type = "docs"
 name = "Version 3.0"
 identifier = "v3.0"
 parent = "whatsnew"
-weight = 2
+weight = 6
 +++
 
 # What's New in Grafana v3.0

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

@@ -0,0 +1,70 @@
++++
+title = "What's New in Grafana v4.1"
+description = "Feature & improvement highlights for Grafana v4.1"
+keywords = ["grafana", "new", "documentation", "4.1.0"]
+type = "docs"
+[menu.docs]
+name = "Version 4.1"
+identifier = "v4.1"
+parent = "whatsnew"
+weight = -1
++++
+
+
+## Whats new in Grafana v4.1
+- **Graph**: Support for shared tooltip on all graphs as you hover over one graph. [#1578](https://github.com/grafana/grafana/pull/1578), [#6274](https://github.com/grafana/grafana/pull/6274)
+- **Victorops**: Add VictorOps notification integration [#6411](https://github.com/grafana/grafana/issues/6411), thx [@ichekrygin](https://github.com/ichekrygin)
+- **Opsgenie**: Add OpsGenie notification integratiion [#6687](https://github.com/grafana/grafana/issues/6687), thx [@kylemcc](https://github.com/kylemcc)
+- **Cloudwatch**: Make it possible to specify access and secret key on the data source config page [#6697](https://github.com/grafana/grafana/issues/6697)
+- **Elasticsearch**: Added support for Elasticsearch 5.x [#5740](https://github.com/grafana/grafana/issues/5740), thx [@lpic10](https://github.com/lpic10)
+- **Panel**: Added help text for panels. [#4079](https://github.com/grafana/grafana/issues/4079), thx [@utkarshcmu](https://github.com/utkarshcmu)
+- [Full changelog](https://github.com/grafana/grafana/blob/master/CHANGELOG.md)
+
+### Shared tooltip
+
+{{< imgbox max-width="60%" img="/img/docs/v41/shared_tooltip.gif" caption="Shared tooltip" >}}
+
+Showing the tooltip on all panels at the same time has been a long standing request in Grafana and we are really happy to finally be able to release it. 
+You can enable/disable the shared tooltip from the dashboard settings menu or cycle between default, shared tooltip and shared crosshair by pressing `CTRL + O` or `CMD + O`.
+
+<div class="clearfix"></div>
+
+### Help text for panel
+
+{{< imgbox max-width="60%" img="/img/docs/v41/helptext_for_panel_settings.png" caption="Hovering help text" >}}
+
+You can set a help text in the general tab on any panel. The help text is using Markdown to enable better formating and linking to other sites that can provide more information.
+
+<div class="clearfix"></div>
+
+{{< imgbox max-width="60%" img="/img/docs/v41/helptext_hover.png" caption="Hovering help text" >}}
+
+Panels with a help text available have a little indicator in the top left corner. You can show the help text by hovering the icon.
+<div class="clearfix"></div>
+
+
+### Easier Cloudwatch configuration
+
+{{< imgbox max-width="60%" img="/img/docs/v41/cloudwatch_settings.png" caption="Cloudwatch configuration" >}}
+
+In Grafana 4.1.0 you can configure your Cloudwatch data source with `access key` and `secret key` directly in the data source configuration page.
+This enables people to use the Cloudwatch data source without having access to the filesystem where Grafana is running.
+
+Once the `access key` and `secret key` have been saved the user will no longer be able to view them. 
+<div class="clearfix"></div>
+
+## Upgrade & Breaking changes
+
+Elasticsearch 1.x is no longer supported. Please upgrade to Elasticsearch 2.x or 5.x. Otherwise Grafana 4.1.0 contains no breaking changes.
+
+## Changelog
+
+Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list
+of new features, changes, and bug fixes.
+
+## Download
+
+Head to [v4.1 download page](/download/4_1_0/) for download links & instructions.
+
+## Thanks
+A big thanks to all the Grafana users who contribute by submitting PRs, bug reports & feedback!

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

@@ -4,10 +4,10 @@ description = "Feature & improvement highlights for Grafana v4.0"
 keywords = ["grafana", "new", "documentation", "4.0"]
 type = "docs"
 [menu.docs]
-name = "Version 4.0 (Latest)"
+name = "Version 4.0"
 identifier = "v4.0"
 parent = "whatsnew"
-weight = -1
+weight = 4
 +++
 
 # What's New in Grafana v4.0

+ 18 - 0
docs/sources/http_api/admin.md

@@ -143,6 +143,7 @@ with Grafana admin permission.
         "protocol":"http",
         "root_url":"%(protocol)s://%(domain)s:%(http_port)s/",
         "router_logging":"true",
+        "data_proxy_logging":"true",
         "static_root_path":"public"
       },
       "session":{
@@ -275,3 +276,20 @@ Change password for specific user
     Content-Type: application/json
 
     {message: "User deleted"}
+
+## Pause all alerts
+
+`DELETE /api/admin/pause-all-alerts`
+
+**Example Request**:
+
+    DELETE /api/admin/pause-all-alerts HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+
+    {state: "new state", message: "alerts pause/un paused", "alertsAffected": 100}    

+ 216 - 0
docs/sources/http_api/alerting.md

@@ -0,0 +1,216 @@
++++
+title = "Alerting HTTP API "
+description = "Grafana Alerting HTTP API"
+keywords = ["grafana", "http", "documentation", "api", "alerting"]
+aliases = ["/http_api/alerting/"]
+type = "docs"
+[menu.docs]
+name = "Alerting"
+parent = "http_api"
++++
+
+
+# Alerting API
+
+You can use the Alerting API to get information about alerts and their states but this API cannot be used to modify the alert. 
+To create new alerts or modify them you need to update the dashboard json that contains the alerts. 
+
+This API can also be used to create, update and delete alert notifications.
+
+## Get alerts
+
+`GET /api/alerts/`
+
+**Example Request**:
+
+    GET /api/alerts HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+    [
+      {
+        "id": 1,
+        "dashboardId": 1,
+        "panelId": 1,
+        "name": "fire place sensor",
+        "message": "Someone is trying to break in through the fire place",
+        "state": "alerting",
+        "newStateDate": "2016-12-25",
+        "executionError": "",
+        "dashboardUri": "http://grafana.com/dashboard/db/sensors"
+      }
+    ]
+
+## Get one alert
+
+`GET /api/alerts/:id`
+
+**Example Request**:
+
+    GET /api/alerts/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+    {
+      "id": 1,
+      "dashboardId": 1,
+      "panelId": 1,
+      "name": "fire place sensor",
+      "message": "Someone is trying to break in through the fire place",
+      "state": "alerting",
+      "newStateDate": "2016-12-25",
+      "executionError": "",
+      "dashboardUri": "http://grafana.com/dashboard/db/sensors"
+    }
+
+
+## Pause alert
+
+`POST /api/alerts/:id/pause`
+
+**Example Request**:
+
+    POST /api/alerts/1/pause HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "alertId": 1,
+      "paused: true
+    }
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+    {
+      "alertId": 1,
+      "state":   "Paused",
+      "message": "alert paused"
+    }
+
+## Get alert notifications
+
+`GET /api/alert-notifications`
+
+**Example Request**:
+
+    GET /api/alert-notifications HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+    
+    {
+      "id": 1,
+      "name": "Team A",
+      "type": "email",
+      "isDefault": true,
+      "created": "2017-01-01 12:45",
+      "updated": "2017-01-01 12:45"
+    }
+
+## Create alert notification
+
+`POST /api/alerts-notifications`
+
+**Example Request**:
+
+    POST /api/alerts-notifications HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "name": "new alert notification",  //Required
+      "type":  "email", //Required
+      "isDefault": false,
+      "settings": {
+        "addresses": "carl@grafana.com;dev@grafana.com"
+      }
+    }
+    
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+    {
+      "id": 1, 
+      "name": "new alert notification",
+      "type": "email",
+      "isDefault": false,
+      "settings": { addresses: "carl@grafana.com;dev@grafana.com"} }
+      "created": "2017-01-01 12:34", 
+      "updated": "2017-01-01 12:34"
+    }
+
+## Update alert notification
+
+`PUT /api/alerts-notifications/1`
+
+**Example Request**:
+
+    PUT /api/alerts-notifications/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    {
+      "id": 1,
+      "name": "new alert notification",  //Required
+      "type":  "email", //Required
+      "isDefault": false,
+      "settings": { 
+        "addresses: "carl@grafana.com;dev@grafana.com"
+      }
+    }
+    
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+    {
+      "id": 1, 
+      "name": "new alert notification",
+      "type": "email",
+      "isDefault": false,
+      "settings": { addresses: "carl@grafana.com;dev@grafana.com"} }
+      "created": "2017-01-01 12:34", 
+      "updated": "2017-01-01 12:34"
+    }
+
+## Delete alert notification
+
+`DELETE /api/alerts-notifications/:notificationId`
+
+**Example Request**:
+
+    DELETE /api/alerts-notifications/1 HTTP/1.1
+    Accept: application/json
+    Content-Type: application/json
+    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+**Example Response**:
+
+    HTTP/1.1 200
+    Content-Type: application/json
+    {
+      "message": "Notification deleted"
+    }

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

@@ -200,7 +200,7 @@ Get all tags of dashboards
 
 **Example Request**:
 
-    GET /api/dashboards/home HTTP/1.1
+    GET /api/dashboards/tags HTTP/1.1
     Accept: application/json
     Content-Type: application/json
     Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk

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

@@ -158,7 +158,7 @@ parent = "http_api"
     HTTP/1.1 200
     Content-Type: application/json
 
-    {"id":1,"message":"Datasource added"}
+    {"id":1,"message":"Datasource added", "name": "test_datasource"}
 
 ## Update an existing data source
 
@@ -193,7 +193,7 @@ parent = "http_api"
     HTTP/1.1 200
     Content-Type: application/json
 
-    {"message":"Datasource updated"}
+    {"message":"Datasource updated", "id": 1, "name": "test_datasource"}
 
 ## Delete an existing data source
 

+ 34 - 0
docs/sources/http_api/user.md

@@ -69,6 +69,40 @@ parent = "http_api"
       "isGrafanaAdmin": true
     }
 
+## Get single user by Username(login) or Email
+
+    `GET /api/users/lookup`
+
+    **Parameter:** `loginOrEmail`
+
+    **Example Request using the email as option**:
+
+        GET /api/users/lookup?loginOrEmail=user@mygraf.com HTTP/1.1
+        Accept: application/json
+        Content-Type: application/json
+        Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    **Example Request using the username as option**:
+        GET /api/users/lookup?loginOrEmail=admin HTTP/1.1
+        Accept: application/json
+        Content-Type: application/json
+        Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+    **Example Response**:
+
+        HTTP/1.1 200
+        Content-Type: application/json
+
+        {
+          "email": "user@mygraf.com"
+          "name": "admin",
+          "login": "admin",
+          "theme": "light",
+          "orgId": 1,
+          "isGrafanaAdmin": true
+        }
+
+
 ## User Update
 
 `PUT /api/users/:id`

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

@@ -144,6 +144,10 @@ Grafana needs a database to store users and dashboards (and other
 things). By default it is configured to use `sqlite3` which is an
 embedded database (included in the main Grafana binary).
 
+### url
+Use either URL or or the other fields below to configure the database
+Example: `mysql://user:secret@host:port/database`
+
 ### type
 
 Either `mysql`, `postgres` or `sqlite3`, it's your choice.
@@ -244,7 +248,10 @@ organization to be created for that new user.
 
 The role new users will be assigned for the main organization (if the
 above setting is set to true).  Defaults to `Viewer`, other valid
-options are `Admin` and `Editor` and `Read-Only Editor`.
+options are `Admin` and `Editor` and `Read Only Editor`. e.g. :
+
+`auto_assign_org_role = Read Only Editor`
+
 
 <hr>
 
@@ -611,6 +618,11 @@ basic auth password
 
 ## [alerting]
 
+### enabled
+Defaults to true. Set to false to disable alerting engine and hide Alerting from UI.
+
+### execute_alerts
+
 ### execute_alerts = true
 
 Makes it possible to turn off alert rule execution.

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

@@ -15,14 +15,14 @@ weight = 1
 
 Description | Download
 ------------ | -------------
-Stable for Debian-based Linux | [4.0.2 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.2-1481203731_amd64.deb)
+Stable for Debian-based Linux | [4.1.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.1-1484211277_amd64.deb)
 
 ## Install Stable
 
 ```
-$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.2-1481203731_amd64.deb
+$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.1-1484211277_amd64.deb
 $ sudo apt-get install -y adduser libfontconfig
-$ sudo dpkg -i grafana_4.0.2-1481203731_amd64.deb
+$ sudo dpkg -i grafana_4.1.1-1484211277_amd64.deb
 ```
 
 ## APT Repository

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

@@ -43,6 +43,7 @@ ssl_skip_verify = false
 # Search user bind dn
 bind_dn = "cn=admin,dc=grafana,dc=org"
 # Search user bind password
+# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
 bind_password = 'grafana'
 
 # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"

+ 4 - 4
docs/sources/installation/rpm.md

@@ -15,24 +15,24 @@ weight = 2
 
 Description | Download
 ------------ | -------------
-Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.0.2 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm)
+Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.1.1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.1-1484211277.x86_64.rpm)
 
 ## Install Stable
 
 You can install Grafana using Yum directly.
 
-    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm
+    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.1-1484211277.x86_64.rpm
 
 Or install manually using `rpm`.
 
 #### On CentOS / Fedora / Redhat:
 
     $ sudo yum install initscripts fontconfig
-    $ sudo rpm -Uvh grafana-4.0.2-1481203731.x86_64.rpm
+    $ sudo rpm -Uvh grafana-4.1.1-1484211277.x86_64.rpm
 
 #### On OpenSuse:
 
-    $ sudo rpm -i --nodeps grafana-4.0.2-1481203731.x86_64.rpm
+    $ sudo rpm -i --nodeps grafana-4.1.1-1484211277.x86_64.rpm
 
 ## Install via YUM Repository
 

+ 1 - 1
docs/sources/installation/windows.md

@@ -13,7 +13,7 @@ weight = 3
 
 Description | Download
 ------------ | -------------
-Latest stable package for Windows | [grafana.4.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2.windows-x64.zip)
+Latest stable package for Windows | [grafana.4.1.1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.1.windows-x64.zip)
 
 ## Configure
 

+ 3 - 3
docs/sources/plugins/datasources.md

@@ -37,7 +37,7 @@ The Datasource should contain the following functions.
 ```
 query(options) //used by panels to get data
 testDatasource() //used by datasource configuration page to make sure the connection is working
-annotationsQuery(options) // used by dashboards to get annotations
+annotationQuery(options) // used by dashboards to get annotations
 metricFindQuery(options) // used by query editor to get metric suggestions.
 ```
 
@@ -119,7 +119,7 @@ An array of
 
 ### Annotation Query
 
-Request object passed to datasource.annotationsQuery function
+Request object passed to datasource.annotationQuery function
 ```json
 {
   "range": { "from": "2016-03-04T04:07:55.144Z", "to": "2016-03-04T07:07:55.144Z" },
@@ -172,4 +172,4 @@ Requires a static template or templateUrl variable which will be rendered as the
 
 A javascript class that will be instantiated and treated as an Angular controller when the user choose this type of datasource in the templating menu in the dashboard.
 
-Requires a static template or templateUrl variable which will be rendered as the view for this controller. The fields that are bound to this controller is then sent to the Database objects annotationsQuery function.
+Requires a static template or templateUrl variable which will be rendered as the view for this controller. The fields that are bound to this controller is then sent to the Database objects annotationQuery function.

+ 4 - 1
docs/sources/project/building_from_source.md

@@ -23,6 +23,8 @@ export GOPATH=`pwd`
 go get github.com/grafana/grafana
 ```
 
+You may see an error such as: `package github.com/grafana/grafana: no buildable Go source files`. This is just a warning, and you can proceed with the directions.
+
 ## Building the backend
 ```
 cd $GOPATH/src/github.com/grafana/grafana
@@ -40,7 +42,8 @@ To build less to css for the frontend you will need a recent version of node (v0
 npm (v2.5.0) and grunt (v0.4.5). Run the following:
 
 ```
-npm install
+npm install -g yarn
+yarn install --pure-lockfile
 npm install -g grunt-cli
 grunt
 ```

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
-  "stable": "4.0.2",
-	"testing": "4.0.2"
+  "stable": "4.1.1",
+	"testing": "4.1.1"
 }

+ 5 - 8
package.json

@@ -4,7 +4,7 @@
     "company": "Coding Instinct AB"
   },
   "name": "grafana",
-  "version": "4.1.0-pre1",
+  "version": "4.2.0-pre1",
   "repository": {
     "type": "git",
     "url": "http://github.com/grafana/grafana.git"
@@ -29,7 +29,6 @@
     "grunt-contrib-watch": "^1.0.0",
     "grunt-exec": "^1.0.1",
     "grunt-filerev": "^2.3.1",
-    "grunt-git-describe": "~2.4.2",
     "grunt-karma": "~2.0.0",
     "grunt-ng-annotate": "^3.0.0",
     "grunt-notify": "^0.4.5",
@@ -42,13 +41,12 @@
     "karma": "1.3.0",
     "karma-chrome-launcher": "~2.0.0",
     "karma-coverage": "1.1.1",
-    "karma-coveralls": "1.1.2",
     "karma-expect": "~1.1.3",
     "karma-mocha": "~1.3.0",
     "karma-phantomjs-launcher": "1.0.2",
     "load-grunt-tasks": "3.5.2",
     "mocha": "3.2.0",
-    "phantomjs-prebuilt": "^2.1.13",
+    "phantomjs-prebuilt": "^2.1.14",
     "reflect-metadata": "0.1.8",
     "rxjs": "^5.0.0-rc.5",
     "sass-lint": "^1.10.2",
@@ -60,9 +58,8 @@
     "npm": "2.14.x"
   },
   "scripts": {
-    "build": "grunt",
-    "test": "grunt test",
-    "coveralls": "grunt karma:coveralls && rm -rf ./coverage"
+    "build": "./node_modules/grunt-cli/bin/grunt",
+    "test": "./node_modules/grunt-cli/bin/grunt test"
   },
   "license": "Apache-2.0",
   "dependencies": {
@@ -78,7 +75,7 @@
     "sinon": "1.17.6",
     "systemjs-builder": "^0.15.34",
     "tether": "^1.4.0",
-    "tether-drop": "git://github.com/torkelo/drop",
+    "tether-drop": "https://github.com/torkelo/drop",
     "tslint": "^4.0.2",
     "typescript": "^2.1.4",
     "virtual-scroll": "^1.1.1"

+ 6 - 0
packaging/deb/control/postinst

@@ -42,6 +42,12 @@ case "$1" in
 	chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana
 	chmod 755 /var/log/grafana /var/lib/grafana
 
+  # copy user config files
+  if [ ! -f $CONF_FILE ]; then
+    cp /usr/share/grafana/conf/sample.ini $CONF_FILE
+    cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
+  fi
+
 	# configuration files should not be modifiable by grafana user, as this can be a security issue
 	chown -Rh root:$GRAFANA_GROUP /etc/grafana/*
 	chmod 755 /etc/grafana

+ 1 - 1
packaging/deb/default/grafana-server

@@ -14,6 +14,6 @@ CONF_DIR=/etc/grafana
 
 CONF_FILE=/etc/grafana/grafana.ini
 
-RESTART_ON_UPGRADE=false
+RESTART_ON_UPGRADE=true
 
 PLUGINS_DIR=/var/lib/grafana/plugins

+ 2 - 2
packaging/publish/publish_both.sh

@@ -1,6 +1,6 @@
 #! /usr/bin/env bash
-deb_ver=4.0.2-1481203731
-rpm_ver=4.0.2-1481203731
+deb_ver=4.1.0-1484127817
+rpm_ver=4.1.0-1484127817
 
 wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
 

+ 2 - 2
packaging/publish/publish_testing.sh

@@ -1,6 +1,6 @@
 #! /usr/bin/env bash
-deb_ver=4.0.2-1481203731
-rpm_ver=4.0.2-1481203731
+deb_ver=4.1.0-1482230757beta1
+rpm_ver=4.1.0-1482230757beta1
 
 wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
 

+ 7 - 0
packaging/rpm/control/postinst

@@ -6,6 +6,7 @@ set -e
 
 startGrafana() {
   if [ -x /bin/systemctl ] ; then
+    /bin/systemctl daemon-reload
 		/bin/systemctl start grafana-server.service
 	elif [ -x /etc/init.d/grafana-server ] ; then
 		/etc/init.d/grafana-server start
@@ -37,6 +38,12 @@ if [ $1 -eq 1 ] ; then
     -c "grafana user" grafana
 	fi
 
+  # copy user config files
+  if [ ! -f $CONF_FILE ]; then
+    cp /usr/share/grafana/conf/sample.ini $CONF_FILE
+    cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
+  fi
+
  	# Set user permissions on /var/log/grafana, /var/lib/grafana
 	mkdir -p /var/log/grafana /var/lib/grafana
 	chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana

+ 1 - 1
packaging/rpm/sysconfig/grafana-server

@@ -14,6 +14,6 @@ CONF_DIR=/etc/grafana
 
 CONF_FILE=/etc/grafana/grafana.ini
 
-RESTART_ON_UPGRADE=false
+RESTART_ON_UPGRADE=true
 
 PLUGINS_DIR=/var/lib/grafana/plugins

+ 6 - 2
pkg/api/alerting.go

@@ -73,9 +73,9 @@ func GetAlerts(c *middleware.Context) Response {
 			Name:           alert.Name,
 			Message:        alert.Message,
 			State:          alert.State,
-			EvalDate:       alert.EvalDate,
 			NewStateDate:   alert.NewStateDate,
 			ExecutionError: alert.ExecutionError,
+			EvalData:       alert.EvalData,
 		})
 	}
 
@@ -121,10 +121,10 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
 	}
 
 	res := backendCmd.Result
-
 	dtoRes := &dtos.AlertTestResult{
 		Firing:         res.Firing,
 		ConditionEvals: res.ConditionEvals,
+		State:          res.Rule.State,
 	}
 
 	if res.Error != nil {
@@ -173,6 +173,10 @@ func DelAlert(c *middleware.Context) Response {
 	return Json(200, resp)
 }
 
+func GetAlertNotifiers(c *middleware.Context) Response {
+	return Json(200, alerting.GetNotifiers())
+}
+
 func GetAlertNotifications(c *middleware.Context) Response {
 	query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
 

+ 3 - 0
pkg/api/api.go

@@ -125,6 +125,8 @@ func (hs *HttpServer) registerRoutes() {
 			r.Get("/", wrap(SearchUsers))
 			r.Get("/:id", wrap(GetUserById))
 			r.Get("/:id/orgs", wrap(GetUserOrgList))
+			// query parameters /users/lookup?loginOrEmail=admin@example.com
+			r.Get("/lookup", wrap(GetUserByLoginOrEmail))
 			r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
 			r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
 		}, reqGrafanaAdmin)
@@ -261,6 +263,7 @@ func (hs *HttpServer) registerRoutes() {
 		})
 
 		r.Get("/alert-notifications", wrap(GetAlertNotifications))
+		r.Get("/alert-notifiers", wrap(GetAlertNotifiers))
 
 		r.Group("/alert-notifications", func() {
 			r.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))

+ 44 - 17
pkg/api/cloudwatch/cloudwatch.go

@@ -17,7 +17,6 @@ import (
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/aws/aws-sdk-go/service/sts"
-	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
@@ -90,7 +89,7 @@ type cache struct {
 var awsCredentialCache map[string]cache = make(map[string]cache)
 var credentialCacheLock sync.RWMutex
 
-func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
+func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
 	cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
 	credentialCacheLock.RLock()
 	if _, ok := awsCredentialCache[cacheKey]; ok {
@@ -98,7 +97,7 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
 			(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
 			result := awsCredentialCache[cacheKey].credential
 			credentialCacheLock.RUnlock()
-			return result
+			return result, nil
 		}
 	}
 	credentialCacheLock.RUnlock()
@@ -130,8 +129,7 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
 		svc := sts.New(session.New(stsConfig), stsConfig)
 		resp, err := svc.AssumeRole(params)
 		if err != nil {
-			// ignore
-			log.Error(3, "CloudWatch: Failed to assume role", err)
+			return nil, err
 		}
 		if resp.Credentials != nil {
 			accessKeyId = *resp.Credentials.AccessKeyId
@@ -165,19 +163,28 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
 	}
 	credentialCacheLock.Unlock()
 
-	return creds
+	return creds, nil
 }
 
-func getAwsConfig(req *cwRequest) *aws.Config {
+func getAwsConfig(req *cwRequest) (*aws.Config, error) {
+	creds, err := getCredentials(req.GetDatasourceInfo())
+	if err != nil {
+		return nil, err
+	}
+
 	cfg := &aws.Config{
 		Region:      aws.String(req.Region),
-		Credentials: getCredentials(req.GetDatasourceInfo()),
+		Credentials: creds,
 	}
-	return cfg
+	return cfg, nil
 }
 
 func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
-	cfg := getAwsConfig(req)
+	cfg, err := getAwsConfig(req)
+	if err != nil {
+		c.JsonApiErr(500, "Unable to call AWS API", err)
+		return
+	}
 	svc := cloudwatch.New(session.New(cfg), cfg)
 
 	reqParam := &struct {
@@ -220,7 +227,11 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
 }
 
 func handleListMetrics(req *cwRequest, c *middleware.Context) {
-	cfg := getAwsConfig(req)
+	cfg, err := getAwsConfig(req)
+	if err != nil {
+		c.JsonApiErr(500, "Unable to call AWS API", err)
+		return
+	}
 	svc := cloudwatch.New(session.New(cfg), cfg)
 
 	reqParam := &struct {
@@ -239,7 +250,7 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
 	}
 
 	var resp cloudwatch.ListMetricsOutput
-	err := svc.ListMetricsPages(params,
+	err = svc.ListMetricsPages(params,
 		func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
 			metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
 			metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
@@ -257,7 +268,11 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
 }
 
 func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
-	cfg := getAwsConfig(req)
+	cfg, err := getAwsConfig(req)
+	if err != nil {
+		c.JsonApiErr(500, "Unable to call AWS API", err)
+		return
+	}
 	svc := cloudwatch.New(session.New(cfg), cfg)
 
 	reqParam := &struct {
@@ -296,7 +311,11 @@ func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
 }
 
 func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
-	cfg := getAwsConfig(req)
+	cfg, err := getAwsConfig(req)
+	if err != nil {
+		c.JsonApiErr(500, "Unable to call AWS API", err)
+		return
+	}
 	svc := cloudwatch.New(session.New(cfg), cfg)
 
 	reqParam := &struct {
@@ -336,7 +355,11 @@ func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
 }
 
 func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
-	cfg := getAwsConfig(req)
+	cfg, err := getAwsConfig(req)
+	if err != nil {
+		c.JsonApiErr(500, "Unable to call AWS API", err)
+		return
+	}
 	svc := cloudwatch.New(session.New(cfg), cfg)
 
 	reqParam := &struct {
@@ -368,7 +391,11 @@ func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
 }
 
 func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
-	cfg := getAwsConfig(req)
+	cfg, err := getAwsConfig(req)
+	if err != nil {
+		c.JsonApiErr(500, "Unable to call AWS API", err)
+		return
+	}
 	svc := ec2.New(session.New(cfg), cfg)
 
 	reqParam := &struct {
@@ -388,7 +415,7 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
 	}
 
 	var resp ec2.DescribeInstancesOutput
-	err := svc.DescribeInstancesPages(params,
+	err = svc.DescribeInstancesPages(params,
 		func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
 			reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
 			for _, reservation := range reservations {

+ 9 - 5
pkg/api/cloudwatch/metrics.go

@@ -111,7 +111,7 @@ func init() {
 		"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
 		"AWS/ES":               {"ClientId", "DomainName"},
 		"AWS/Events":           {"RuleName"},
-		"AWS/Firehose":         {},
+		"AWS/Firehose":         {"DeliveryStreamName"},
 		"AWS/IoT":              {"Protocol"},
 		"AWS/Kinesis":          {"StreamName", "ShardID"},
 		"AWS/KinesisAnalytics": {"Flow", "Id", "Application"},
@@ -140,8 +140,8 @@ func init() {
 // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
 func handleGetRegions(req *cwRequest, c *middleware.Context) {
 	regions := []string{
-		"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "cn-north-1",
-		"eu-central-1", "eu-west-1", "sa-east-1", "us-east-1", "us-west-1", "us-west-2", "us-gov-west-1",
+		"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1",
+		"eu-central-1", "eu-west-1", "eu-west-2", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2",
 	}
 
 	result := []interface{}{}
@@ -248,9 +248,13 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
 }
 
 func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
+	creds, err := getCredentials(cwData)
+	if err != nil {
+		return cloudwatch.ListMetricsOutput{}, err
+	}
 	cfg := &aws.Config{
 		Region:      aws.String(cwData.Region),
-		Credentials: getCredentials(cwData),
+		Credentials: creds,
 	}
 
 	svc := cloudwatch.New(session.New(cfg), cfg)
@@ -260,7 +264,7 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
 	}
 
 	var resp cloudwatch.ListMetricsOutput
-	err := svc.ListMetricsPages(params,
+	err = svc.ListMetricsPages(params,
 		func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
 			metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
 			metrics, _ := awsutil.ValuesAtPath(page, "Metrics")

+ 33 - 0
pkg/api/dataproxy.go

@@ -1,6 +1,8 @@
 package api
 
 import (
+	"bytes"
+	"io/ioutil"
 	"net/http"
 	"net/http/httputil"
 	"net/url"
@@ -8,6 +10,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/api/cloudwatch"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
@@ -15,6 +18,10 @@ import (
 	"github.com/grafana/grafana/pkg/util"
 )
 
+var (
+	dataproxyLogger log.Logger = log.New("data-proxy-log")
+)
+
 func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
 	director := func(req *http.Request) {
 		req.URL.Scheme = targetUrl.Scheme
@@ -121,6 +128,32 @@ func ProxyDataSourceRequest(c *middleware.Context) {
 		c.JsonApiErr(400, "Unable to load TLS certificate", err)
 		return
 	}
+
+	logProxyRequest(ds.Type, c)
 	proxy.ServeHTTP(c.Resp, c.Req.Request)
 	c.Resp.Header().Del("Set-Cookie")
 }
+
+func logProxyRequest(dataSourceType string, c *middleware.Context) {
+	if !setting.DataProxyLogging {
+		return
+	}
+
+	var body string
+	if c.Req.Request.Body != nil {
+		buffer, err := ioutil.ReadAll(c.Req.Request.Body)
+		if err == nil {
+			c.Req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buffer))
+			body = string(buffer)
+		}
+	}
+
+	dataproxyLogger.Info("Proxying incoming request",
+		"userid", c.UserId,
+		"orgid", c.OrgId,
+		"username", c.Login,
+		"datasource", dataSourceType,
+		"uri", c.Req.RequestURI,
+		"method", c.Req.Request.Method,
+		"body", body)
+}

+ 2 - 2
pkg/api/datasources.go

@@ -100,7 +100,7 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
 		return
 	}
 
-	c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id})
+	c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id, "name": cmd.Result.Name})
 }
 
 func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Response {
@@ -117,7 +117,7 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp
 		return ApiError(500, "Failed to update datasource", err)
 	}
 
-	return Json(200, util.DynMap{"message": "Datasource updated"})
+	return Json(200, util.DynMap{"message": "Datasource updated", "id": cmd.Id, "name": cmd.Name})
 }
 
 func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {

+ 4 - 1
pkg/api/dtos/alerting.go

@@ -3,6 +3,7 @@ package dtos
 import (
 	"time"
 
+	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
 )
@@ -16,6 +17,7 @@ type AlertRule struct {
 	State          m.AlertStateType `json:"state"`
 	NewStateDate   time.Time        `json:"newStateDate"`
 	EvalDate       time.Time        `json:"evalDate"`
+	EvalData       *simplejson.Json `json:"evalData"`
 	ExecutionError string           `json:"executionError"`
 	DashbboardUri  string           `json:"dashboardUri"`
 }
@@ -36,6 +38,7 @@ type AlertTestCommand struct {
 
 type AlertTestResult struct {
 	Firing         bool                  `json:"firing"`
+	State          m.AlertStateType      `json:"state"`
 	ConditionEvals string                `json:"conditionEvals"`
 	TimeMs         string                `json:"timeMs"`
 	Error          string                `json:"error,omitempty"`
@@ -51,7 +54,7 @@ type AlertTestResultLog struct {
 type EvalMatch struct {
 	Tags   map[string]string `json:"tags,omitempty"`
 	Metric string            `json:"metric"`
-	Value  float64           `json:"value"`
+	Value  null.Float        `json:"value"`
 }
 
 type NotificationTestCommand struct {

+ 1 - 1
pkg/api/dtos/user.go

@@ -31,7 +31,7 @@ type AdminUpdateUserPasswordForm struct {
 }
 
 type AdminUpdateUserPermissionsForm struct {
-	IsGrafanaAdmin bool `json:"IsGrafanaAdmin"`
+	IsGrafanaAdmin bool `json:"isGrafanaAdmin" binding:"Required"`
 }
 
 type AdminUserListItem struct {

+ 1 - 0
pkg/api/frontendsettings.go

@@ -140,6 +140,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 		"allowOrgCreate":    (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
 		"authProxyEnabled":  setting.AuthProxyEnabled,
 		"ldapEnabled":       setting.LdapEnabled,
+		"alertingEnabled":   setting.AlertingEnabled,
 		"buildInfo": map[string]interface{}{
 			"version":       setting.BuildVersion,
 			"commit":        setting.BuildCommit,

+ 2 - 2
pkg/api/index.go

@@ -103,10 +103,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 		Children: dashboardChildNavs,
 	})
 
-	if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
+	if setting.AlertingEnabled && (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"},
+			{Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications"},
 		}
 
 		data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{

+ 6 - 5
pkg/api/render.go

@@ -14,11 +14,12 @@ func RenderToPng(c *middleware.Context) {
 	queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
 
 	renderOpts := &renderer.RenderOpts{
-		Path:    c.Params("*") + queryParams,
-		Width:   queryReader.Get("width", "800"),
-		Height:  queryReader.Get("height", "400"),
-		OrgId:   c.OrgId,
-		Timeout: queryReader.Get("timeout", "30"),
+		Path:     c.Params("*") + queryParams,
+		Width:    queryReader.Get("width", "800"),
+		Height:   queryReader.Get("height", "400"),
+		OrgId:    c.OrgId,
+		Timeout:  queryReader.Get("timeout", "30"),
+		Timezone: queryReader.Get("tz", ""),
 	}
 
 	pngPath, err := renderer.RenderToPng(renderOpts)

+ 30 - 6
pkg/api/user.go

@@ -13,7 +13,7 @@ func GetSignedInUser(c *middleware.Context) Response {
 	return getUserUserProfile(c.UserId)
 }
 
-// GET /api/user/:id
+// GET /api/users/:id
 func GetUserById(c *middleware.Context) Response {
 	return getUserUserProfile(c.ParamsInt64(":id"))
 }
@@ -22,12 +22,36 @@ func getUserUserProfile(userId int64) Response {
 	query := m.GetUserProfileQuery{UserId: userId}
 
 	if err := bus.Dispatch(&query); err != nil {
+		if err == m.ErrUserNotFound {
+			return ApiError(404, m.ErrUserNotFound.Error(), nil)
+		}
 		return ApiError(500, "Failed to get user", err)
 	}
 
 	return Json(200, query.Result)
 }
 
+// GET /api/users/lookup
+func GetUserByLoginOrEmail(c *middleware.Context) Response {
+	query := m.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")}
+	if err := bus.Dispatch(&query); err != nil {
+		if err == m.ErrUserNotFound {
+			return ApiError(404, m.ErrUserNotFound.Error(), nil)
+		}
+		return ApiError(500, "Failed to get user", err)
+	}
+	user := query.Result
+	result := m.UserProfileDTO{
+		Name:           user.Name,
+		Email:          user.Email,
+		Login:          user.Login,
+		Theme:          user.Theme,
+		IsGrafanaAdmin: user.IsAdmin,
+		OrgId:          user.OrgId,
+	}
+	return Json(200, &result)
+}
+
 // POST /api/user
 func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
 	if setting.AuthProxyEnabled {
@@ -60,7 +84,7 @@ func UpdateUserActiveOrg(c *middleware.Context) Response {
 	cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId}
 
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed change active organization", err)
+		return ApiError(500, "Failed to change active organization", err)
 	}
 
 	return ApiSuccess("Active organization changed")
@@ -70,12 +94,12 @@ func handleUpdateUser(cmd m.UpdateUserCommand) Response {
 	if len(cmd.Login) == 0 {
 		cmd.Login = cmd.Email
 		if len(cmd.Login) == 0 {
-			return ApiError(400, "Validation error, need specify either username or email", nil)
+			return ApiError(400, "Validation error, need to specify either username or email", nil)
 		}
 	}
 
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "failed to update user", err)
+		return ApiError(500, "Failed to update user", err)
 	}
 
 	return ApiSuccess("User updated")
@@ -95,7 +119,7 @@ func getUserOrgList(userId int64) Response {
 	query := m.GetUserOrgListQuery{UserId: userId}
 
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Faile to get user organziations", err)
+		return ApiError(500, "Failed to get user organizations", err)
 	}
 
 	return Json(200, query.Result)
@@ -130,7 +154,7 @@ func UserSetUsingOrg(c *middleware.Context) Response {
 	cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
 
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed change active organization", err)
+		return ApiError(500, "Failed to change active organization", err)
 	}
 
 	return ApiSuccess("Active organization changed")

+ 1 - 1
pkg/cmd/grafana-server/server.go

@@ -55,7 +55,7 @@ func (g *GrafanaServerImpl) Start() {
 	plugins.Init()
 
 	// init alerting
-	if setting.ExecuteAlerts {
+	if setting.AlertingEnabled && setting.ExecuteAlerts {
 		engine := alerting.NewEngine()
 		g.childRoutines.Go(func() error { return engine.Run(g.context) })
 	}

+ 13 - 10
pkg/components/imguploader/imguploader.go

@@ -2,6 +2,7 @@ package imguploader
 
 import (
 	"fmt"
+	"regexp"
 
 	"github.com/grafana/grafana/pkg/setting"
 )
@@ -30,19 +31,21 @@ func NewImageUploader() (ImageUploader, error) {
 		accessKey := s3sec.Key("access_key").MustString("")
 		secretKey := s3sec.Key("secret_key").MustString("")
 
-		if bucket == "" {
+		region := ""
+		rBucket := regexp.MustCompile(`https?:\/\/(.*)\.s3(-([^.]+))?\.amazonaws\.com\/?`)
+		matches := rBucket.FindStringSubmatch(bucket)
+		if len(matches) == 0 {
 			return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3")
+		} else {
+			bucket = matches[1]
+			if matches[3] != "" {
+				region = matches[3]
+			} else {
+				region = "us-east-1"
+			}
 		}
 
-		if accessKey == "" {
-			return nil, fmt.Errorf("Could not find accessKey setting for image.uploader.s3")
-		}
-
-		if secretKey == "" {
-			return nil, fmt.Errorf("Could not find secretKey setting for image.uploader.s3")
-		}
-
-		return NewS3Uploader(bucket, accessKey, secretKey), nil
+		return NewS3Uploader(region, bucket, "public-read", accessKey, secretKey), nil
 	case "webdav":
 		webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
 		if err != nil {

+ 3 - 2
pkg/components/imguploader/imguploader_test.go

@@ -19,7 +19,7 @@ func TestImageUploaderFactory(t *testing.T) {
 			setting.ImageUploadProvider = "s3"
 
 			s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
-			s3sec.NewKey("bucket_url", "bucket_url")
+			s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com")
 			s3sec.NewKey("access_key", "access_key")
 			s3sec.NewKey("secret_key", "secret_key")
 
@@ -29,9 +29,10 @@ func TestImageUploaderFactory(t *testing.T) {
 			original, ok := uploader.(*S3Uploader)
 
 			So(ok, ShouldBeTrue)
+			So(original.region, ShouldEqual, "us-east-2")
+			So(original.bucket, ShouldEqual, "foo.bar.baz")
 			So(original.accessKey, ShouldEqual, "access_key")
 			So(original.secretKey, ShouldEqual, "secret_key")
-			So(original.bucket, ShouldEqual, "bucket_url")
 		})
 
 		Convey("Webdav uploader", func() {

+ 38 - 32
pkg/components/imguploader/s3uploader.go

@@ -1,26 +1,33 @@
 package imguploader
 
 import (
-	"io/ioutil"
-	"net/http"
-	"net/url"
-	"path"
+	"os"
+	"time"
 
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
+	"github.com/aws/aws-sdk-go/aws/ec2metadata"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/s3"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/util"
-	"github.com/kr/s3/s3util"
 )
 
 type S3Uploader struct {
+	region    string
 	bucket    string
+	acl       string
 	secretKey string
 	accessKey string
 	log       log.Logger
 }
 
-func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
+func NewS3Uploader(region, bucket, acl, accessKey, secretKey string) *S3Uploader {
 	return &S3Uploader{
+		region:    region,
 		bucket:    bucket,
+		acl:       acl,
 		accessKey: accessKey,
 		secretKey: secretKey,
 		log:       log.New("s3uploader"),
@@ -28,42 +35,41 @@ func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
 }
 
 func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
-
-	s3util.DefaultConfig.AccessKey = u.accessKey
-	s3util.DefaultConfig.SecretKey = u.secretKey
-
-	header := make(http.Header)
-	header.Add("x-amz-acl", "public-read")
-	header.Add("Content-Type", "image/png")
-
-	var imageUrl *url.URL
-	var err error
-
-	if imageUrl, err = url.Parse(u.bucket); err != nil {
-		return "", err
+	sess := session.New()
+	creds := credentials.NewChainCredentials(
+		[]credentials.Provider{
+			&credentials.StaticProvider{Value: credentials.Value{
+				AccessKeyID:     u.accessKey,
+				SecretAccessKey: u.secretKey,
+			}},
+			&credentials.EnvProvider{},
+			&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
+		})
+	cfg := &aws.Config{
+		Region:      aws.String(u.region),
+		Credentials: creds,
 	}
 
-	// add image to url
-	imageUrl.Path = path.Join(imageUrl.Path, util.GetRandomString(20)+".png")
-	imageUrlString := imageUrl.String()
-	log.Debug("Uploading image to s3", "url", imageUrlString)
+	key := util.GetRandomString(20) + ".png"
+	log.Debug("Uploading image to s3", "bucket = ", u.bucket, ", key = ", key)
 
-	writer, err := s3util.Create(imageUrlString, header, nil)
+	file, err := os.Open(imageDiskPath)
 	if err != nil {
 		return "", err
 	}
 
-	defer writer.Close()
-
-	imgData, err := ioutil.ReadFile(imageDiskPath)
-	if err != nil {
-		return "", err
+	svc := s3.New(session.New(cfg), cfg)
+	params := &s3.PutObjectInput{
+		Bucket:      aws.String(u.bucket),
+		Key:         aws.String(key),
+		ACL:         aws.String(u.acl),
+		Body:        file,
+		ContentType: aws.String("image/png"),
 	}
-
-	_, err = writer.Write(imgData)
+	_, err = svc.PutObject(params)
 	if err != nil {
 		return "", err
 	}
 
-	return imageUrlString, nil
+	return "https://" + u.bucket + ".s3.amazonaws.com/" + key, nil
 }

+ 10 - 0
vendor/gopkg.in/guregu/null.v3/float.go → pkg/components/null/float.go

@@ -96,6 +96,16 @@ func (f Float) MarshalText() ([]byte, error) {
 	return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
 }
 
+// MarshalText implements encoding.TextMarshaler.
+// It will encode a blank string if this Float is null.
+func (f Float) String() string {
+	if !f.Valid {
+		return "null"
+	}
+
+	return fmt.Sprintf("%1.3f", f.Float64)
+}
+
 // SetValid changes this Float's value and also sets it to be non-null.
 func (f *Float) SetValid(n float64) {
 	f.Float64 = n

+ 35 - 5
pkg/components/renderer/renderer.go

@@ -11,6 +11,8 @@ import (
 
 	"strconv"
 
+	"strings"
+
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/setting"
@@ -18,15 +20,38 @@ import (
 )
 
 type RenderOpts struct {
-	Path    string
-	Width   string
-	Height  string
-	Timeout string
-	OrgId   int64
+	Path     string
+	Width    string
+	Height   string
+	Timeout  string
+	OrgId    int64
+	Timezone string
 }
 
 var rendererLog log.Logger = log.New("png-renderer")
 
+func isoTimeOffsetToPosixTz(isoOffset string) string {
+	// invert offset
+	if strings.HasPrefix(isoOffset, "UTC+") {
+		return strings.Replace(isoOffset, "UTC+", "UTC-", 1)
+	}
+	if strings.HasPrefix(isoOffset, "UTC-") {
+		return strings.Replace(isoOffset, "UTC-", "UTC+", 1)
+	}
+	return isoOffset
+}
+
+func appendEnviron(baseEnviron []string, name string, value string) []string {
+	results := make([]string, 0)
+	prefix := fmt.Sprintf("%s=", name)
+	for _, v := range baseEnviron {
+		if !strings.HasPrefix(v, prefix) {
+			results = append(results, v)
+		}
+	}
+	return append(results, fmt.Sprintf("%s=%s", name, value))
+}
+
 func RenderToPng(params *RenderOpts) (string, error) {
 	rendererLog.Info("Rendering", "path", params.Path)
 
@@ -73,6 +98,11 @@ func RenderToPng(params *RenderOpts) (string, error) {
 		return "", err
 	}
 
+	if params.Timezone != "" {
+		baseEnviron := os.Environ()
+		cmd.Env = appendEnviron(baseEnviron, "TZ", isoTimeOffsetToPosixTz(params.Timezone))
+	}
+
 	err = cmd.Start()
 	if err != nil {
 		return "", err

+ 6 - 0
pkg/metrics/metrics.go

@@ -45,8 +45,11 @@ var (
 	M_Alerting_Notification_Sent_Email     Counter
 	M_Alerting_Notification_Sent_Webhook   Counter
 	M_Alerting_Notification_Sent_PagerDuty Counter
+	M_Alerting_Notification_Sent_LINE      Counter
 	M_Alerting_Notification_Sent_Victorops Counter
 	M_Alerting_Notification_Sent_OpsGenie  Counter
+	M_Alerting_Notification_Sent_Telegram  Counter
+	M_Alerting_Notification_Sent_Sensu     Counter
 	M_Aws_CloudWatch_GetMetricStatistics   Counter
 	M_Aws_CloudWatch_ListMetrics           Counter
 
@@ -114,6 +117,9 @@ func initMetricVars(settings *MetricSettings) {
 	M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty")
 	M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
 	M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie")
+	M_Alerting_Notification_Sent_Telegram = RegCounter("alerting.notifications_sent", "type", "telegram")
+	M_Alerting_Notification_Sent_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu")
+	M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE")
 
 	M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
 	M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")

+ 0 - 1
pkg/models/alert.go

@@ -73,7 +73,6 @@ type Alert struct {
 	Frequency      int64
 
 	EvalData     *simplejson.Json
-	EvalDate     time.Time
 	NewStateDate time.Time
 	StateChanges int
 

+ 1 - 0
pkg/models/notifications.go

@@ -23,6 +23,7 @@ type SendWebhookSync struct {
 	Password   string
 	Body       string
 	HttpMethod string
+	HttpHeader map[string]string
 }
 
 type SendResetPasswordEmailCommand struct {

+ 1 - 1
pkg/services/alerting/conditions/evaluator.go

@@ -3,9 +3,9 @@ package conditions
 import (
 	"encoding/json"
 
+	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/services/alerting"
-	"gopkg.in/guregu/null.v3"
 )
 
 var (

+ 2 - 2
pkg/services/alerting/conditions/evaluator_test.go

@@ -3,10 +3,10 @@ package conditions
 import (
 	"testing"
 
-	"gopkg.in/guregu/null.v3"
+	. "github.com/smartystreets/goconvey/convey"
 
+	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/components/simplejson"
-	. "github.com/smartystreets/goconvey/convey"
 )
 
 func evalutorScenario(json string, reducedValue float64, datapoints ...float64) bool {

+ 21 - 3
pkg/services/alerting/conditions/query.go

@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/alerting"
@@ -45,18 +46,18 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
 	emptySerieCount := 0
 	evalMatchCount := 0
 	var matches []*alerting.EvalMatch
+
 	for _, series := range seriesList {
 		reducedValue := c.Reducer.Reduce(series)
 		evalMatch := c.Evaluator.Eval(reducedValue)
 
 		if reducedValue.Valid == false {
 			emptySerieCount++
-			continue
 		}
 
 		if context.IsTestRun {
 			context.Logs = append(context.Logs, &alerting.ResultLogEntry{
-				Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue.Float64),
+				Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %s", c.Index, evalMatch, series.Name, reducedValue),
 			})
 		}
 
@@ -65,9 +66,26 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
 
 			matches = append(matches, &alerting.EvalMatch{
 				Metric: series.Name,
-				Value:  reducedValue.Float64,
+				Value:  reducedValue,
+			})
+		}
+	}
+
+	// handle no series special case
+	if len(seriesList) == 0 {
+		// eval condition for null value
+		evalMatch := c.Evaluator.Eval(null.FloatFromPtr(nil))
+
+		if context.IsTestRun {
+			context.Logs = append(context.Logs, &alerting.ResultLogEntry{
+				Message: fmt.Sprintf("Condition[%d]: Eval: %v, Query Returned No Series (reduced to null/no value)", evalMatch),
 			})
 		}
+
+		if evalMatch {
+			evalMatchCount++
+			matches = append(matches, &alerting.EvalMatch{Metric: "NoData", Value: null.FloatFromPtr(nil)})
+		}
 	}
 
 	return &alerting.ConditionResult{

+ 32 - 2
pkg/services/alerting/conditions/query_test.go

@@ -4,9 +4,8 @@ import (
 	"context"
 	"testing"
 
-	null "gopkg.in/guregu/null.v3"
-
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/alerting"
@@ -72,7 +71,38 @@ func TestQueryCondition(t *testing.T) {
 				So(cr.Firing, ShouldBeTrue)
 			})
 
+			Convey("No series", func() {
+				Convey("Should set NoDataFound when condition is gt", func() {
+					ctx.series = tsdb.TimeSeriesSlice{}
+					cr, err := ctx.exec()
+
+					So(err, ShouldBeNil)
+					So(cr.Firing, ShouldBeFalse)
+					So(cr.NoDataFound, ShouldBeTrue)
+				})
+
+				Convey("Should be firing when condition is no_value", func() {
+					ctx.evaluator = `{"type": "no_value", "params": []}`
+					ctx.series = tsdb.TimeSeriesSlice{}
+					cr, err := ctx.exec()
+
+					So(err, ShouldBeNil)
+					So(cr.Firing, ShouldBeTrue)
+				})
+			})
+
 			Convey("Empty series", func() {
+				Convey("Should set Firing if eval match", func() {
+					ctx.evaluator = `{"type": "no_value", "params": []}`
+					ctx.series = tsdb.TimeSeriesSlice{
+						tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
+					}
+					cr, err := ctx.exec()
+
+					So(err, ShouldBeNil)
+					So(cr.Firing, ShouldBeTrue)
+				})
+
 				Convey("Should set NoDataFound both series are empty", func() {
 					ctx.series = tsdb.TimeSeriesSlice{
 						tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),

+ 1 - 1
pkg/services/alerting/conditions/reducer.go

@@ -5,8 +5,8 @@ import (
 
 	"sort"
 
+	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/tsdb"
-	"gopkg.in/guregu/null.v3"
 )
 
 type QueryReducer interface {

+ 12 - 2
pkg/services/alerting/conditions/reducer_test.go

@@ -3,10 +3,10 @@ package conditions
 import (
 	"testing"
 
-	"gopkg.in/guregu/null.v3"
+	. "github.com/smartystreets/goconvey/convey"
 
+	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/tsdb"
-	. "github.com/smartystreets/goconvey/convey"
 )
 
 func TestSimpleReducer(t *testing.T) {
@@ -57,6 +57,16 @@ func TestSimpleReducer(t *testing.T) {
 			So(result, ShouldEqual, float64(2))
 		})
 
+		Convey("avg with only nulls", func() {
+			reducer := NewSimpleReducer("avg")
+			series := &tsdb.TimeSeries{
+				Name: "test time serie",
+			}
+
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
+			So(reducer.Reduce(series).Valid, ShouldEqual, false)
+		})
+
 		Convey("avg of number values and null values should ignore nulls", func() {
 			reducer := NewSimpleReducer("avg")
 			series := &tsdb.TimeSeries{

+ 35 - 0
pkg/services/alerting/eval_handler.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
+	"github.com/grafana/grafana/pkg/models"
 )
 
 type DefaultEvalHandler struct {
@@ -60,6 +61,40 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 	context.Firing = firing
 	context.NoDataFound = noDataFound
 	context.EndTime = time.Now()
+	context.Rule.State = e.getNewState(context)
+
 	elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
 	metrics.M_Alerting_Execution_Time.Update(elapsedTime)
 }
+
+// This should be move into evalContext once its been refactored.
+func (handler *DefaultEvalHandler) getNewState(evalContext *EvalContext) models.AlertStateType {
+	if evalContext.Error != nil {
+		handler.log.Error("Alert Rule Result Error",
+			"ruleId", evalContext.Rule.Id,
+			"name", evalContext.Rule.Name,
+			"error", evalContext.Error,
+			"changing state to", evalContext.Rule.ExecutionErrorState.ToAlertState())
+
+		if evalContext.Rule.ExecutionErrorState == models.ExecutionErrorKeepState {
+			return evalContext.PrevAlertState
+		} else {
+			return evalContext.Rule.ExecutionErrorState.ToAlertState()
+		}
+	} else if evalContext.Firing {
+		return models.AlertStateAlerting
+	} else if evalContext.NoDataFound {
+		handler.log.Info("Alert Rule returned no data",
+			"ruleId", evalContext.Rule.Id,
+			"name", evalContext.Rule.Name,
+			"changing state to", evalContext.Rule.NoDataState.ToAlertState())
+
+		if evalContext.Rule.NoDataState == models.NoDataKeepState {
+			return evalContext.PrevAlertState
+		} else {
+			return evalContext.Rule.NoDataState.ToAlertState()
+		}
+	}
+
+	return models.AlertStateOK
+}

+ 73 - 3
pkg/services/alerting/eval_handler_test.go

@@ -2,8 +2,10 @@ package alerting
 
 import (
 	"context"
+	"fmt"
 	"testing"
 
+	"github.com/grafana/grafana/pkg/models"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
@@ -18,8 +20,8 @@ func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
 	return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator, NoDataFound: c.noData}, nil
 }
 
-func TestAlertingExecutor(t *testing.T) {
-	Convey("Test alert execution", t, func() {
+func TestAlertingEvaluationHandler(t *testing.T) {
+	Convey("Test alert evaluation handler", t, func() {
 		handler := NewEvalHandler()
 
 		Convey("Show return triggered with single passing condition", func() {
@@ -37,7 +39,7 @@ func TestAlertingExecutor(t *testing.T) {
 		Convey("Show return false with not passing asdf", func() {
 			context := NewEvalContext(context.TODO(), &Rule{
 				Conditions: []Condition{
-					&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
+					&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{{}, {}}},
 					&conditionStub{firing: false, operator: "and"},
 				},
 			})
@@ -164,5 +166,73 @@ func TestAlertingExecutor(t *testing.T) {
 			handler.Eval(context)
 			So(context.NoDataFound, ShouldBeTrue)
 		})
+
+		Convey("EvalHandler can replace alert state based for errors and no_data", func() {
+			ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
+			dummieError := fmt.Errorf("dummie error")
+			Convey("Should update alert state", func() {
+
+				Convey("ok -> alerting", func() {
+					ctx.PrevAlertState = models.AlertStateOK
+					ctx.Firing = true
+
+					So(handler.getNewState(ctx), ShouldEqual, models.AlertStateAlerting)
+				})
+
+				Convey("ok -> error(alerting)", func() {
+					ctx.PrevAlertState = models.AlertStateOK
+					ctx.Error = dummieError
+					ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
+
+					ctx.Rule.State = handler.getNewState(ctx)
+					So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
+				})
+
+				Convey("ok -> error(keep_last)", func() {
+					ctx.PrevAlertState = models.AlertStateOK
+					ctx.Error = dummieError
+					ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
+
+					ctx.Rule.State = handler.getNewState(ctx)
+					So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
+				})
+
+				Convey("pending -> error(keep_last)", func() {
+					ctx.PrevAlertState = models.AlertStatePending
+					ctx.Error = dummieError
+					ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
+
+					ctx.Rule.State = handler.getNewState(ctx)
+					So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
+				})
+
+				Convey("ok -> no_data(alerting)", func() {
+					ctx.PrevAlertState = models.AlertStateOK
+					ctx.Rule.NoDataState = models.NoDataSetAlerting
+					ctx.NoDataFound = true
+
+					ctx.Rule.State = handler.getNewState(ctx)
+					So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
+				})
+
+				Convey("ok -> no_data(keep_last)", func() {
+					ctx.PrevAlertState = models.AlertStateOK
+					ctx.Rule.NoDataState = models.NoDataKeepState
+					ctx.NoDataFound = true
+
+					ctx.Rule.State = handler.getNewState(ctx)
+					So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
+				})
+
+				Convey("pending -> no_data(keep_last)", func() {
+					ctx.PrevAlertState = models.AlertStatePending
+					ctx.Rule.NoDataState = models.NoDataKeepState
+					ctx.NoDataFound = true
+
+					ctx.Rule.State = handler.getNewState(ctx)
+					So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
+				})
+			})
+		})
 	})
 }

+ 15 - 2
pkg/services/alerting/extractor.go

@@ -60,12 +60,25 @@ func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Jso
 	return nil
 }
 
+func copyJson(in *simplejson.Json) (*simplejson.Json, error) {
+	rawJson, err := in.MarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+
+	return simplejson.NewJson(rawJson)
+}
+
 func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
 	e.log.Debug("GetAlerts")
 
-	alerts := make([]*m.Alert, 0)
+	dashboardJson, err := copyJson(e.Dash.Data)
+	if err != nil {
+		return nil, err
+	}
 
-	for _, rowObj := range e.Dash.Data.Get("rows").MustArray() {
+	alerts := make([]*m.Alert, 0)
+	for _, rowObj := range dashboardJson.Get("rows").MustArray() {
 		row := simplejson.NewFromAny(rowObj)
 
 		for _, panelObj := range row.Get("panels").MustArray() {

+ 28 - 0
pkg/services/alerting/extractor_test.go

@@ -110,6 +110,34 @@ func TestAlertRuleExtraction(t *testing.T) {
       ]
       }`
 
+		Convey("Extractor should not modify the original json", func() {
+			dashJson, err := simplejson.NewJson([]byte(json))
+			So(err, ShouldBeNil)
+
+			dash := m.NewDashboardFromJson(dashJson)
+
+			getTarget := func(j *simplejson.Json) string {
+				rowObj := j.Get("rows").MustArray()[0]
+				row := simplejson.NewFromAny(rowObj)
+				panelObj := row.Get("panels").MustArray()[0]
+				panel := simplejson.NewFromAny(panelObj)
+				conditionObj := panel.Get("alert").Get("conditions").MustArray()[0]
+				condition := simplejson.NewFromAny(conditionObj)
+				return condition.Get("query").Get("model").Get("target").MustString()
+			}
+
+			Convey("Dashboard json rows.panels.alert.query.model.target should be empty", func() {
+				So(getTarget(dashJson), ShouldEqual, "")
+			})
+
+			extractor := NewDashAlertExtractor(dash, 1)
+			_, _ = extractor.GetAlerts()
+
+			Convey("Dashboard json should not be updated after extracting rules", func() {
+				So(getTarget(dashJson), ShouldEqual, "")
+			})
+		})
+
 		Convey("Parsing and validating dashboard containing graphite alerts", func() {
 
 			dashJson, err := simplejson.NewJson([]byte(json))

+ 3 - 1
pkg/services/alerting/models.go

@@ -1,5 +1,7 @@
 package alerting
 
+import "github.com/grafana/grafana/pkg/components/null"
+
 type Job struct {
 	Offset     int64
 	OffsetWait bool
@@ -14,7 +16,7 @@ type ResultLogEntry struct {
 }
 
 type EvalMatch struct {
-	Value  float64           `json:"value"`
+	Value  null.Float        `json:"value"`
 	Metric string            `json:"metric"`
 	Tags   map[string]string `json:"tags"`
 }

+ 23 - 5
pkg/services/alerting/notifier.go

@@ -13,6 +13,14 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 )
 
+type NotifierPlugin struct {
+	Type            string          `json:"type"`
+	Name            string          `json:"name"`
+	Description     string          `json:"description"`
+	OptionsTemplate string          `json:"optionsTemplate"`
+	Factory         NotifierFactory `json:"-"`
+}
+
 type RootNotifier struct {
 	log log.Logger
 }
@@ -130,12 +138,12 @@ func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, contex
 }
 
 func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
-	factory, found := notifierFactories[model.Type]
+	notifierPlugin, found := notifierFactories[model.Type]
 	if !found {
 		return nil, errors.New("Unsupported notification type")
 	}
 
-	return factory(model)
+	return notifierPlugin.Factory(model)
 }
 
 func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
@@ -152,8 +160,18 @@ func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
 
 type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
 
-var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory)
+var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)
+
+func RegisterNotifier(plugin *NotifierPlugin) {
+	notifierFactories[plugin.Type] = plugin
+}
+
+func GetNotifiers() []*NotifierPlugin {
+	list := make([]*NotifierPlugin, 0)
+
+	for _, value := range notifierFactories {
+		list = append(list, value)
+	}
 
-func RegisterNotifier(typeName string, factory NotifierFactory) {
-	notifierFactories[typeName] = factory
+	return list
 }

+ 15 - 1
pkg/services/alerting/notifiers/email.go

@@ -13,7 +13,21 @@ import (
 )
 
 func init() {
-	alerting.RegisterNotifier("email", NewEmailNotifier)
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:        "email",
+		Name:        "Email",
+		Description: "Sends notifications using Grafana server configured STMP settings",
+		Factory:     NewEmailNotifier,
+		OptionsTemplate: `
+      <h3 class="page-heading">Email addresses</h3>
+      <div class="gf-form">
+         <textarea rows="7" class="gf-form-input width-25" required ng-model="ctrl.model.settings.addresses"></textarea>
+      </div>
+      <div class="gf-form">
+      <span>You can enter multiple email addresses using a ";" separator</span>
+      </div>
+    `,
+	})
 }
 
 type EmailNotifier struct {

+ 94 - 0
pkg/services/alerting/notifiers/line.go

@@ -0,0 +1,94 @@
+package notifiers
+
+import (
+	"fmt"
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/alerting"
+	"net/url"
+)
+
+func init() {
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:        "LINE",
+		Name:        "LINE",
+		Description: "Send notifications to LINE notify",
+		Factory:     NewLINENotifier,
+		OptionsTemplate: `
+    <div class="gf-form-group">
+      <h3 class="page-heading">LINE notify settings</h3>
+      <div class="gf-form">
+        <span class="gf-form-label width-14">Token</span>
+        <input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.token" placeholder="LINE notify token key"></input>
+      </div>
+    </div>
+`,
+	})
+}
+
+const (
+	lineNotifyUrl string = "https://notify-api.line.me/api/notify"
+)
+
+func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+	token := model.Settings.Get("token").MustString()
+	if token == "" {
+		return nil, alerting.ValidationError{Reason: "Could not find token in settings"}
+	}
+
+	return &LineNotifier{
+		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+		Token:        token,
+		log:          log.New("alerting.notifier.line"),
+	}, nil
+}
+
+type LineNotifier struct {
+	NotifierBase
+	Token string
+	log   log.Logger
+}
+
+func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error {
+	this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+	metrics.M_Alerting_Notification_Sent_LINE.Inc(1)
+
+	var err error
+	switch evalContext.Rule.State {
+	case m.AlertStateAlerting:
+		err = this.createAlert(evalContext)
+	}
+	return err
+}
+
+func (this *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
+	this.log.Info("Creating Line notify", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+	ruleUrl, err := evalContext.GetRuleUrl()
+	if err != nil {
+		this.log.Error("Failed get rule link", "error", err)
+		return err
+	}
+
+	form := url.Values{}
+	body := fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message)
+	form.Add("message", body)
+
+	cmd := &m.SendWebhookSync{
+		Url:        lineNotifyUrl,
+		HttpMethod: "POST",
+		HttpHeader: map[string]string{
+			"Authorization": fmt.Sprintf("Bearer %s", this.Token),
+			"Content-Type":  "application/x-www-form-urlencoded",
+		},
+		Body: form.Encode(),
+	}
+
+	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+		this.log.Error("Failed to send notification to LINE", "error", err, "body", string(body))
+		return err
+	}
+
+	return nil
+}

+ 49 - 0
pkg/services/alerting/notifiers/line_test.go

@@ -0,0 +1,49 @@
+package notifiers
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	m "github.com/grafana/grafana/pkg/models"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestLineNotifier(t *testing.T) {
+	Convey("Line notifier tests", t, func() {
+		Convey("empty settings should return error", func() {
+			json := `{ }`
+
+			settingsJSON, _ := simplejson.NewJson([]byte(json))
+			model := &m.AlertNotification{
+				Name:     "line_testing",
+				Type:     "line",
+				Settings: settingsJSON,
+			}
+
+			_, err := NewLINENotifier(model)
+			So(err, ShouldNotBeNil)
+
+		})
+		Convey("settings should trigger incident", func() {
+			json := `
+			{
+  "token": "abcdefgh0123456789"
+			}`
+			settingsJSON, _ := simplejson.NewJson([]byte(json))
+			model := &m.AlertNotification{
+				Name:     "line_testing",
+				Type:     "line",
+				Settings: settingsJSON,
+			}
+
+			not, err := NewLINENotifier(model)
+			lineNotifier := not.(*LineNotifier)
+
+			So(err, ShouldBeNil)
+			So(lineNotifier.Name, ShouldEqual, "line_testing")
+			So(lineNotifier.Type, ShouldEqual, "line")
+			So(lineNotifier.Token, ShouldEqual, "abcdefgh0123456789")
+		})
+
+	})
+}

+ 22 - 1
pkg/services/alerting/notifiers/opsgenie.go

@@ -13,7 +13,28 @@ import (
 )
 
 func init() {
-	alerting.RegisterNotifier("opsgenie", NewOpsGenieNotifier)
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:        "opsgenie",
+		Name:        "OpsGenie",
+		Description: "Sends notifications to OpsGenie",
+		Factory:     NewOpsGenieNotifier,
+		OptionsTemplate: `
+      <h3 class="page-heading">OpsGenie settings</h3>
+      <div class="gf-form">
+        <span class="gf-form-label width-14">API Key</span>
+        <input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.apiKey" placeholder="OpsGenie API Key"></input>
+      </div>
+      <div class="gf-form">
+        <gf-form-switch
+           class="gf-form"
+           label="Auto close incidents"
+           label-class="width-14"
+           checked="ctrl.model.settings.autoClose"
+           tooltip="Automatically close alerts in OpseGenie once the alert goes back to ok.">
+        </gf-form-switch>
+      </div>
+    `,
+	})
 }
 
 var (

+ 22 - 1
pkg/services/alerting/notifiers/pagerduty.go

@@ -12,7 +12,28 @@ import (
 )
 
 func init() {
-	alerting.RegisterNotifier("pagerduty", NewPagerdutyNotifier)
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:        "pagerduty",
+		Name:        "PagerDuty",
+		Description: "Sends notifications to PagerDuty",
+		Factory:     NewPagerdutyNotifier,
+		OptionsTemplate: `
+      <h3 class="page-heading">PagerDuty settings</h3>
+      <div class="gf-form">
+        <span class="gf-form-label width-14">Integration Key</span>
+        <input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.integrationKey" placeholder="Pagerduty integeration Key"></input>
+      </div>
+      <div class="gf-form">
+        <gf-form-switch
+           class="gf-form"
+           label="Auto resolve incidents"
+           label-class="width-14"
+           checked="ctrl.model.settings.autoResolve"
+           tooltip="Resolve incidents in pagerduty once the alert goes back to ok.">
+        </gf-form-switch>
+      </div>
+    `,
+	})
 }
 
 var (

+ 115 - 0
pkg/services/alerting/notifiers/sensu.go

@@ -0,0 +1,115 @@
+package notifiers
+
+import (
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/alerting"
+	"strconv"
+	"strings"
+)
+
+func init() {
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:        "sensu",
+		Name:        "Sensu",
+		Description: "Sends HTTP POST request to a Sensu API",
+		Factory:     NewSensuNotifier,
+		OptionsTemplate: `
+      <h3 class="page-heading">Sensu settings</h3>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">Url</span>
+				<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url" placeholder="http://sensu-api.local:4567/results"></input>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">Username</span>
+        <input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.username"></input>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">Password</span>
+        <input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.password"></input>
+      </div>
+    `,
+	})
+
+}
+
+func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+	url := model.Settings.Get("url").MustString()
+	if url == "" {
+		return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
+	}
+
+	return &SensuNotifier{
+		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+		Url:          url,
+		User:         model.Settings.Get("username").MustString(),
+		Password:     model.Settings.Get("password").MustString(),
+		log:          log.New("alerting.notifier.sensu"),
+	}, nil
+}
+
+type SensuNotifier struct {
+	NotifierBase
+	Url      string
+	User     string
+	Password string
+	log      log.Logger
+}
+
+func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
+	this.log.Info("Sending sensu result")
+	metrics.M_Alerting_Notification_Sent_Sensu.Inc(1)
+
+	bodyJSON := simplejson.New()
+	bodyJSON.Set("ruleId", evalContext.Rule.Id)
+	// Sensu alerts cannot have spaces in them
+	bodyJSON.Set("name", strings.Replace(evalContext.Rule.Name, " ", "_", -1))
+	// Sensu alerts require a command
+	// We set it to the grafana ruleID
+	bodyJSON.Set("source", "grafana_rule_"+strconv.FormatInt(evalContext.Rule.Id, 10))
+	// Finally, sensu expects an output
+	// We set it to a default output
+	bodyJSON.Set("output", "Grafana Metric Condition Met")
+	bodyJSON.Set("evalMatches", evalContext.EvalMatches)
+
+	if evalContext.Rule.State == "alerting" {
+		bodyJSON.Set("status", 2)
+	} else if evalContext.Rule.State == "no_data" {
+		bodyJSON.Set("status", 1)
+	} else {
+		bodyJSON.Set("status", 0)
+	}
+
+	ruleUrl, err := evalContext.GetRuleUrl()
+	if err == nil {
+		bodyJSON.Set("ruleUrl", ruleUrl)
+	}
+
+	if evalContext.ImagePublicUrl != "" {
+		bodyJSON.Set("imageUrl", evalContext.ImagePublicUrl)
+	}
+
+	if evalContext.Rule.Message != "" {
+		bodyJSON.Set("message", evalContext.Rule.Message)
+	}
+
+	body, _ := bodyJSON.MarshalJSON()
+
+	cmd := &m.SendWebhookSync{
+		Url:        this.Url,
+		User:       this.User,
+		Password:   this.Password,
+		Body:       string(body),
+		HttpMethod: "POST",
+	}
+
+	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+		this.log.Error("Failed to send sensu event", "error", err, "sensu", this.Name)
+		return err
+	}
+
+	return nil
+}

+ 52 - 0
pkg/services/alerting/notifiers/sensu_test.go

@@ -0,0 +1,52 @@
+package notifiers
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	m "github.com/grafana/grafana/pkg/models"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestSensuNotifier(t *testing.T) {
+	Convey("Sensu notifier tests", t, func() {
+
+		Convey("Parsing alert notification from settings", func() {
+			Convey("empty settings should return error", func() {
+				json := `{ }`
+
+				settingsJSON, _ := simplejson.NewJson([]byte(json))
+				model := &m.AlertNotification{
+					Name:     "sensu",
+					Type:     "sensu",
+					Settings: settingsJSON,
+				}
+
+				_, err := NewSensuNotifier(model)
+				So(err, ShouldNotBeNil)
+			})
+
+			Convey("from settings", func() {
+				json := `
+				{
+					"url": "http://sensu-api.example.com:4567/results"
+				}`
+
+				settingsJSON, _ := simplejson.NewJson([]byte(json))
+				model := &m.AlertNotification{
+					Name:     "sensu",
+					Type:     "sensu",
+					Settings: settingsJSON,
+				}
+
+				not, err := NewSensuNotifier(model)
+				sensuNotifier := not.(*SensuNotifier)
+
+				So(err, ShouldBeNil)
+				So(sensuNotifier.Name, ShouldEqual, "sensu")
+				So(sensuNotifier.Type, ShouldEqual, "sensu")
+				So(sensuNotifier.Url, ShouldEqual, "http://sensu-api.example.com:4567/results")
+			})
+		})
+	})
+}

+ 36 - 1
pkg/services/alerting/notifiers/slack.go

@@ -13,7 +13,42 @@ import (
 )
 
 func init() {
-	alerting.RegisterNotifier("slack", NewSlackNotifier)
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:        "slack",
+		Name:        "Slack",
+		Description: "Sends notifications using Grafana server configured STMP settings",
+		Factory:     NewSlackNotifier,
+		OptionsTemplate: `
+      <h3 class="page-heading">Slack settings</h3>
+      <div class="gf-form max-width-30">
+        <span class="gf-form-label width-6">Url</span>
+        <input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Slack incoming webhook url"></input>
+      </div>
+      <div class="gf-form max-width-30">
+        <span class="gf-form-label width-6">Recipient</span>
+        <input type="text"
+          class="gf-form-input max-width-30"
+          ng-model="ctrl.model.settings.recipient"
+          data-placement="right">
+        </input>
+        <info-popover mode="right-absolute">
+          Override default channel or user, use #channel-name or @username
+        </info-popover>
+      </div>
+      <div class="gf-form max-width-30">
+        <span class="gf-form-label width-6">Mention</span>
+        <input type="text"
+          class="gf-form-input max-width-30"
+          ng-model="ctrl.model.settings.mention"
+          data-placement="right">
+        </input>
+        <info-popover mode="right-absolute">
+          Mention a user or a group using @ when notifying in a channel
+        </info-popover>
+      </div>
+    `,
+	})
+
 }
 
 func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {

+ 113 - 0
pkg/services/alerting/notifiers/telegram.go

@@ -0,0 +1,113 @@
+package notifiers
+
+import (
+	"fmt"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/metrics"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/alerting"
+)
+
+var (
+	telegeramApiUrl string = "https://api.telegram.org/bot%s/%s"
+)
+
+func init() {
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:        "telegram",
+		Name:        "Telegram",
+		Description: "Sends notifications to Telegram",
+		Factory:     NewTelegramNotifier,
+		OptionsTemplate: `
+      <h3 class="page-heading">Telegram API settings</h3>
+      <div class="gf-form">
+        <span class="gf-form-label width-9">BOT API Token</span>
+        <input type="text" required
+					class="gf-form-input"
+					ng-model="ctrl.model.settings.bottoken"
+					placeholder="Telegram BOT API Token"></input>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-9">Chat ID</span>
+        <input type="text" required
+					class="gf-form-input"
+					ng-model="ctrl.model.settings.chatid"
+					data-placement="right">
+        </input>
+        <info-popover mode="right-absolute">
+					Integer Telegram Chat Identifier
+        </info-popover>
+      </div>
+    `,
+	})
+
+}
+
+type TelegramNotifier struct {
+	NotifierBase
+	BotToken string
+	ChatID   string
+	log      log.Logger
+}
+
+func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+	if model.Settings == nil {
+		return nil, alerting.ValidationError{Reason: "No Settings Supplied"}
+	}
+
+	botToken := model.Settings.Get("bottoken").MustString()
+	chatId := model.Settings.Get("chatid").MustString()
+
+	if botToken == "" {
+		return nil, alerting.ValidationError{Reason: "Could not find Bot Token in settings"}
+	}
+
+	if chatId == "" {
+		return nil, alerting.ValidationError{Reason: "Could not find Chat Id in settings"}
+	}
+
+	return &TelegramNotifier{
+		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+		BotToken:     botToken,
+		ChatID:       chatId,
+		log:          log.New("alerting.notifier.telegram"),
+	}, nil
+}
+
+func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
+	this.log.Info("Sending alert notification to", "bot_token", this.BotToken)
+	this.log.Info("Sending alert notification to", "chat_id", this.ChatID)
+	metrics.M_Alerting_Notification_Sent_Telegram.Inc(1)
+
+	bodyJSON := simplejson.New()
+
+	bodyJSON.Set("chat_id", this.ChatID)
+	bodyJSON.Set("parse_mode", "html")
+
+	message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
+
+	ruleUrl, err := evalContext.GetRuleUrl()
+	if err == nil {
+		message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
+	}
+	bodyJSON.Set("text", message)
+
+	url := fmt.Sprintf(telegeramApiUrl, this.BotToken, "sendMessage")
+	body, _ := bodyJSON.MarshalJSON()
+
+	cmd := &m.SendWebhookSync{
+		Url:        url,
+		Body:       string(body),
+		HttpMethod: "POST",
+	}
+
+	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+		this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name)
+		return err
+	}
+
+	return nil
+}

+ 55 - 0
pkg/services/alerting/notifiers/telegram_test.go

@@ -0,0 +1,55 @@
+package notifiers
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	m "github.com/grafana/grafana/pkg/models"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestTelegramNotifier(t *testing.T) {
+	Convey("Telegram notifier tests", t, func() {
+
+		Convey("Parsing alert notification from settings", func() {
+			Convey("empty settings should return error", func() {
+				json := `{ }`
+
+				settingsJSON, _ := simplejson.NewJson([]byte(json))
+				model := &m.AlertNotification{
+					Name:     "telegram_testing",
+					Type:     "telegram",
+					Settings: settingsJSON,
+				}
+
+				_, err := NewTelegramNotifier(model)
+				So(err, ShouldNotBeNil)
+			})
+
+			Convey("settings should trigger incident", func() {
+				json := `
+				{
+          "bottoken": "abcdefgh0123456789",
+					"chatid": "-1234567890"
+				}`
+
+				settingsJSON, _ := simplejson.NewJson([]byte(json))
+				model := &m.AlertNotification{
+					Name:     "telegram_testing",
+					Type:     "telegram",
+					Settings: settingsJSON,
+				}
+
+				not, err := NewTelegramNotifier(model)
+				telegramNotifier := not.(*TelegramNotifier)
+
+				So(err, ShouldBeNil)
+				So(telegramNotifier.Name, ShouldEqual, "telegram_testing")
+				So(telegramNotifier.Type, ShouldEqual, "telegram")
+				So(telegramNotifier.BotToken, ShouldEqual, "abcdefgh0123456789")
+				So(telegramNotifier.ChatID, ShouldEqual, "-1234567890")
+			})
+
+		})
+	})
+}

+ 13 - 1
pkg/services/alerting/notifiers/victorops.go

@@ -16,7 +16,19 @@ import (
 const AlertStateCritical = "CRITICAL"
 
 func init() {
-	alerting.RegisterNotifier("victorops", NewVictoropsNotifier)
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:        "victorops",
+		Name:        "VictorOps",
+		Description: "Sends notifications to VictorOps",
+		Factory:     NewVictoropsNotifier,
+		OptionsTemplate: `
+      <h3 class="page-heading">VictorOps settings</h3>
+      <div class="gf-form">
+        <span class="gf-form-label width-6">Url</span>
+        <input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="VictorOps url"></input>
+      </div>
+    `,
+	})
 }
 
 // NewVictoropsNotifier creates an instance of VictoropsNotifier that

+ 30 - 2
pkg/services/alerting/notifiers/webhook.go

@@ -10,7 +10,35 @@ import (
 )
 
 func init() {
-	alerting.RegisterNotifier("webhook", NewWebHookNotifier)
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:        "webhook",
+		Name:        "webhook",
+		Description: "Sends HTTP POST request to a URL",
+		Factory:     NewWebHookNotifier,
+		OptionsTemplate: `
+      <h3 class="page-heading">Webhook settings</h3>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">Url</span>
+        <input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">Http Method</span>
+        <div class="gf-form-select-wrapper width-14">
+          <select class="gf-form-input" ng-model="ctrl.model.settings.httpMethod" ng-options="t for t in ['POST', 'PUT']">
+          </select>
+        </div>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">Username</span>
+        <input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.username"></input>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">Password</span>
+        <input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.password"></input>
+      </div>
+    `,
+	})
+
 }
 
 func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
@@ -22,7 +50,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 	return &WebhookNotifier{
 		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
 		Url:          url,
-		User:         model.Settings.Get("user").MustString(),
+		User:         model.Settings.Get("username").MustString(),
 		Password:     model.Settings.Get("password").MustString(),
 		HttpMethod:   model.Settings.Get("httpMethod").MustString("POST"),
 		log:          log.New("alerting.notifier.webhook"),

+ 5 - 34
pkg/services/alerting/result_handler.go

@@ -27,50 +27,21 @@ func NewResultHandler() *DefaultResultHandler {
 	}
 }
 
-func (handler *DefaultResultHandler) GetStateFromEvaluation(evalContext *EvalContext) m.AlertStateType {
-	if evalContext.Error != nil {
-		handler.log.Error("Alert Rule Result Error",
-			"ruleId", evalContext.Rule.Id,
-			"name", evalContext.Rule.Name,
-			"error", evalContext.Error,
-			"changing state to", evalContext.Rule.ExecutionErrorState.ToAlertState())
-
-		if evalContext.Rule.ExecutionErrorState == m.ExecutionErrorKeepState {
-			return evalContext.PrevAlertState
-		} else {
-			return evalContext.Rule.ExecutionErrorState.ToAlertState()
-		}
-	} else if evalContext.Firing {
-		return m.AlertStateAlerting
-	} else if evalContext.NoDataFound {
-		handler.log.Info("Alert Rule returned no data",
-			"ruleId", evalContext.Rule.Id,
-			"name", evalContext.Rule.Name,
-			"changing state to", evalContext.Rule.NoDataState.ToAlertState())
-
-		if evalContext.Rule.NoDataState == m.NoDataKeepState {
-			return evalContext.PrevAlertState
-		} else {
-			return evalContext.Rule.NoDataState.ToAlertState()
-		}
-	}
-
-	return m.AlertStateOK
-}
-
 func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
 	executionError := ""
 	annotationData := simplejson.New()
 
-	evalContext.Rule.State = handler.GetStateFromEvaluation(evalContext)
+	if evalContext.Firing {
+		annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
+	}
 
 	if evalContext.Error != nil {
 		executionError = evalContext.Error.Error()
 		annotationData.Set("errorMessage", executionError)
 	}
 
-	if evalContext.Firing {
-		annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
+	if evalContext.NoDataFound {
+		annotationData.Set("no_data", true)
 	}
 
 	countStateResult(evalContext.Rule.State)

+ 0 - 90
pkg/services/alerting/result_handler_test.go

@@ -1,90 +0,0 @@
-package alerting
-
-import (
-	"context"
-	"testing"
-
-	"fmt"
-
-	"github.com/grafana/grafana/pkg/models"
-	. "github.com/smartystreets/goconvey/convey"
-)
-
-func TestAlertingResultHandler(t *testing.T) {
-	Convey("Result handler", t, func() {
-		ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
-		dummieError := fmt.Errorf("dummie")
-		handler := NewResultHandler()
-
-		Convey("Should update alert state", func() {
-
-			Convey("ok -> alerting", func() {
-				ctx.PrevAlertState = models.AlertStateOK
-				ctx.Firing = true
-
-				So(handler.GetStateFromEvaluation(ctx), ShouldEqual, models.AlertStateAlerting)
-				So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
-			})
-
-			Convey("ok -> error(alerting)", func() {
-				ctx.PrevAlertState = models.AlertStateOK
-				ctx.Error = dummieError
-				ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
-
-				ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
-				So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
-				So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
-			})
-
-			Convey("ok -> error(keep_last)", func() {
-				ctx.PrevAlertState = models.AlertStateOK
-				ctx.Error = dummieError
-				ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
-
-				ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
-				So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
-				So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
-			})
-
-			Convey("pending -> error(keep_last)", func() {
-				ctx.PrevAlertState = models.AlertStatePending
-				ctx.Error = dummieError
-				ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
-
-				ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
-				So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
-				So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
-			})
-
-			Convey("ok -> no_data(alerting)", func() {
-				ctx.PrevAlertState = models.AlertStateOK
-				ctx.Rule.NoDataState = models.NoDataSetAlerting
-				ctx.NoDataFound = true
-
-				ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
-				So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
-				So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
-			})
-
-			Convey("ok -> no_data(keep_last)", func() {
-				ctx.PrevAlertState = models.AlertStateOK
-				ctx.Rule.NoDataState = models.NoDataKeepState
-				ctx.NoDataFound = true
-
-				ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
-				So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
-				So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
-			})
-
-			Convey("pending -> no_data(keep_last)", func() {
-				ctx.PrevAlertState = models.AlertStatePending
-				ctx.Rule.NoDataState = models.NoDataKeepState
-				ctx.NoDataFound = true
-
-				ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
-				So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
-				So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
-			})
-		})
-	})
-}

+ 3 - 2
pkg/services/alerting/test_notification.go

@@ -4,6 +4,7 @@ import (
 	"context"
 
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
@@ -63,12 +64,12 @@ func evalMatchesBasedOnState() []*EvalMatch {
 	matches := make([]*EvalMatch, 0)
 	matches = append(matches, &EvalMatch{
 		Metric: "High value",
-		Value:  100,
+		Value:  null.FloatFrom(100),
 	})
 
 	matches = append(matches, &EvalMatch{
 		Metric: "Higher Value",
-		Value:  200,
+		Value:  null.FloatFrom(200),
 	})
 
 	return matches

+ 1 - 0
pkg/services/notifications/mailer.go

@@ -101,6 +101,7 @@ func createDialer() (*gomail.Dialer, error) {
 
 	d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
 	d.TLSConfig = tlsconfig
+	d.LocalName = setting.InstanceName
 	return d, nil
 }
 

+ 1 - 0
pkg/services/notifications/notifications.go

@@ -65,6 +65,7 @@ func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
 		Password:   cmd.Password,
 		Body:       cmd.Body,
 		HttpMethod: cmd.HttpMethod,
+		HttpHeader: cmd.HttpHeader,
 	})
 }
 

+ 5 - 0
pkg/services/notifications/webhook.go

@@ -19,6 +19,7 @@ type Webhook struct {
 	Password   string
 	Body       string
 	HttpMethod string
+	HttpHeader map[string]string
 }
 
 var (
@@ -63,6 +64,10 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
 		request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
 	}
 
+	for k, v := range webhook.HttpHeader {
+		request.Header.Set(k, v)
+	}
+
 	resp, err := ctxhttp.Do(ctx, http.DefaultClient, request)
 	if err != nil {
 		return err

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác