Browse Source

Merge branch 'master' of https://github.com/grafana/grafana

Leandro Piccilli 9 năm trước cách đây
mục cha
commit
0000065053
100 tập tin đã thay đổi với 1885 bổ sung529 xóa
  1. 1 1
      .github/CONTRIBUTING.md
  2. 9 0
      CHANGELOG.md
  3. 4 1
      Gruntfile.js
  4. 1 1
      README.md
  5. 38 6
      build.go
  6. 37 21
      conf/defaults.ini
  7. 20 1
      conf/sample.ini
  8. 1 5
      docs/sources/datasources/influxdb.md
  9. 4 6
      docs/sources/http_api/admin.md
  10. 41 0
      docs/sources/installation/configuration.md
  11. 19 0
      pkg/api/alerting.go
  12. 5 2
      pkg/api/api.go
  13. 4 7
      pkg/api/dtos/models.go
  14. 23 0
      pkg/api/dtos/playlist.go
  15. 1 1
      pkg/api/frontendsettings.go
  16. 14 3
      pkg/api/index.go
  17. 7 5
      pkg/api/login.go
  18. 8 8
      pkg/api/login_oauth.go
  19. 39 24
      pkg/api/metrics.go
  20. 19 10
      pkg/api/playlist_play.go
  21. 5 19
      pkg/api/render.go
  22. 0 2
      pkg/cmd/grafana-cli/services/services.go
  23. 14 39
      pkg/cmd/grafana-server/main.go
  24. 128 0
      pkg/cmd/grafana-server/server.go
  25. 5 4
      pkg/cmd/grafana-server/web.go
  26. 24 9
      pkg/components/renderer/renderer.go
  27. 32 4
      pkg/log/log.go
  28. 4 4
      pkg/metrics/gauge.go
  29. 2 0
      pkg/metrics/graphite.go
  30. 12 0
      pkg/metrics/metrics.go
  31. 22 0
      pkg/metrics/publish.go
  32. 3 25
      pkg/middleware/middleware.go
  33. 55 0
      pkg/middleware/render_auth.go
  34. 0 1
      pkg/middleware/session.go
  35. 15 0
      pkg/models/alert.go
  36. 3 0
      pkg/models/dashboard_snapshot.go
  37. 1 0
      pkg/models/models.go
  38. 0 11
      pkg/models/playlist.go
  39. 10 0
      pkg/models/server.go
  40. 4 4
      pkg/models/stats.go
  41. 10 9
      pkg/models/user.go
  42. 1 0
      pkg/plugins/datasource_plugin.go
  43. 6 1
      pkg/plugins/frontend_plugin.go
  44. 26 9
      pkg/plugins/update_checker.go
  45. 14 11
      pkg/services/alerting/conditions/evaluator.go
  46. 4 2
      pkg/services/alerting/conditions/evaluator_test.go
  47. 44 12
      pkg/services/alerting/conditions/query.go
  48. 14 19
      pkg/services/alerting/conditions/query_test.go
  49. 16 15
      pkg/services/alerting/conditions/reducer.go
  50. 11 14
      pkg/services/alerting/conditions/reducer_test.go
  51. 64 21
      pkg/services/alerting/engine.go
  52. 2 11
      pkg/services/alerting/eval_context.go
  53. 1 1
      pkg/services/alerting/eval_handler.go
  54. 2 2
      pkg/services/alerting/extractor.go
  55. 0 2
      pkg/services/alerting/extractor_test.go
  56. 0 20
      pkg/services/alerting/init/init.go
  57. 13 9
      pkg/services/alerting/notifier.go
  58. 2 3
      pkg/services/alerting/notifiers/webhook.go
  59. 85 0
      pkg/services/cleanup/cleanup.go
  60. 2 1
      pkg/services/notifications/mailer.go
  61. 1 1
      pkg/services/notifications/webhook.go
  62. 18 1
      pkg/services/sqlstore/alert.go
  63. 2 1
      pkg/services/sqlstore/alert_notification.go
  64. 1 1
      pkg/services/sqlstore/annotation.go
  65. 20 0
      pkg/services/sqlstore/dashboard_snapshot.go
  66. 5 0
      pkg/services/sqlstore/migrations/dashboard_mig.go
  67. 1 0
      pkg/services/sqlstore/migrator/dialect.go
  68. 27 26
      pkg/services/sqlstore/migrator/migrator.go
  69. 4 0
      pkg/services/sqlstore/migrator/mysql_dialect.go
  70. 4 0
      pkg/services/sqlstore/migrator/postgres_dialect.go
  71. 7 0
      pkg/services/sqlstore/migrator/sqlite_dialect.go
  72. 5 1
      pkg/services/sqlstore/user.go
  73. 13 6
      pkg/setting/setting.go
  74. 2 3
      pkg/setting/setting_oauth.go
  75. 114 0
      pkg/social/grafananet_oauth.go
  76. 27 13
      pkg/social/social.go
  77. 1 1
      pkg/tsdb/batch.go
  78. 7 6
      pkg/tsdb/graphite/graphite.go
  79. 0 22
      pkg/tsdb/graphite/graphite_test.go
  80. 4 2
      pkg/tsdb/graphite/types.go
  81. 48 14
      pkg/tsdb/models.go
  82. 161 0
      pkg/tsdb/prometheus/prometheus.go
  83. 26 0
      pkg/tsdb/prometheus/prometheus_test.go
  84. 11 0
      pkg/tsdb/prometheus/types.go
  85. 0 12
      pkg/tsdb/query.go
  86. 2 2
      pkg/tsdb/query_context.go
  87. 130 0
      pkg/tsdb/testdata/scenarios.go
  88. 39 0
      pkg/tsdb/testdata/testdata.go
  89. 90 0
      pkg/tsdb/time_range.go
  90. 95 0
      pkg/tsdb/time_range_test.go
  91. 15 15
      pkg/tsdb/tsdb_test.go
  92. 14 9
      public/app/core/controllers/login_ctrl.js
  93. 2 0
      public/app/core/core.ts
  94. 16 6
      public/app/core/directives/metric_segment.js
  95. 1 1
      public/app/core/directives/value_select_dropdown.js
  96. 4 0
      public/app/core/services/backend_srv.ts
  97. 1 0
      public/app/core/services/context_srv.ts
  98. 1 0
      public/app/core/services/segment_srv.js
  99. 3 0
      public/app/core/time_series2.ts
  100. 12 0
      public/app/core/utils/colors.ts

+ 1 - 1
.github/CONTRIBUTING.md

@@ -12,7 +12,7 @@ grunt karma:dev
 
 ### Run tests for backend assets before commit
 ```
-test -z "$(gofmt -s -l . | grep -v vendor/src/ | tee /dev/stderr)"
+test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)' | tee /dev/stderr)"
 ```
 
 ### Run tests for frontend assets before commit

+ 9 - 0
CHANGELOG.md

@@ -12,6 +12,10 @@
 * **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613)
 * **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827)
 * **OAuth**: Add support for generic oauth, closes [#4718](https://github.com/grafana/grafana/pull/4718)
+* **Cloudwatch**: Add support to expand multi select template variable, closes [#5003](https://github.com/grafana/grafana/pull/5003)
+* **Graph Panel**: Now supports flexible lower/upper bounds on Y-Max and Y-Min, PR [#5720](https://github.com/grafana/grafana/pull/5720)
+* **Background Tasks**: Now support automatic purging of old snapshots, closes [#4087](https://github.com/grafana/grafana/issues/4087)
+* **Background Tasks**: Now support automatic purging of old rendered images, closes [#2172](https://github.com/grafana/grafana/issues/2172)
 
 ### Breaking changes
 * **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971)
@@ -19,6 +23,11 @@
 
 ### Bugfixes
 * **Table Panel**: Fixed problem when switching to Mixed datasource in metrics tab, fixes [#5999](https://github.com/grafana/grafana/pull/5999)
+* **Playlist**: Fixed problem with play order not matching order defined in playlist, fixes [#5467](https://github.com/grafana/grafana/pull/5467)
+* **Graph panel**: Fixed problem with auto decimals on y axis when datamin=datamax, fixes [#6070](https://github.com/grafana/grafana/pull/6070)
+* **Snapshot**: Can view embedded panels/png rendered panels in snapshots without login, fixes [#3769](https://github.com/grafana/grafana/pull/3769)
+* **Elasticsearch**: Fix for query template variable when looking up terms without query, no longer relies on elasticsearch default field, fixes [#3887](https://github.com/grafana/grafana/pull/3887)
+* **PNG Rendering**: Fix for server side rendering when using auth proxy, fixes [#5906](https://github.com/grafana/grafana/pull/5906)
 
 # 3.1.2 (unreleased)
 * **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)

+ 4 - 1
Gruntfile.js

@@ -9,7 +9,6 @@ module.exports = function (grunt) {
     genDir: 'public_gen',
     destDir: 'dist',
     tempDir: 'tmp',
-    arch: os.arch(),
     platform: process.platform.replace('win32', 'windows'),
   };
 
@@ -17,6 +16,10 @@ module.exports = function (grunt) {
     config.arch = process.env.hasOwnProperty('ProgramFiles(x86)') ? 'x64' : 'x86';
   }
 
+  config.arch = grunt.option('arch') || os.arch();
+
+  config.phjs = grunt.option('phjsToRelease');
+
   config.pkg.version = grunt.option('pkgVer') || config.pkg.version;
   console.log('Version', config.pkg.version);
 

+ 1 - 1
README.md

@@ -96,7 +96,7 @@ easily the grafana repository you want to build.
 ```bash
 go get github.com/*your_account*/grafana
 mkdir $GOPATH/src/github.com/grafana
-ln -s  github.com/*your_account*/grafana $GOPATH/src/github.com/grafana/grafana
+ln -s  $GOPATH/src/github.com/*your_account*/grafana $GOPATH/src/github.com/grafana/grafana
 ```
 
 ### Building the backend

+ 38 - 6
build.go

@@ -25,11 +25,16 @@ var (
 	versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
 	goarch    string
 	goos      string
+	gocc      string
+	gocxx     string
+	cgo       string
+	pkgArch   string
 	version   string = "v1"
 	// deb & rpm does not support semver so have to handle their version a little differently
 	linuxPackageVersion   string = "v1"
 	linuxPackageIteration string = ""
 	race                  bool
+	phjsToRelease         string
 	workingDir            string
 	binaries              []string = []string{"grafana-server", "grafana-cli"}
 )
@@ -47,6 +52,11 @@ func main() {
 
 	flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
 	flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
+	flag.StringVar(&gocc, "cc", "", "CC")
+	flag.StringVar(&gocxx, "cxx", "", "CXX")
+	flag.StringVar(&cgo, "cgo-enabled", "", "CGO_ENABLED")
+	flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
+	flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
 	flag.BoolVar(&race, "race", race, "Use race detector")
 	flag.Parse()
 
@@ -73,15 +83,15 @@ func main() {
 			grunt("test")
 
 		case "package":
-			grunt("release", fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
+			grunt(gruntBuildArg("release")...)
 			createLinuxPackages()
 
 		case "pkg-rpm":
-			grunt("release")
+			grunt(gruntBuildArg("release")...)
 			createRpmPackages()
 
 		case "pkg-deb":
-			grunt("release")
+			grunt(gruntBuildArg("release")...)
 			createDebPackages()
 
 		case "latest":
@@ -258,6 +268,10 @@ func createPackage(options linuxPackageOptions) {
 		"-p", "./dist",
 	}
 
+	if pkgArch != "" {
+		args = append(args, "-a", pkgArch)
+	}
+
 	if linuxPackageIteration != "" {
 		args = append(args, "--iteration", linuxPackageIteration)
 	}
@@ -307,11 +321,20 @@ func grunt(params ...string) {
 	runPrint("./node_modules/.bin/grunt", params...)
 }
 
+func gruntBuildArg(task string) []string {
+	args := []string{task, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration)}
+	if pkgArch != "" {
+		args = append(args, fmt.Sprintf("--arch=%v", pkgArch))
+	}
+	if phjsToRelease != "" {
+		args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease))
+	}
+	return args
+}
+
 func setup() {
 	runPrint("go", "get", "-v", "github.com/kardianos/govendor")
-  runPrint("go", "get", "-v", "github.com/blang/semver")
-	runPrint("go", "get", "-v", "github.com/mattn/go-sqlite3")
-	runPrint("go", "install", "-v", "github.com/mattn/go-sqlite3")
+	runPrint("go", "install", "-v", "./pkg/cmd/grafana-server")
 }
 
 func test(pkg string) {
@@ -382,6 +405,15 @@ func setBuildEnv() {
 	if goarch == "386" {
 		os.Setenv("GO386", "387")
 	}
+	if cgo != "" {
+		os.Setenv("CGO_ENABLED", cgo)
+	}
+	if gocc != "" {
+		os.Setenv("CC", gocc)
+	}
+	if gocxx != "" {
+		os.Setenv("CXX", gocxx)
+	}
 }
 
 func getGitSha() string {

+ 37 - 21
conf/defaults.ini

@@ -9,7 +9,7 @@ app_mode = production
 # instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty
 instance_name = ${HOSTNAME}
 
-#################################### Paths ####################################
+#################################### Paths ###############################
 [paths]
 # Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
 #
@@ -23,7 +23,7 @@ logs = data/log
 #
 plugins = data/plugins
 
-#################################### Server ####################################
+#################################### Server ##############################
 [server]
 # Protocol (http or https)
 protocol = http
@@ -57,7 +57,7 @@ enable_gzip = false
 cert_file =
 cert_key =
 
-#################################### Database ####################################
+#################################### Database ############################
 [database]
 # You can configure the database connection by specifying type, host, name, user and password
 # as seperate properties or as on string using the url propertie.
@@ -84,7 +84,7 @@ server_cert_name =
 # For "sqlite3" only, path relative to data_path setting
 path = grafana.db
 
-#################################### Session ####################################
+#################################### Session #############################
 [session]
 # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
 provider = file
@@ -112,7 +112,7 @@ cookie_secure = false
 session_life_time = 86400
 gc_interval_time = 86400
 
-#################################### Analytics ####################################
+#################################### Analytics ###########################
 [analytics]
 # Server reporting, sends usage counters to stats.grafana.org every 24 hours.
 # No ip addresses are being tracked, only simple counters to track
@@ -133,7 +133,7 @@ google_analytics_ua_id =
 # Google Tag Manager ID, only enabled if you specify an id here
 google_tag_manager_id =
 
-#################################### Security ####################################
+#################################### Security ############################
 [security]
 # default admin user, created on startup
 admin_user = admin
@@ -161,6 +161,12 @@ external_enabled = true
 external_snapshot_url = https://snapshots-origin.raintank.io
 external_snapshot_name = Publish to snapshot.raintank.io
 
+# remove expired snapshot
+snapshot_remove_expired = true
+
+# remove snapshots after 90 days
+snapshot_TTL_days = 90
+
 #################################### Users ####################################
 [users]
 # disable user signup / registration
@@ -184,10 +190,11 @@ login_hint = email or username
 # Default UI theme ("dark" or "light")
 default_theme = dark
 
-# Allow users to sign in using username and password
-allow_user_pass_login = true
+[auth]
+# Set to true to disable (hide) the login form, useful if you use OAuth
+disable_login_form = false
 
-#################################### Anonymous Auth ##########################
+#################################### Anonymous Auth ######################
 [auth.anonymous]
 # enable anonymous access
 enabled = false
@@ -198,7 +205,7 @@ org_name = Main Org.
 # specify role for unauthenticated users
 org_role = Viewer
 
-#################################### Github Auth ##########################
+#################################### Github Auth #########################
 [auth.github]
 enabled = false
 allow_sign_up = false
@@ -211,7 +218,7 @@ api_url = https://api.github.com/user
 team_ids =
 allowed_organizations =
 
-#################################### Google Auth ##########################
+#################################### Google Auth #########################
 [auth.google]
 enabled = false
 allow_sign_up = false
@@ -223,7 +230,16 @@ token_url = https://accounts.google.com/o/oauth2/token
 api_url = https://www.googleapis.com/oauth2/v1/userinfo
 allowed_domains =
 
-#################################### Generic OAuth ##########################
+#################################### Grafana.net Auth ####################
+[auth.grafananet]
+enabled = false
+allow_sign_up = false
+client_id = some_id
+client_secret = some_secret
+scopes = user:email
+allowed_organizations =
+
+#################################### Generic OAuth #######################
 [auth.generic_oauth]
 enabled = false
 allow_sign_up = false
@@ -247,12 +263,12 @@ header_name = X-WEBAUTH-USER
 header_property = username
 auto_sign_up = true
 
-#################################### Auth LDAP ##########################
+#################################### Auth LDAP ###########################
 [auth.ldap]
 enabled = false
 config_file = /etc/grafana/ldap.toml
 
-#################################### SMTP / Emailing ##########################
+#################################### SMTP / Emailing #####################
 [smtp]
 enabled = false
 host = localhost:25
@@ -322,18 +338,18 @@ facility =
 tag =
 
 
-#################################### AMQP Event Publisher ##########################
+#################################### AMQP Event Publisher ################
 [event_publisher]
 enabled = false
 rabbitmq_url = amqp://localhost/
 exchange = grafana_events
 
-#################################### Dashboard JSON files ##########################
+#################################### Dashboard JSON files ################
 [dashboards.json]
 enabled = false
 path = /var/lib/grafana/dashboards
 
-#################################### Usage Quotas ##########################
+#################################### Usage Quotas ########################
 [quota]
 enabled = false
 
@@ -368,7 +384,7 @@ global_api_key = -1
 # global limit on number of logged in users.
 global_session = -1
 
-#################################### Alerting ######################################
+#################################### Alerting ############################
 # docs about alerting can be found in /docs/sources/alerting/
 #              __.-/|
 #              \`o_O'
@@ -387,7 +403,7 @@ global_session = -1
 [alerting]
 enabled = true
 
-#################################### Internal Grafana Metrics ##########################
+#################################### Internal Grafana Metrics ############
 # Metrics available at HTTP API Url /api/metrics
 [metrics]
 enabled           = true
@@ -402,9 +418,9 @@ prefix = prod.grafana.%(instance_name)s.
 [grafana_net]
 url = https://grafana.net
 
-#################################### External image storage ##########################
+#################################### External Image Storage ##############
 [external_image_storage]
-# You can choose between (s3, webdav or internal)
+# You can choose between (s3, webdav)
 provider = s3
 
 [external_image_storage.s3]

+ 20 - 1
conf/sample.ini

@@ -116,7 +116,7 @@
 # in some UI views to notify that grafana or plugin update exists
 # This option does not cause any auto updates, nor send any information
 # only a GET request to http://grafana.net to get latest versions
-check_for_updates = true
+;check_for_updates = true
 
 # Google Analytics universal tracking code, only enabled if you specify an id here
 ;google_analytics_ua_id =
@@ -149,6 +149,12 @@ check_for_updates = true
 ;external_snapshot_url = https://snapshots-origin.raintank.io
 ;external_snapshot_name = Publish to snapshot.raintank.io
 
+# remove expired snapshot
+;snapshot_remove_expired = true
+
+# remove snapshots after 90 days
+;snapshot_TTL_days = 90
+
 #################################### Users ####################################
 [users]
 # disable user signup / registration
@@ -169,6 +175,10 @@ check_for_updates = true
 # Default UI theme ("dark" or "light")
 ;default_theme = dark
 
+[auth]
+# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
+;disable_login_form = false
+
 #################################### Anonymous Auth ##########################
 [auth.anonymous]
 # enable anonymous access
@@ -218,6 +228,15 @@ check_for_updates = true
 ;team_ids =
 ;allowed_organizations =
 
+#################################### Grafana.net Auth ####################
+[auth.grafananet]
+;enabled = false
+;allow_sign_up = false
+;client_id = some_id
+;client_secret = some_secret
+;scopes = user:email
+;allowed_organizations =
+
 #################################### Auth Proxy ##########################
 [auth.proxy]
 ;enabled = false

+ 1 - 5
docs/sources/datasources/influxdb.md

@@ -6,11 +6,7 @@ page_keywords: grafana, influxdb, metrics, query, documentation
 
 # InfluxDB
 
-There are currently two separate datasources for InfluxDB in Grafana: InfluxDB 0.8.x and InfluxDB 0.9.x.
-The API and capabilities of InfluxDB 0.9.x are completely different from InfluxDB 0.8.x which is why Grafana handles
-them as different data sources.
-
-InfluxDB 0.9 is rapidly evolving and we continue to track its API. InfluxDB 0.8 is no longer maintained by InfluxDB Inc, but we provide support as a convenience to existing users.
+Grafana ships with very a feature data source plugin for InfluxDB. Supporting a feature rich query editor, annotation and templating queries.
 
 ## Adding the data source
 ![](/img/v2/add_Influx.jpg)

+ 4 - 6
docs/sources/http_api/admin.md

@@ -6,6 +6,10 @@ page_keywords: grafana, admin, http, api, documentation
 
 # Admin API
 
+The admin http API does not currently work with an api token. Api Token's are currently only linked to an organization and organization role. They cannot given
+the permission of server admin, only user's can be given that permission. So in order to use these API calls you will have to use basic auth and Grafana user
+with Grafana admin permission.
+
 ## Settings
 
 `GET /api/admin/settings`
@@ -15,7 +19,6 @@ page_keywords: grafana, admin, http, api, documentation
     GET /api/admin/settings
     Accept: application/json
     Content-Type: application/json
-    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
 **Example Response**:
 
@@ -171,7 +174,6 @@ page_keywords: grafana, admin, http, api, documentation
     GET /api/admin/stats
     Accept: application/json
     Content-Type: application/json
-    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
 **Example Response**:
 
@@ -201,7 +203,6 @@ Create new user
     POST /api/admin/users HTTP/1.1
     Accept: application/json
     Content-Type: application/json
-    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
     {
       "name":"User",
@@ -228,7 +229,6 @@ Change password for specific user
     PUT /api/admin/users/2/password HTTP/1.1
     Accept: application/json
     Content-Type: application/json
-    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
 **Example Response**:
 
@@ -246,7 +246,6 @@ Change password for specific user
     PUT /api/admin/users/2/permissions HTTP/1.1
     Accept: application/json
     Content-Type: application/json
-    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
 **Example Response**:
 
@@ -264,7 +263,6 @@ Change password for specific user
     DELETE /api/admin/users/2 HTTP/1.1
     Accept: application/json
     Content-Type: application/json
-    Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
 **Example Response**:
 

+ 41 - 0
docs/sources/installation/configuration.md

@@ -238,6 +238,14 @@ options are `Admin` and `Editor` and `Read-Only Editor`.
 
 <hr>
 
+## [auth]
+
+### disable_login_form
+
+Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false.
+
+<hr>
+
 ## [auth.anonymous]
 
 ### enabled
@@ -484,6 +492,33 @@ Grafana backend index those json dashboards which will make them appear in regul
 ### path
 The full path to a directory containing your json dashboards.
 
+## [smtp]
+Email server settings.
+
+### enabled
+defaults to false
+
+### host
+defaults to localhost:25
+
+### user
+In case of SMTP auth, defaults to `empty`
+
+### password
+In case of SMTP auth, defaults to `empty`
+
+### cert_file
+File path to a cert file, defaults to `empty`
+
+### key_file
+File path to a key file, defaults to `empty`
+
+### skip_verify
+Verify SSL for smtp server? defaults to `false`
+
+### from_address
+Address used when sending out emails, defaults to `admin@grafana.localhost`
+
 ## [log]
 
 ### mode
@@ -525,3 +560,9 @@ Set root url to a Grafana instance where you want to publish external snapshots
 
 ### external_snapshot_name
 Set name for external snapshot button. Defaults to `Publish to snapshot.raintank.io`
+
+### remove expired snapshot
+Enabled to automatically remove expired snapshots
+
+### remove snapshots after 90 days
+Time to live for snapshots.

+ 19 - 0
pkg/api/alerting.go

@@ -25,6 +25,25 @@ func ValidateOrgAlert(c *middleware.Context) {
 	}
 }
 
+func GetAlertStatesForDashboard(c *middleware.Context) Response {
+	dashboardId := c.QueryInt64("dashboardId")
+
+	if dashboardId == 0 {
+		return ApiError(400, "Missing query parameter dashboardId", nil)
+	}
+
+	query := models.GetAlertStatesForDashboardQuery{
+		OrgId:       c.OrgId,
+		DashboardId: c.QueryInt64("dashboardId"),
+	}
+
+	if err := bus.Dispatch(&query); err != nil {
+		return ApiError(500, "Failed to fetch alert states", err)
+	}
+
+	return Json(200, query.Result)
+}
+
 // GET /api/alerts
 func GetAlerts(c *middleware.Context) Response {
 	query := models.GetAlertsQuery{

+ 5 - 2
pkg/api/api.go

@@ -58,6 +58,7 @@ func Register(r *macaron.Macaron) {
 	r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
 
 	r.Get("/dashboard/*", reqSignedIn, Index)
+	r.Get("/dashboard-solo/snapshot/*", Index)
 	r.Get("/dashboard-solo/*", reqSignedIn, Index)
 	r.Get("/import/dashboard", reqSignedIn, Index)
 	r.Get("/dashboards/*", reqSignedIn, Index)
@@ -202,9 +203,9 @@ func Register(r *macaron.Macaron) {
 
 		r.Get("/plugins", wrap(GetPluginList))
 		r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
+		r.Get("/plugins/:pluginId/readme", wrap(GetPluginReadme))
 
 		r.Group("/plugins", func() {
-			r.Get("/:pluginId/readme", wrap(GetPluginReadme))
 			r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
 			r.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
 		}, reqOrgAdmin)
@@ -243,7 +244,8 @@ func Register(r *macaron.Macaron) {
 		r.Get("/search/", Search)
 
 		// metrics
-		r.Get("/metrics/test", wrap(GetTestMetrics))
+		r.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
+		r.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
 
 		// metrics
 		r.Get("/metrics", wrap(GetInternalMetrics))
@@ -252,6 +254,7 @@ func Register(r *macaron.Macaron) {
 			r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
 			r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
 			r.Get("/", wrap(GetAlerts))
+			r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
 		})
 
 		r.Get("/alert-notifications", wrap(GetAlertNotifications))

+ 4 - 7
pkg/api/dtos/models.go

@@ -96,13 +96,10 @@ func (slice DataSourceList) Swap(i, j int) {
 	slice[i], slice[j] = slice[j], slice[i]
 }
 
-type MetricQueryResultDto struct {
-	Data []MetricQueryResultDataDto `json:"data"`
-}
-
-type MetricQueryResultDataDto struct {
-	Target     string       `json:"target"`
-	DataPoints [][2]float64 `json:"datapoints"`
+type MetricRequest struct {
+	From    string             `json:"from"`
+	To      string             `json:"to"`
+	Queries []*simplejson.Json `json:"queries"`
 }
 
 type UserStars struct {

+ 23 - 0
pkg/api/dtos/playlist.go

@@ -0,0 +1,23 @@
+package dtos
+
+type PlaylistDashboard struct {
+	Id    int64  `json:"id"`
+	Slug  string `json:"slug"`
+	Title string `json:"title"`
+	Uri   string `json:"uri"`
+	Order int    `json:"order"`
+}
+
+type PlaylistDashboardsSlice []PlaylistDashboard
+
+func (slice PlaylistDashboardsSlice) Len() int {
+	return len(slice)
+}
+
+func (slice PlaylistDashboardsSlice) Less(i, j int) bool {
+	return slice[i].Order < slice[j].Order
+}
+
+func (slice PlaylistDashboardsSlice) Swap(i, j int) {
+	slice[i], slice[j] = slice[j], slice[i]
+}

+ 1 - 1
pkg/api/frontendsettings.go

@@ -38,7 +38,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 		url := ds.Url
 
 		if ds.Access == m.DS_ACCESS_PROXY {
-			url = setting.AppSubUrl + "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10)
+			url = "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10)
 		}
 
 		var dsMap = map[string]interface{}{

+ 14 - 3
pkg/api/index.go

@@ -1,6 +1,7 @@
 package api
 
 import (
+	"fmt"
 	"strings"
 
 	"github.com/grafana/grafana/pkg/api/dtos"
@@ -32,6 +33,16 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 		locale = parts[0]
 	}
 
+	appUrl := setting.AppUrl
+	appSubUrl := setting.AppSubUrl
+
+	// special case when doing localhost call from phantomjs
+	if c.IsRenderCall {
+		appUrl = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort)
+		appSubUrl = ""
+		settings["appSubUrl"] = ""
+	}
+
 	var data = dtos.IndexViewData{
 		User: &dtos.CurrentUser{
 			Id:             c.UserId,
@@ -49,8 +60,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 			Locale:         locale,
 		},
 		Settings:                settings,
-		AppUrl:                  setting.AppUrl,
-		AppSubUrl:               setting.AppSubUrl,
+		AppUrl:                  appUrl,
+		AppSubUrl:               appSubUrl,
 		GoogleAnalyticsId:       setting.GoogleAnalyticsId,
 		GoogleTagManagerId:      setting.GoogleTagManagerId,
 		BuildVersion:            setting.BuildVersion,
@@ -154,7 +165,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 				}
 			}
 
-			if c.OrgRole == m.ROLE_ADMIN {
+			if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
 				appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
 				appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
 			}

+ 7 - 5
pkg/api/login.go

@@ -25,13 +25,15 @@ func LoginView(c *middleware.Context) {
 		return
 	}
 
-	viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
-	viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
-	viewData.Settings["genericOAuthEnabled"] = setting.OAuthService.Generic
-	viewData.Settings["oauthProviderName"] = setting.OAuthService.OAuthProviderName
+	enabledOAuths := make(map[string]interface{})
+	for key, oauth := range setting.OAuthService.OAuthInfos {
+		enabledOAuths[key] = map[string]string{"name": oauth.Name}
+	}
+
+	viewData.Settings["oauth"] = enabledOAuths
 	viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
 	viewData.Settings["loginHint"] = setting.LoginHint
-	viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin
+	viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
 
 	if !tryLoginUsingRememberCookie(c) {
 		c.HTML(200, VIEW_INDEX, viewData)

+ 8 - 8
pkg/api/login_oauth.go

@@ -3,7 +3,6 @@ package api
 import (
 	"errors"
 	"fmt"
-	"net/url"
 
 	"golang.org/x/oauth2"
 
@@ -46,9 +45,9 @@ func OAuthLogin(ctx *middleware.Context) {
 	userInfo, err := connect.UserInfo(token)
 	if err != nil {
 		if err == social.ErrMissingTeamMembership {
-			ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled"))
+			ctx.Redirect(setting.AppSubUrl + "/login?failCode=1000")
 		} else if err == social.ErrMissingOrganizationMembership {
-			ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github organization membership not fulfilled"))
+			ctx.Redirect(setting.AppSubUrl + "/login?failCode=1001")
 		} else {
 			ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
 		}
@@ -60,7 +59,7 @@ func OAuthLogin(ctx *middleware.Context) {
 	// validate that the email is allowed to login to grafana
 	if !connect.IsEmailAllowed(userInfo.Email) {
 		ctx.Logger.Info("OAuth login attempt with unallowed email", "email", userInfo.Email)
-		ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required email domain not fulfilled"))
+		ctx.Redirect(setting.AppSubUrl + "/login?failCode=1002")
 		return
 	}
 
@@ -83,10 +82,11 @@ func OAuthLogin(ctx *middleware.Context) {
 			return
 		}
 		cmd := m.CreateUserCommand{
-			Login:   userInfo.Email,
-			Email:   userInfo.Email,
-			Name:    userInfo.Name,
-			Company: userInfo.Company,
+			Login:          userInfo.Email,
+			Email:          userInfo.Email,
+			Name:           userInfo.Name,
+			Company:        userInfo.Company,
+			DefaultOrgRole: userInfo.Role,
 		}
 
 		if err = bus.Dispatch(&cmd); err != nil {

+ 39 - 24
pkg/api/metrics.go

@@ -2,39 +2,54 @@ package api
 
 import (
 	"encoding/json"
-	"math/rand"
 	"net/http"
-	"strconv"
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
+	"github.com/grafana/grafana/pkg/tsdb"
+	"github.com/grafana/grafana/pkg/tsdb/testdata"
 	"github.com/grafana/grafana/pkg/util"
 )
 
-func GetTestMetrics(c *middleware.Context) Response {
-	from := c.QueryInt64("from")
-	to := c.QueryInt64("to")
-	maxDataPoints := c.QueryInt64("maxDataPoints")
-	stepInSeconds := (to - from) / maxDataPoints
-
-	result := dtos.MetricQueryResultDto{}
-	result.Data = make([]dtos.MetricQueryResultDataDto, 1)
-
-	for seriesIndex := range result.Data {
-		points := make([][2]float64, maxDataPoints)
-		walker := rand.Float64() * 100
-		time := from
-
-		for i := range points {
-			points[i][0] = walker
-			points[i][1] = float64(time)
-			walker += rand.Float64() - 0.5
-			time += stepInSeconds
-		}
+// POST /api/tsdb/query
+func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
+	timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
+
+	request := &tsdb.Request{TimeRange: timeRange}
+
+	for _, query := range reqDto.Queries {
+		request.Queries = append(request.Queries, &tsdb.Query{
+			RefId:         query.Get("refId").MustString("A"),
+			MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
+			IntervalMs:    query.Get("intervalMs").MustInt64(1000),
+			Model:         query,
+			DataSource: &tsdb.DataSourceInfo{
+				Name:     "Grafana TestDataDB",
+				PluginId: "grafana-testdata-datasource",
+			},
+		})
+	}
+
+	resp, err := tsdb.HandleRequest(request)
+	if err != nil {
+		return ApiError(500, "Metric request error", err)
+	}
+
+	return Json(200, &resp)
+}
 
-		result.Data[seriesIndex].Target = "test-series-" + strconv.Itoa(seriesIndex)
-		result.Data[seriesIndex].DataPoints = points
+// GET /api/tsdb/testdata/scenarios
+func GetTestDataScenarios(c *middleware.Context) Response {
+	result := make([]interface{}, 0)
+
+	for _, scenario := range testdata.ScenarioRegistry {
+		result = append(result, map[string]interface{}{
+			"id":          scenario.Id,
+			"name":        scenario.Name,
+			"description": scenario.Description,
+			"stringInput": scenario.StringInput,
+		})
 	}
 
 	return Json(200, &result)

+ 19 - 10
pkg/api/playlist_play.go

@@ -1,16 +1,18 @@
 package api
 
 import (
+	"sort"
 	"strconv"
 
+	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	_ "github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/search"
 )
 
-func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, error) {
-	result := make([]m.PlaylistDashboardDto, 0)
+func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]int) (dtos.PlaylistDashboardsSlice, error) {
+	result := make(dtos.PlaylistDashboardsSlice, 0)
 
 	if len(dashboardByIds) > 0 {
 		dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIds}
@@ -19,11 +21,12 @@ func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, e
 		}
 
 		for _, item := range dashboardQuery.Result {
-			result = append(result, m.PlaylistDashboardDto{
+			result = append(result, dtos.PlaylistDashboard{
 				Id:    item.Id,
 				Slug:  item.Slug,
 				Title: item.Title,
 				Uri:   "db/" + item.Slug,
+				Order: dashboardIdOrder[item.Id],
 			})
 		}
 	}
@@ -31,8 +34,8 @@ func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, e
 	return result, nil
 }
 
-func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.PlaylistDashboardDto {
-	result := make([]m.PlaylistDashboardDto, 0)
+func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
+	result := make(dtos.PlaylistDashboardsSlice, 0)
 
 	if len(dashboardByTag) > 0 {
 		for _, tag := range dashboardByTag {
@@ -47,10 +50,11 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.P
 
 			if err := bus.Dispatch(&searchQuery); err == nil {
 				for _, item := range searchQuery.Result {
-					result = append(result, m.PlaylistDashboardDto{
+					result = append(result, dtos.PlaylistDashboard{
 						Id:    item.Id,
 						Title: item.Title,
 						Uri:   item.Uri,
+						Order: dashboardTagOrder[tag],
 					})
 				}
 			}
@@ -60,28 +64,33 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.P
 	return result
 }
 
-func LoadPlaylistDashboards(orgId, userId, playlistId int64) ([]m.PlaylistDashboardDto, error) {
+func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
 	playlistItems, _ := LoadPlaylistItems(playlistId)
 
 	dashboardByIds := make([]int64, 0)
 	dashboardByTag := make([]string, 0)
+	dashboardIdOrder := make(map[int64]int)
+	dashboardTagOrder := make(map[string]int)
 
 	for _, i := range playlistItems {
 		if i.Type == "dashboard_by_id" {
 			dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
 			dashboardByIds = append(dashboardByIds, dashboardId)
+			dashboardIdOrder[dashboardId] = i.Order
 		}
 
 		if i.Type == "dashboard_by_tag" {
 			dashboardByTag = append(dashboardByTag, i.Value)
+			dashboardTagOrder[i.Value] = i.Order
 		}
 	}
 
-	result := make([]m.PlaylistDashboardDto, 0)
+	result := make(dtos.PlaylistDashboardsSlice, 0)
 
-	var k, _ = populateDashboardsById(dashboardByIds)
+	var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
 	result = append(result, k...)
-	result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag)...)
+	result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag, dashboardTagOrder)...)
 
+	sort.Sort(sort.Reverse(result))
 	return result, nil
 }

+ 5 - 19
pkg/api/render.go

@@ -6,35 +6,21 @@ import (
 
 	"github.com/grafana/grafana/pkg/components/renderer"
 	"github.com/grafana/grafana/pkg/middleware"
-	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 )
 
 func RenderToPng(c *middleware.Context) {
 	queryReader := util.NewUrlQueryReader(c.Req.URL)
 	queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
-	sessionId := c.Session.ID()
-
-	// Handle api calls authenticated without session
-	if sessionId == "" && c.ApiKeyId != 0 {
-		c.Session.Start(c)
-		c.Session.Set(middleware.SESS_KEY_APIKEY, c.ApiKeyId)
-		// release will make sure the new session is persisted before
-		// we spin up phantomjs
-		c.Session.Release()
-		// cleanup session after render is complete
-		defer func() { c.Session.Destory(c) }()
-	}
 
 	renderOpts := &renderer.RenderOpts{
-		Url:       c.Params("*") + queryParams,
-		Width:     queryReader.Get("width", "800"),
-		Height:    queryReader.Get("height", "400"),
-		SessionId: c.Session.ID(),
-		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"),
 	}
 
-	renderOpts.Url = setting.ToAbsUrl(renderOpts.Url)
 	pngPath, err := renderer.RenderToPng(renderOpts)
 
 	if err != nil {

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

@@ -141,8 +141,6 @@ func createRequest(repoUrl string, subPaths ...string) ([]byte, error) {
 
 	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
 
-	logger.Info("grafanaVersion ", grafanaVersion)
-
 	req.Header.Set("grafana-version", grafanaVersion)
 	req.Header.Set("User-Agent", "grafana "+grafanaVersion)
 

+ 14 - 39
pkg/cmd/grafana-server/main.go

@@ -13,16 +13,15 @@ import (
 	"time"
 
 	"github.com/grafana/grafana/pkg/log"
-	"github.com/grafana/grafana/pkg/login"
-	"github.com/grafana/grafana/pkg/metrics"
-	"github.com/grafana/grafana/pkg/plugins"
-	alertingInit "github.com/grafana/grafana/pkg/services/alerting/init"
-	"github.com/grafana/grafana/pkg/services/eventpublisher"
-	"github.com/grafana/grafana/pkg/services/notifications"
-	"github.com/grafana/grafana/pkg/services/search"
+	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/sqlstore"
 	"github.com/grafana/grafana/pkg/setting"
-	"github.com/grafana/grafana/pkg/social"
+
+	_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
+	_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
+	_ "github.com/grafana/grafana/pkg/tsdb/graphite"
+	_ "github.com/grafana/grafana/pkg/tsdb/prometheus"
+	_ "github.com/grafana/grafana/pkg/tsdb/testdata"
 )
 
 var version = "3.1.0"
@@ -56,26 +55,8 @@ func main() {
 	setting.BuildCommit = commit
 	setting.BuildStamp = buildstampInt64
 
-	go listenToSystemSignels()
-
-	flag.Parse()
-	writePIDFile()
-	initRuntime()
-	metrics.Init()
-
-	search.Init()
-	login.Init()
-	social.NewOAuthService()
-	eventpublisher.Init()
-	plugins.Init()
-	alertingInit.Init()
-
-	if err := notifications.Init(); err != nil {
-		log.Fatal(3, "Notification service failed to initialize", err)
-	}
-
-	StartServer()
-	exitChan <- 0
+	server := NewGrafanaServer()
+	server.Start()
 }
 
 func initRuntime() {
@@ -93,7 +74,9 @@ func initRuntime() {
 	logger.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
 
 	setting.LogConfigurationInfo()
+}
 
+func initSql() {
 	sqlstore.NewEngine()
 	sqlstore.EnsureAdminUser()
 }
@@ -116,7 +99,7 @@ func writePIDFile() {
 	}
 }
 
-func listenToSystemSignels() {
+func listenToSystemSignals(server models.GrafanaServer) {
 	signalChan := make(chan os.Signal, 1)
 	code := 0
 
@@ -124,16 +107,8 @@ func listenToSystemSignels() {
 
 	select {
 	case sig := <-signalChan:
-		log.Info("Received signal %s. shutting down", sig)
+		server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
 	case code = <-exitChan:
-		switch code {
-		case 0:
-			log.Info("Shutting down")
-		default:
-			log.Warn("Shutting down")
-		}
+		server.Shutdown(code, "startup error")
 	}
-
-	log.Close()
-	os.Exit(code)
 }

+ 128 - 0
pkg/cmd/grafana-server/server.go

@@ -0,0 +1,128 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"os"
+	"time"
+
+	"golang.org/x/sync/errgroup"
+
+	"github.com/grafana/grafana/pkg/api"
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/login"
+	"github.com/grafana/grafana/pkg/metrics"
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
+	"github.com/grafana/grafana/pkg/services/alerting"
+	"github.com/grafana/grafana/pkg/services/cleanup"
+	"github.com/grafana/grafana/pkg/services/eventpublisher"
+	"github.com/grafana/grafana/pkg/services/notifications"
+	"github.com/grafana/grafana/pkg/services/search"
+	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/social"
+)
+
+func NewGrafanaServer() models.GrafanaServer {
+	rootCtx, shutdownFn := context.WithCancel(context.Background())
+	childRoutines, childCtx := errgroup.WithContext(rootCtx)
+
+	return &GrafanaServerImpl{
+		context:       childCtx,
+		shutdownFn:    shutdownFn,
+		childRoutines: childRoutines,
+		log:           log.New("server"),
+	}
+}
+
+type GrafanaServerImpl struct {
+	context       context.Context
+	shutdownFn    context.CancelFunc
+	childRoutines *errgroup.Group
+	log           log.Logger
+}
+
+func (g *GrafanaServerImpl) Start() {
+	go listenToSystemSignals(g)
+
+	writePIDFile()
+	initRuntime()
+	initSql()
+	metrics.Init()
+	search.Init()
+	login.Init()
+	social.NewOAuthService()
+	eventpublisher.Init()
+	plugins.Init()
+
+	// init alerting
+	if setting.AlertingEnabled {
+		engine := alerting.NewEngine()
+		g.childRoutines.Go(func() error { return engine.Run(g.context) })
+	}
+
+	// cleanup service
+	cleanUpService := cleanup.NewCleanUpService()
+	g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
+
+	if err := notifications.Init(); err != nil {
+		g.log.Error("Notification service failed to initialize", "erro", err)
+		g.Shutdown(1, "Startup failed")
+		return
+	}
+
+	g.startHttpServer()
+}
+
+func (g *GrafanaServerImpl) startHttpServer() {
+	logger = log.New("http.server")
+
+	var err error
+	m := newMacaron()
+	api.Register(m)
+
+	listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
+	g.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl)
+
+	switch setting.Protocol {
+	case setting.HTTP:
+		err = http.ListenAndServe(listenAddr, m)
+	case setting.HTTPS:
+		err = http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
+	default:
+		g.log.Error("Invalid protocol", "protocol", setting.Protocol)
+		g.Shutdown(1, "Startup failed")
+	}
+
+	if err != nil {
+		g.log.Error("Fail to start server", "error", err)
+		g.Shutdown(1, "Startup failed")
+		return
+	}
+}
+
+func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
+	g.log.Info("Shutdown started", "code", code, "reason", reason)
+
+	g.shutdownFn()
+	err := g.childRoutines.Wait()
+
+	g.log.Info("Shutdown completed", "reason", err)
+	log.Close()
+	os.Exit(code)
+}
+
+// implement context.Context
+func (g *GrafanaServerImpl) Deadline() (deadline time.Time, ok bool) {
+	return g.context.Deadline()
+}
+func (g *GrafanaServerImpl) Done() <-chan struct{} {
+	return g.context.Done()
+}
+func (g *GrafanaServerImpl) Err() error {
+	return g.context.Err()
+}
+func (g *GrafanaServerImpl) Value(key interface{}) interface{} {
+	return g.context.Value(key)
+}

+ 5 - 4
pkg/cmd/grafana-server/web.go

@@ -6,7 +6,6 @@ package main
 import (
 	"fmt"
 	"net/http"
-	"os"
 	"path"
 
 	"gopkg.in/macaron.v1"
@@ -79,7 +78,7 @@ func mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
 	))
 }
 
-func StartServer() {
+func StartServer() int {
 	logger = log.New("server")
 
 	var err error
@@ -95,11 +94,13 @@ func StartServer() {
 		err = http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
 	default:
 		logger.Error("Invalid protocol", "protocol", setting.Protocol)
-		os.Exit(1)
+		return 1
 	}
 
 	if err != nil {
 		logger.Error("Fail to start server", "error", err)
-		os.Exit(1)
+		return 1
 	}
+
+	return 0
 }

+ 24 - 9
pkg/components/renderer/renderer.go

@@ -12,36 +12,51 @@ import (
 	"strconv"
 
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 )
 
 type RenderOpts struct {
-	Url       string
-	Width     string
-	Height    string
-	SessionId string
-	Timeout   string
+	Path    string
+	Width   string
+	Height  string
+	Timeout string
+	OrgId   int64
 }
 
 var rendererLog log.Logger = log.New("png-renderer")
 
 func RenderToPng(params *RenderOpts) (string, error) {
-	rendererLog.Info("Rendering", "url", params.Url)
+	rendererLog.Info("Rendering", "path", params.Path)
 
 	var executable = "phantomjs"
 	if runtime.GOOS == "windows" {
 		executable = executable + ".exe"
 	}
 
+	url := fmt.Sprintf("%s://localhost:%s/%s", setting.Protocol, setting.HttpPort, params.Path)
+
 	binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable))
 	scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js"))
 	pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
 	pngPath = pngPath + ".png"
 
-	cmd := exec.Command(binPath, "--ignore-ssl-errors=true", scriptPath, "url="+params.Url, "width="+params.Width,
-		"height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName,
-		"domain="+setting.Domain, "sessionid="+params.SessionId)
+	renderKey := middleware.AddRenderAuthKey(params.OrgId)
+	defer middleware.RemoveRenderAuthKey(renderKey)
+
+	cmdArgs := []string{
+		"--ignore-ssl-errors=true",
+		scriptPath,
+		"url=" + url,
+		"width=" + params.Width,
+		"height=" + params.Height,
+		"png=" + pngPath,
+		"domain=" + setting.Domain,
+		"renderKey=" + renderKey,
+	}
+
+	cmd := exec.Command(binPath, cmdArgs...)
 	stdout, err := cmd.StdoutPipe()
 
 	if err != nil {

+ 32 - 4
pkg/log/log.go

@@ -32,11 +32,25 @@ func New(logger string, ctx ...interface{}) Logger {
 }
 
 func Trace(format string, v ...interface{}) {
-	Root.Debug(fmt.Sprintf(format, v))
+	var message string
+	if len(v) > 0 {
+		message = fmt.Sprintf(format, v)
+	} else {
+		message = format
+	}
+
+	Root.Debug(message)
 }
 
 func Debug(format string, v ...interface{}) {
-	Root.Debug(fmt.Sprintf(format, v))
+	var message string
+	if len(v) > 0 {
+		message = fmt.Sprintf(format, v)
+	} else {
+		message = format
+	}
+
+	Root.Debug(message)
 }
 
 func Debug2(message string, v ...interface{}) {
@@ -44,7 +58,14 @@ func Debug2(message string, v ...interface{}) {
 }
 
 func Info(format string, v ...interface{}) {
-	Root.Info(fmt.Sprintf(format, v))
+	var message string
+	if len(v) > 0 {
+		message = fmt.Sprintf(format, v)
+	} else {
+		message = format
+	}
+
+	Root.Info(message)
 }
 
 func Info2(message string, v ...interface{}) {
@@ -52,7 +73,14 @@ func Info2(message string, v ...interface{}) {
 }
 
 func Warn(format string, v ...interface{}) {
-	Root.Warn(fmt.Sprintf(format, v))
+	var message string
+	if len(v) > 0 {
+		message = fmt.Sprintf(format, v)
+	} else {
+		message = format
+	}
+
+	Root.Warn(message)
 }
 
 func Warn2(message string, v ...interface{}) {

+ 4 - 4
pkg/metrics/gauge.go

@@ -24,10 +24,10 @@ func NewGauge(meta *MetricMeta) Gauge {
 	}
 }
 
-func RegGauge(meta *MetricMeta) Gauge {
-	g := NewGauge(meta)
-	MetricStats.Register(g)
-	return g
+func RegGauge(name string, tagStrings ...string) Gauge {
+	tr := NewGauge(NewMetricMeta(name, tagStrings))
+	MetricStats.Register(tr)
+	return tr
 }
 
 // GaugeSnapshot is a read-only copy of another Gauge.

+ 2 - 0
pkg/metrics/graphite.go

@@ -63,6 +63,8 @@ func (this *GraphitePublisher) Publish(metrics []Metric) {
 		switch metric := m.(type) {
 		case Counter:
 			this.addCount(buf, metricName+".count", metric.Count(), now)
+		case Gauge:
+			this.addCount(buf, metricName, metric.Value(), now)
 		case Timer:
 			percentiles := metric.Percentiles([]float64{0.25, 0.75, 0.90, 0.99})
 			this.addCount(buf, metricName+".count", metric.Count(), now)

+ 12 - 0
pkg/metrics/metrics.go

@@ -49,6 +49,12 @@ var (
 	// Timers
 	M_DataSource_ProxyReq_Timer Timer
 	M_Alerting_Exeuction_Time   Timer
+
+	// StatTotals
+	M_StatTotal_Dashboards Gauge
+	M_StatTotal_Users      Gauge
+	M_StatTotal_Orgs       Gauge
+	M_StatTotal_Playlists  Gauge
 )
 
 func initMetricVars(settings *MetricSettings) {
@@ -105,4 +111,10 @@ func initMetricVars(settings *MetricSettings) {
 	// Timers
 	M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
 	M_Alerting_Exeuction_Time = RegTimer("alerting.execution_time")
+
+	// StatTotals
+	M_StatTotal_Dashboards = RegGauge("stat_totals", "stat", "dashboards")
+	M_StatTotal_Users = RegGauge("stat_totals", "stat", "users")
+	M_StatTotal_Orgs = RegGauge("stat_totals", "stat", "orgs")
+	M_StatTotal_Playlists = RegGauge("stat_totals", "stat", "playlists")
 }

+ 22 - 0
pkg/metrics/publish.go

@@ -15,6 +15,7 @@ import (
 )
 
 var metricsLogger log.Logger = log.New("metrics")
+var metricPublishCounter int64 = 0
 
 func Init() {
 	settings := readSettings()
@@ -45,12 +46,33 @@ func sendMetrics(settings *MetricSettings) {
 		return
 	}
 
+	updateTotalStats()
+
 	metrics := MetricStats.GetSnapshots()
 	for _, publisher := range settings.Publishers {
 		publisher.Publish(metrics)
 	}
 }
 
+func updateTotalStats() {
+
+	// every interval also publish totals
+	metricPublishCounter++
+	if metricPublishCounter%10 == 0 {
+		// get stats
+		statsQuery := m.GetSystemStatsQuery{}
+		if err := bus.Dispatch(&statsQuery); err != nil {
+			metricsLogger.Error("Failed to get system stats", "error", err)
+			return
+		}
+
+		M_StatTotal_Dashboards.Update(statsQuery.Result.DashboardCount)
+		M_StatTotal_Users.Update(statsQuery.Result.UserCount)
+		M_StatTotal_Playlists.Update(statsQuery.Result.PlaylistCount)
+		M_StatTotal_Orgs.Update(statsQuery.Result.OrgCount)
+	}
+}
+
 func sendUsageStats() {
 	if !setting.ReportingEnabled {
 		return

+ 3 - 25
pkg/middleware/middleware.go

@@ -22,6 +22,7 @@ type Context struct {
 	Session SessionStore
 
 	IsSignedIn     bool
+	IsRenderCall   bool
 	AllowAnonymous bool
 	Logger         log.Logger
 }
@@ -42,11 +43,11 @@ func GetContextHandler() macaron.Handler {
 		// then init session and look for userId in session
 		// then look for api key in session (special case for render calls via api)
 		// then test if anonymous access is enabled
-		if initContextWithApiKey(ctx) ||
+		if initContextWithRenderAuth(ctx) ||
+			initContextWithApiKey(ctx) ||
 			initContextWithBasicAuth(ctx) ||
 			initContextWithAuthProxy(ctx) ||
 			initContextWithUserSessionCookie(ctx) ||
-			initContextWithApiKeyFromSession(ctx) ||
 			initContextWithAnonymousUser(ctx) {
 		}
 
@@ -176,29 +177,6 @@ func initContextWithBasicAuth(ctx *Context) bool {
 	}
 }
 
-// special case for panel render calls with api key
-func initContextWithApiKeyFromSession(ctx *Context) bool {
-	keyId := ctx.Session.Get(SESS_KEY_APIKEY)
-	if keyId == nil {
-		return false
-	}
-
-	keyQuery := m.GetApiKeyByIdQuery{ApiKeyId: keyId.(int64)}
-	if err := bus.Dispatch(&keyQuery); err != nil {
-		ctx.Logger.Error("Failed to get api key by id", "id", keyId, "error", err)
-		return false
-	} else {
-		apikey := keyQuery.Result
-
-		ctx.IsSignedIn = true
-		ctx.SignedInUser = &m.SignedInUser{}
-		ctx.OrgRole = apikey.Role
-		ctx.ApiKeyId = apikey.Id
-		ctx.OrgId = apikey.OrgId
-		return true
-	}
-}
-
 // Handle handles and logs error by given status.
 func (ctx *Context) Handle(status int, title string, err error) {
 	if err != nil {

+ 55 - 0
pkg/middleware/render_auth.go

@@ -0,0 +1,55 @@
+package middleware
+
+import (
+	"sync"
+
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/util"
+)
+
+var renderKeysLock sync.Mutex
+var renderKeys map[string]*m.SignedInUser = make(map[string]*m.SignedInUser)
+
+func initContextWithRenderAuth(ctx *Context) bool {
+	key := ctx.GetCookie("renderKey")
+	if key == "" {
+		return false
+	}
+
+	renderKeysLock.Lock()
+	defer renderKeysLock.Unlock()
+
+	if renderUser, exists := renderKeys[key]; !exists {
+		ctx.JsonApiErr(401, "Invalid Render Key", nil)
+		return true
+	} else {
+
+		ctx.IsSignedIn = true
+		ctx.SignedInUser = renderUser
+		ctx.IsRenderCall = true
+		return true
+	}
+}
+
+type renderContextFunc func(key string) (string, error)
+
+func AddRenderAuthKey(orgId int64) string {
+	renderKeysLock.Lock()
+
+	key := util.GetRandomString(32)
+
+	renderKeys[key] = &m.SignedInUser{
+		OrgId:   orgId,
+		OrgRole: m.ROLE_VIEWER,
+	}
+
+	renderKeysLock.Unlock()
+
+	return key
+}
+
+func RemoveRenderAuthKey(key string) {
+	renderKeysLock.Lock()
+	delete(renderKeys, key)
+	renderKeysLock.Unlock()
+}

+ 0 - 1
pkg/middleware/session.go

@@ -13,7 +13,6 @@ import (
 
 const (
 	SESS_KEY_USERID = "uid"
-	SESS_KEY_APIKEY = "apikey_id" // used fror render requests with api keys
 )
 
 var sessionManager *session.Manager

+ 15 - 0
pkg/models/alert.go

@@ -135,3 +135,18 @@ type GetAlertByIdQuery struct {
 
 	Result *Alert
 }
+
+type GetAlertStatesForDashboardQuery struct {
+	OrgId       int64
+	DashboardId int64
+
+	Result []*AlertStateInfoDTO
+}
+
+type AlertStateInfoDTO struct {
+	Id           int64          `json:"id"`
+	DashboardId  int64          `json:"dashboardId"`
+	PanelId      int64          `json:"panelId"`
+	State        AlertStateType `json:"state"`
+	NewStateDate time.Time      `json:"newStateDate"`
+}

+ 3 - 0
pkg/models/dashboard_snapshot.go

@@ -63,6 +63,9 @@ type DeleteDashboardSnapshotCommand struct {
 	DeleteKey string `json:"-"`
 }
 
+type DeleteExpiredSnapshotsCommand struct {
+}
+
 type GetDashboardSnapshotQuery struct {
 	Key string
 

+ 1 - 0
pkg/models/models.go

@@ -7,4 +7,5 @@ const (
 	GOOGLE
 	TWITTER
 	GENERIC
+	GRAFANANET
 )

+ 0 - 11
pkg/models/playlist.go

@@ -57,17 +57,6 @@ func (this PlaylistDashboard) TableName() string {
 type Playlists []*Playlist
 type PlaylistDashboards []*PlaylistDashboard
 
-//
-// DTOS
-//
-
-type PlaylistDashboardDto struct {
-	Id    int64  `json:"id"`
-	Slug  string `json:"slug"`
-	Title string `json:"title"`
-	Uri   string `json:"uri"`
-}
-
 //
 // COMMANDS
 //

+ 10 - 0
pkg/models/server.go

@@ -0,0 +1,10 @@
+package models
+
+import "context"
+
+type GrafanaServer interface {
+	context.Context
+
+	Start()
+	Shutdown(code int, reason string)
+}

+ 4 - 4
pkg/models/stats.go

@@ -1,10 +1,10 @@
 package models
 
 type SystemStats struct {
-	DashboardCount int
-	UserCount      int
-	OrgCount       int
-	PlaylistCount  int
+	DashboardCount int64
+	UserCount      int64
+	OrgCount       int64
+	PlaylistCount  int64
 }
 
 type DataSourceStats struct {

+ 10 - 9
pkg/models/user.go

@@ -44,15 +44,16 @@ func (u *User) NameOrFallback() string {
 // COMMANDS
 
 type CreateUserCommand struct {
-	Email         string
-	Login         string
-	Name          string
-	Company       string
-	OrgName       string
-	Password      string
-	EmailVerified bool
-	IsAdmin       bool
-	SkipOrgSetup  bool
+	Email          string
+	Login          string
+	Name           string
+	Company        string
+	OrgName        string
+	Password       string
+	EmailVerified  bool
+	IsAdmin        bool
+	SkipOrgSetup   bool
+	DefaultOrgRole string
 
 	Result User
 }

+ 1 - 0
pkg/plugins/datasource_plugin.go

@@ -6,6 +6,7 @@ type DataSourcePlugin struct {
 	FrontendPluginBase
 	Annotations bool   `json:"annotations"`
 	Metrics     bool   `json:"metrics"`
+	Alerting    bool   `json:"alerting"`
 	BuiltIn     bool   `json:"builtIn"`
 	Mixed       bool   `json:"mixed"`
 	App         string `json:"app"`

+ 6 - 1
pkg/plugins/frontend_plugin.go

@@ -43,7 +43,12 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
 	appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1)
 	fp.IncludedInAppId = app.Id
 	fp.BaseUrl = app.BaseUrl
-	fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
+
+	if isExternalPlugin(app.PluginDir) {
+		fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
+	} else {
+		fp.Module = util.JoinUrlFragments("app/plugins/app/"+app.Id, appSubPath) + "/module"
+	}
 }
 
 func (fp *FrontendPluginBase) handleModuleDefaults() {

+ 26 - 9
pkg/plugins/update_checker.go

@@ -9,6 +9,11 @@ import (
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/setting"
+	"github.com/hashicorp/go-version"
+)
+
+var (
+	httpClient http.Client = http.Client{Timeout: time.Duration(10 * time.Second)}
 )
 
 type GrafanaNetPlugin struct {
@@ -39,26 +44,23 @@ func StartPluginUpdateChecker() {
 }
 
 func getAllExternalPluginSlugs() string {
-	str := ""
-
+	var result []string
 	for _, plug := range Plugins {
 		if plug.IsCorePlugin {
 			continue
 		}
 
-		str += plug.Id + ","
+		result = append(result, plug.Id)
 	}
 
-	return str
+	return strings.Join(result, ",")
 }
 
 func checkForUpdates() {
 	log.Trace("Checking for updates")
 
-	client := http.Client{Timeout: time.Duration(5 * time.Second)}
-
 	pluginSlugs := getAllExternalPluginSlugs()
-	resp, err := client.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
+	resp, err := httpClient.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
 
 	if err != nil {
 		log.Trace("Failed to get plugins repo from grafana.net, %v", err.Error())
@@ -84,12 +86,20 @@ func checkForUpdates() {
 		for _, gplug := range gNetPlugins {
 			if gplug.Slug == plug.Id {
 				plug.GrafanaNetVersion = gplug.Version
-				plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion
+
+				plugVersion, err1 := version.NewVersion(plug.Info.Version)
+				gplugVersion, err2 := version.NewVersion(gplug.Version)
+
+				if err1 != nil || err2 != nil {
+					plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion
+				} else {
+					plug.GrafanaNetHasUpdate = plugVersion.LessThan(gplugVersion)
+				}
 			}
 		}
 	}
 
-	resp2, err := client.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json")
+	resp2, err := httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json")
 	if err != nil {
 		log.Trace("Failed to get latest.json repo from github: %v", err.Error())
 		return
@@ -116,4 +126,11 @@ func checkForUpdates() {
 		GrafanaLatestVersion = githubLatest.Stable
 		GrafanaHasUpdate = githubLatest.Stable != setting.BuildVersion
 	}
+
+	currVersion, err1 := version.NewVersion(setting.BuildVersion)
+	latestVersion, err2 := version.NewVersion(GrafanaLatestVersion)
+
+	if err1 == nil && err2 == nil {
+		GrafanaHasUpdate = currVersion.LessThan(latestVersion)
+	}
 }

+ 14 - 11
pkg/services/alerting/conditions/evaluator.go

@@ -5,6 +5,7 @@ import (
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/services/alerting"
+	"gopkg.in/guregu/null.v3"
 )
 
 var (
@@ -13,13 +14,13 @@ var (
 )
 
 type AlertEvaluator interface {
-	Eval(reducedValue *float64) bool
+	Eval(reducedValue null.Float) bool
 }
 
 type NoDataEvaluator struct{}
 
-func (e *NoDataEvaluator) Eval(reducedValue *float64) bool {
-	return reducedValue == nil
+func (e *NoDataEvaluator) Eval(reducedValue null.Float) bool {
+	return reducedValue.Valid == false
 }
 
 type ThresholdEvaluator struct {
@@ -43,16 +44,16 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu
 	return defaultEval, nil
 }
 
-func (e *ThresholdEvaluator) Eval(reducedValue *float64) bool {
-	if reducedValue == nil {
+func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
+	if reducedValue.Valid == false {
 		return false
 	}
 
 	switch e.Type {
 	case "gt":
-		return *reducedValue > e.Threshold
+		return reducedValue.Float64 > e.Threshold
 	case "lt":
-		return *reducedValue < e.Threshold
+		return reducedValue.Float64 < e.Threshold
 	}
 
 	return false
@@ -86,16 +87,18 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
 	return rangedEval, nil
 }
 
-func (e *RangedEvaluator) Eval(reducedValue *float64) bool {
-	if reducedValue == nil {
+func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
+	if reducedValue.Valid == false {
 		return false
 	}
 
+	floatValue := reducedValue.Float64
+
 	switch e.Type {
 	case "within_range":
-		return (e.Lower < *reducedValue && e.Upper > *reducedValue) || (e.Upper < *reducedValue && e.Lower > *reducedValue)
+		return (e.Lower < floatValue && e.Upper > floatValue) || (e.Upper < floatValue && e.Lower > floatValue)
 	case "outside_range":
-		return (e.Upper < *reducedValue && e.Lower < *reducedValue) || (e.Upper > *reducedValue && e.Lower > *reducedValue)
+		return (e.Upper < floatValue && e.Lower < floatValue) || (e.Upper > floatValue && e.Lower > floatValue)
 	}
 
 	return false

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

@@ -3,6 +3,8 @@ package conditions
 import (
 	"testing"
 
+	"gopkg.in/guregu/null.v3"
+
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	. "github.com/smartystreets/goconvey/convey"
 )
@@ -14,7 +16,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64)
 	evaluator, err := NewAlertEvaluator(jsonModel)
 	So(err, ShouldBeNil)
 
-	return evaluator.Eval(&reducedValue)
+	return evaluator.Eval(null.FloatFrom(reducedValue))
 }
 
 func TestEvalutors(t *testing.T) {
@@ -51,6 +53,6 @@ func TestEvalutors(t *testing.T) {
 		evaluator, err := NewAlertEvaluator(jsonModel)
 		So(err, ShouldBeNil)
 
-		So(evaluator.Eval(nil), ShouldBeTrue)
+		So(evaluator.Eval(null.FloatFromPtr(nil)), ShouldBeTrue)
 	})
 }

+ 44 - 12
pkg/services/alerting/conditions/query.go

@@ -2,6 +2,8 @@ package conditions
 
 import (
 	"fmt"
+	"strings"
+	"time"
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
@@ -32,7 +34,8 @@ type AlertQuery struct {
 }
 
 func (c *QueryCondition) Eval(context *alerting.EvalContext) {
-	seriesList, err := c.executeQuery(context)
+	timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
+	seriesList, err := c.executeQuery(context, timeRange)
 	if err != nil {
 		context.Error = err
 		return
@@ -43,21 +46,21 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
 		reducedValue := c.Reducer.Reduce(series)
 		evalMatch := c.Evaluator.Eval(reducedValue)
 
-		if reducedValue == nil {
+		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),
+				Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue.Float64),
 			})
 		}
 
 		if evalMatch {
 			context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
 				Metric: series.Name,
-				Value:  *reducedValue,
+				Value:  reducedValue.Float64,
 			})
 		}
 	}
@@ -66,7 +69,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
 	context.Firing = len(context.EvalMatches) > 0
 }
 
-func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeSeriesSlice, error) {
+func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) {
 	getDsInfo := &m.GetDataSourceByIdQuery{
 		Id:    c.Query.DatasourceId,
 		OrgId: context.Rule.OrgId,
@@ -76,7 +79,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeS
 		return nil, fmt.Errorf("Could not find datasource")
 	}
 
-	req := c.getRequestForAlertRule(getDsInfo.Result)
+	req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
 	result := make(tsdb.TimeSeriesSlice, 0)
 
 	resp, err := c.HandleRequest(req)
@@ -102,16 +105,13 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeS
 	return result, nil
 }
 
-func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource) *tsdb.Request {
+func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRange *tsdb.TimeRange) *tsdb.Request {
 	req := &tsdb.Request{
-		TimeRange: tsdb.TimeRange{
-			From: c.Query.From,
-			To:   c.Query.To,
-		},
+		TimeRange: timeRange,
 		Queries: []*tsdb.Query{
 			{
 				RefId: "A",
-				Query: c.Query.Model.Get("target").MustString(),
+				Model: c.Query.Model,
 				DataSource: &tsdb.DataSourceInfo{
 					Id:                datasource.Id,
 					Name:              datasource.Name,
@@ -141,6 +141,15 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro
 	condition.Query.Model = queryJson.Get("model")
 	condition.Query.From = queryJson.Get("params").MustArray()[1].(string)
 	condition.Query.To = queryJson.Get("params").MustArray()[2].(string)
+
+	if err := validateFromValue(condition.Query.From); err != nil {
+		return nil, err
+	}
+
+	if err := validateToValue(condition.Query.To); err != nil {
+		return nil, err
+	}
+
 	condition.Query.DatasourceId = queryJson.Get("datasourceId").MustInt64()
 
 	reducerJson := model.Get("reducer")
@@ -155,3 +164,26 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro
 	condition.Evaluator = evaluator
 	return &condition, nil
 }
+
+func validateFromValue(from string) error {
+	fromRaw := strings.Replace(from, "now-", "", 1)
+
+	_, err := time.ParseDuration("-" + fromRaw)
+	return err
+}
+
+func validateToValue(to string) error {
+	if to == "now" {
+		return nil
+	} else if strings.HasPrefix(to, "now-") {
+		withoutNow := strings.Replace(to, "now-", "", 1)
+
+		_, err := time.ParseDuration("-" + withoutNow)
+		if err == nil {
+			return nil
+		}
+	}
+
+	_, err := time.ParseDuration(to)
+	return err
+}

+ 14 - 19
pkg/services/alerting/conditions/query_test.go

@@ -3,6 +3,8 @@ package conditions
 import (
 	"testing"
 
+	null "gopkg.in/guregu/null.v3"
+
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
@@ -41,9 +43,8 @@ func TestQueryCondition(t *testing.T) {
 			})
 
 			Convey("should fire when avg is above 100", func() {
-				one := float64(120)
-				two := float64(0)
-				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})}
+				points := tsdb.NewTimeSeriesPointsFromArgs(120, 0)
+				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
 				ctx.exec()
 
 				So(ctx.result.Error, ShouldBeNil)
@@ -51,9 +52,8 @@ func TestQueryCondition(t *testing.T) {
 			})
 
 			Convey("Should not fire when avg is below 100", func() {
-				one := float64(90)
-				two := float64(0)
-				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})}
+				points := tsdb.NewTimeSeriesPointsFromArgs(90, 0)
+				ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
 				ctx.exec()
 
 				So(ctx.result.Error, ShouldBeNil)
@@ -61,11 +61,9 @@ func TestQueryCondition(t *testing.T) {
 			})
 
 			Convey("Should fire if only first serie matches", func() {
-				one := float64(120)
-				two := float64(0)
 				ctx.series = tsdb.TimeSeriesSlice{
-					tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}}),
-					tsdb.NewTimeSeries("test2", [][2]*float64{{&two, &two}}),
+					tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
+					tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)),
 				}
 				ctx.exec()
 
@@ -76,8 +74,8 @@ func TestQueryCondition(t *testing.T) {
 			Convey("Empty series", func() {
 				Convey("Should set NoDataFound both series are empty", func() {
 					ctx.series = tsdb.TimeSeriesSlice{
-						tsdb.NewTimeSeries("test1", [][2]*float64{}),
-						tsdb.NewTimeSeries("test2", [][2]*float64{}),
+						tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
+						tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()),
 					}
 					ctx.exec()
 
@@ -86,10 +84,9 @@ func TestQueryCondition(t *testing.T) {
 				})
 
 				Convey("Should set NoDataFound both series contains null", func() {
-					one := float64(120)
 					ctx.series = tsdb.TimeSeriesSlice{
-						tsdb.NewTimeSeries("test1", [][2]*float64{{nil, &one}}),
-						tsdb.NewTimeSeries("test2", [][2]*float64{{nil, &one}}),
+						tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
+						tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
 					}
 					ctx.exec()
 
@@ -98,11 +95,9 @@ func TestQueryCondition(t *testing.T) {
 				})
 
 				Convey("Should not set NoDataFound if one serie is empty", func() {
-					one := float64(120)
-					two := float64(0)
 					ctx.series = tsdb.TimeSeriesSlice{
-						tsdb.NewTimeSeries("test1", [][2]*float64{}),
-						tsdb.NewTimeSeries("test2", [][2]*float64{{&one, &two}}),
+						tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
+						tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
 					}
 					ctx.exec()
 

+ 16 - 15
pkg/services/alerting/conditions/reducer.go

@@ -4,19 +4,20 @@ import (
 	"math"
 
 	"github.com/grafana/grafana/pkg/tsdb"
+	"gopkg.in/guregu/null.v3"
 )
 
 type QueryReducer interface {
-	Reduce(timeSeries *tsdb.TimeSeries) *float64
+	Reduce(timeSeries *tsdb.TimeSeries) null.Float
 }
 
 type SimpleReducer struct {
 	Type string
 }
 
-func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
+func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
 	if len(series.Points) == 0 {
-		return nil
+		return null.FloatFromPtr(nil)
 	}
 
 	value := float64(0)
@@ -25,36 +26,36 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
 	switch s.Type {
 	case "avg":
 		for _, point := range series.Points {
-			if point[0] != nil {
-				value += *point[0]
+			if point[0].Valid {
+				value += point[0].Float64
 				allNull = false
 			}
 		}
 		value = value / float64(len(series.Points))
 	case "sum":
 		for _, point := range series.Points {
-			if point[0] != nil {
-				value += *point[0]
+			if point[0].Valid {
+				value += point[0].Float64
 				allNull = false
 			}
 		}
 	case "min":
 		value = math.MaxFloat64
 		for _, point := range series.Points {
-			if point[0] != nil {
+			if point[0].Valid {
 				allNull = false
-				if value > *point[0] {
-					value = *point[0]
+				if value > point[0].Float64 {
+					value = point[0].Float64
 				}
 			}
 		}
 	case "max":
 		value = -math.MaxFloat64
 		for _, point := range series.Points {
-			if point[0] != nil {
+			if point[0].Valid {
 				allNull = false
-				if value < *point[0] {
-					value = *point[0]
+				if value < point[0].Float64 {
+					value = point[0].Float64
 				}
 			}
 		}
@@ -64,10 +65,10 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
 	}
 
 	if allNull {
-		return nil
+		return null.FloatFromPtr(nil)
 	}
 
-	return &value
+	return null.FloatFrom(value)
 }
 
 func NewSimpleReducer(typ string) *SimpleReducer {

+ 11 - 14
pkg/services/alerting/conditions/reducer_test.go

@@ -10,44 +10,41 @@ import (
 func TestSimpleReducer(t *testing.T) {
 	Convey("Test simple reducer by calculating", t, func() {
 		Convey("avg", func() {
-			result := *testReducer("avg", 1, 2, 3)
+			result := testReducer("avg", 1, 2, 3)
 			So(result, ShouldEqual, float64(2))
 		})
 
 		Convey("sum", func() {
-			result := *testReducer("sum", 1, 2, 3)
+			result := testReducer("sum", 1, 2, 3)
 			So(result, ShouldEqual, float64(6))
 		})
 
 		Convey("min", func() {
-			result := *testReducer("min", 3, 2, 1)
+			result := testReducer("min", 3, 2, 1)
 			So(result, ShouldEqual, float64(1))
 		})
 
 		Convey("max", func() {
-			result := *testReducer("max", 1, 2, 3)
+			result := testReducer("max", 1, 2, 3)
 			So(result, ShouldEqual, float64(3))
 		})
 
 		Convey("count", func() {
-			result := *testReducer("count", 1, 2, 3000)
+			result := testReducer("count", 1, 2, 3000)
 			So(result, ShouldEqual, float64(3))
 		})
 	})
 }
 
-func testReducer(typ string, datapoints ...float64) *float64 {
+func testReducer(typ string, datapoints ...float64) float64 {
 	reducer := NewSimpleReducer(typ)
-	var timeserie [][2]*float64
-	dummieTimestamp := float64(521452145)
+	series := &tsdb.TimeSeries{
+		Name: "test time serie",
+	}
 
 	for idx := range datapoints {
-		timeserie = append(timeserie, [2]*float64{&datapoints[idx], &dummieTimestamp})
+		series.Points = append(series.Points, tsdb.NewTimePoint(datapoints[idx], 1234134))
 	}
 
-	tsdb := &tsdb.TimeSeries{
-		Name:   "test time serie",
-		Points: timeserie,
-	}
-	return reducer.Reduce(tsdb)
+	return reducer.Reduce(series).Float64
 }

+ 64 - 21
pkg/services/alerting/engine.go

@@ -1,10 +1,12 @@
 package alerting
 
 import (
+	"context"
 	"time"
 
 	"github.com/benbjohnson/clock"
 	"github.com/grafana/grafana/pkg/log"
+	"golang.org/x/sync/errgroup"
 )
 
 type Engine struct {
@@ -34,12 +36,19 @@ func NewEngine() *Engine {
 	return e
 }
 
-func (e *Engine) Start() {
-	e.log.Info("Starting Alerting Engine")
+func (e *Engine) Run(ctx context.Context) error {
+	e.log.Info("Initializing Alerting")
 
-	go e.alertingTicker()
-	go e.execDispatcher()
-	go e.resultDispatcher()
+	g, ctx := errgroup.WithContext(ctx)
+
+	g.Go(func() error { return e.alertingTicker(ctx) })
+	g.Go(func() error { return e.execDispatcher(ctx) })
+	g.Go(func() error { return e.resultDispatcher(ctx) })
+
+	err := g.Wait()
+
+	e.log.Info("Stopped Alerting", "reason", err)
+	return err
 }
 
 func (e *Engine) Stop() {
@@ -47,7 +56,7 @@ func (e *Engine) Stop() {
 	close(e.resultQueue)
 }
 
-func (e *Engine) alertingTicker() {
+func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
 	defer func() {
 		if err := recover(); err != nil {
 			e.log.Error("Scheduler Panic: stopping alertingTicker", "error", err, "stack", log.Stack(1))
@@ -58,6 +67,8 @@ func (e *Engine) alertingTicker() {
 
 	for {
 		select {
+		case <-grafanaCtx.Done():
+			return grafanaCtx.Err()
 		case tick := <-e.ticker.C:
 			// TEMP SOLUTION update rules ever tenth tick
 			if tickIndex%10 == 0 {
@@ -70,37 +81,69 @@ func (e *Engine) alertingTicker() {
 	}
 }
 
-func (e *Engine) execDispatcher() {
-	for job := range e.execQueue {
-		e.log.Debug("Starting executing alert rule", "alert id", job.Rule.Id)
-		go e.executeJob(job)
+func (e *Engine) execDispatcher(grafanaCtx context.Context) error {
+	for {
+		select {
+		case <-grafanaCtx.Done():
+			close(e.resultQueue)
+			return grafanaCtx.Err()
+		case job := <-e.execQueue:
+			go e.executeJob(grafanaCtx, job)
+		}
 	}
 }
 
-func (e *Engine) executeJob(job *Job) {
+func (e *Engine) executeJob(grafanaCtx context.Context, job *Job) error {
 	defer func() {
 		if err := recover(); err != nil {
 			e.log.Error("Execute Alert Panic", "error", err, "stack", log.Stack(1))
 		}
 	}()
 
-	job.Running = true
-	context := NewEvalContext(job.Rule)
-	e.evalHandler.Eval(context)
-	job.Running = false
+	done := make(chan *EvalContext, 1)
+	go func() {
+		job.Running = true
+		context := NewEvalContext(job.Rule)
+		e.evalHandler.Eval(context)
+		job.Running = false
+		done <- context
+		close(done)
+	}()
+
+	select {
+
+	case <-grafanaCtx.Done():
+		return grafanaCtx.Err()
+	case evalContext := <-done:
+		e.resultQueue <- evalContext
+	}
 
-	e.resultQueue <- context
+	return nil
 }
 
-func (e *Engine) resultDispatcher() {
+func (e *Engine) resultDispatcher(grafanaCtx context.Context) error {
+	for {
+		select {
+		case <-grafanaCtx.Done():
+			//handle all responses before shutting down.
+			for result := range e.resultQueue {
+				e.handleResponse(result)
+			}
+
+			return grafanaCtx.Err()
+		case result := <-e.resultQueue:
+			e.handleResponse(result)
+		}
+	}
+}
+
+func (e *Engine) handleResponse(result *EvalContext) {
 	defer func() {
 		if err := recover(); err != nil {
 			e.log.Error("Panic in resultDispatcher", "error", err, "stack", log.Stack(1))
 		}
 	}()
 
-	for result := range e.resultQueue {
-		e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing)
-		e.resultHandler.Handle(result)
-	}
+	e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing)
+	e.resultHandler.Handle(result)
 }

+ 2 - 11
pkg/services/alerting/eval_context.go

@@ -71,7 +71,7 @@ func (c *EvalContext) GetNotificationTitle() string {
 	return "[" + c.GetStateModel().Text + "] " + c.Rule.Name
 }
 
-func (c *EvalContext) getDashboardSlug() (string, error) {
+func (c *EvalContext) GetDashboardSlug() (string, error) {
 	if c.dashboardSlug != "" {
 		return c.dashboardSlug, nil
 	}
@@ -86,7 +86,7 @@ func (c *EvalContext) getDashboardSlug() (string, error) {
 }
 
 func (c *EvalContext) GetRuleUrl() (string, error) {
-	if slug, err := c.getDashboardSlug(); err != nil {
+	if slug, err := c.GetDashboardSlug(); err != nil {
 		return "", err
 	} else {
 		ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId)
@@ -94,15 +94,6 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
 	}
 }
 
-func (c *EvalContext) GetImageUrl() (string, error) {
-	if slug, err := c.getDashboardSlug(); err != nil {
-		return "", err
-	} else {
-		ruleUrl := fmt.Sprintf("%sdashboard-solo/db/%s?&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId)
-		return ruleUrl, nil
-	}
-}
-
 func NewEvalContext(rule *Rule) *EvalContext {
 	return &EvalContext{
 		StartTime:   time.Now(),

+ 1 - 1
pkg/services/alerting/eval_handler.go

@@ -20,7 +20,7 @@ type DefaultEvalHandler struct {
 func NewEvalHandler() *DefaultEvalHandler {
 	return &DefaultEvalHandler{
 		log:             log.New("alerting.evalHandler"),
-		alertJobTimeout: time.Second * 10,
+		alertJobTimeout: time.Second * 15,
 	}
 }
 

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

@@ -74,9 +74,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
 				continue
 			}
 
+			// backward compatability check, can be removed later
 			enabled, hasEnabled := jsonAlert.CheckGet("enabled")
-
-			if !hasEnabled || !enabled.MustBool() {
+			if hasEnabled && enabled.MustBool() == false {
 				continue
 			}
 

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

@@ -42,7 +42,6 @@ func TestAlertRuleExtraction(t *testing.T) {
               "name": "name1",
               "message": "desc1",
               "handler": 1,
-              "enabled": true,
               "frequency": "60s",
               "conditions": [
               {
@@ -66,7 +65,6 @@ func TestAlertRuleExtraction(t *testing.T) {
               "name": "name2",
               "message": "desc2",
               "handler": 0,
-              "enabled": true,
               "frequency": "60s",
               "severity": "warning",
               "conditions": [

+ 0 - 20
pkg/services/alerting/init/init.go

@@ -1,20 +0,0 @@
-package init
-
-import (
-	"github.com/grafana/grafana/pkg/services/alerting"
-	_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
-	_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
-	"github.com/grafana/grafana/pkg/setting"
-	_ "github.com/grafana/grafana/pkg/tsdb/graphite"
-)
-
-var engine *alerting.Engine
-
-func Init() {
-	if !setting.AlertingEnabled {
-		return
-	}
-
-	engine = alerting.NewEngine()
-	engine.Start()
-}

+ 13 - 9
pkg/services/alerting/notifier.go

@@ -2,6 +2,7 @@ package alerting
 
 import (
 	"errors"
+	"fmt"
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/imguploader"
@@ -60,20 +61,23 @@ func (n *RootNotifier) sendNotifications(notifiers []Notifier, context *EvalCont
 	}
 }
 
-func (n *RootNotifier) uploadImage(context *EvalContext) error {
-	uploader, _ := imguploader.NewImageUploader()
-
-	imageUrl, err := context.GetImageUrl()
+func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {
+	uploader, err := imguploader.NewImageUploader()
 	if err != nil {
 		return err
 	}
 
 	renderOpts := &renderer.RenderOpts{
-		Url:       imageUrl,
-		Width:     "800",
-		Height:    "400",
-		SessionId: "123",
-		Timeout:   "10",
+		Width:   "800",
+		Height:  "400",
+		Timeout: "30",
+		OrgId:   context.Rule.OrgId,
+	}
+
+	if slug, err := context.GetDashboardSlug(); err != nil {
+		return err
+	} else {
+		renderOpts.Path = fmt.Sprintf("dashboard-solo/db/%s?&panelId=%d", slug, context.Rule.PanelId)
 	}
 
 	if imagePath, err := renderer.RenderToPng(renderOpts); err != nil {

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

@@ -52,9 +52,8 @@ func (this *WebhookNotifier) Notify(context *alerting.EvalContext) {
 		bodyJSON.Set("rule_url", ruleUrl)
 	}
 
-	imageUrl, err := context.GetImageUrl()
-	if err == nil {
-		bodyJSON.Set("image_url", imageUrl)
+	if context.ImagePublicUrl != "" {
+		bodyJSON.Set("image_url", context.ImagePublicUrl)
 	}
 
 	body, _ := bodyJSON.MarshalJSON()

+ 85 - 0
pkg/services/cleanup/cleanup.go

@@ -0,0 +1,85 @@
+package cleanup
+
+import (
+	"context"
+	"io/ioutil"
+	"os"
+	"path"
+	"time"
+
+	"golang.org/x/sync/errgroup"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/log"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
+)
+
+type CleanUpService struct {
+	log log.Logger
+}
+
+func NewCleanUpService() *CleanUpService {
+	return &CleanUpService{
+		log: log.New("cleanup"),
+	}
+}
+
+func (service *CleanUpService) Run(ctx context.Context) error {
+	service.log.Info("Initializing CleanUpService")
+
+	g, _ := errgroup.WithContext(ctx)
+	g.Go(func() error { return service.start(ctx) })
+
+	err := g.Wait()
+	service.log.Info("Stopped CleanUpService", "reason", err)
+	return err
+}
+
+func (service *CleanUpService) start(ctx context.Context) error {
+	service.cleanUpTmpFiles()
+
+	ticker := time.NewTicker(time.Hour * 1)
+	for {
+		select {
+		case <-ticker.C:
+			service.cleanUpTmpFiles()
+			service.deleteExpiredSnapshots()
+		case <-ctx.Done():
+			return ctx.Err()
+		}
+	}
+}
+
+func (service *CleanUpService) cleanUpTmpFiles() {
+	if _, err := os.Stat(setting.ImagesDir); os.IsNotExist(err) {
+		return
+	}
+
+	files, err := ioutil.ReadDir(setting.ImagesDir)
+	if err != nil {
+		service.log.Error("Problem reading image dir", "error", err)
+		return
+	}
+
+	var toDelete []os.FileInfo
+	for _, file := range files {
+		if file.ModTime().AddDate(0, 0, 1).Before(time.Now()) {
+			toDelete = append(toDelete, file)
+		}
+	}
+
+	for _, file := range toDelete {
+		fullPath := path.Join(setting.ImagesDir, file.Name())
+		err := os.Remove(fullPath)
+		if err != nil {
+			service.log.Error("Failed to delete temp file", "file", file.Name(), "error", err)
+		}
+	}
+
+	service.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files))
+}
+
+func (service *CleanUpService) deleteExpiredSnapshots() {
+	bus.Dispatch(&m.DeleteExpiredSnapshotsCommand{})
+}

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

@@ -12,6 +12,7 @@ import (
 	"net/smtp"
 	"os"
 	"strings"
+	"time"
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/setting"
@@ -66,7 +67,7 @@ func sendToSmtpServer(recipients []string, msgContent []byte) error {
 		tlsconfig.Certificates = []tls.Certificate{cert}
 	}
 
-	conn, err := net.Dial("tcp", net.JoinHostPort(host, port))
+	conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), time.Second*10)
 	if err != nil {
 		return err
 	}

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

@@ -44,7 +44,7 @@ func sendWebRequest(webhook *Webhook) error {
 	webhookLog.Debug("Sending webhook", "url", webhook.Url)
 
 	client := http.Client{
-		Timeout: time.Duration(3 * time.Second),
+		Timeout: time.Duration(10 * time.Second),
 	}
 
 	request, err := http.NewRequest("POST", webhook.Url, bytes.NewReader([]byte(webhook.Body)))

+ 18 - 1
pkg/services/sqlstore/alert.go

@@ -17,6 +17,7 @@ func init() {
 	bus.AddHandler("sql", DeleteAlertById)
 	bus.AddHandler("sql", GetAllAlertQueryHandler)
 	bus.AddHandler("sql", SetAlertState)
+	bus.AddHandler("sql", GetAlertStatesForDashboard)
 }
 
 func GetAlertById(query *m.GetAlertByIdQuery) error {
@@ -92,7 +93,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
 		params = append(params, query.Limit)
 	}
 
-	sql.WriteString("ORDER BY name ASC")
+	sql.WriteString(" ORDER BY name ASC")
 
 	alerts := make([]*m.Alert, 0)
 	if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil {
@@ -241,3 +242,19 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
 		return nil
 	})
 }
+
+func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error {
+	var rawSql = `SELECT
+	                id,
+	                dashboard_id,
+	                panel_id,
+	                state,
+	                new_state_date
+	                FROM alert
+	                WHERE org_id = ? AND dashboard_id = ?`
+
+	query.Result = make([]*m.AlertStateInfoDTO, 0)
+	err := x.Sql(rawSql, query.OrgId, query.DashboardId).Find(&query.Result)
+
+	return err
+}

+ 2 - 1
pkg/services/sqlstore/alert_notification.go

@@ -66,7 +66,8 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro
 	sql.WriteString(` WHERE alert_notification.org_id = ?`)
 	params = append(params, query.OrgId)
 
-	sql.WriteString(` AND ((alert_notification.is_default = 1)`)
+	sql.WriteString(` AND ((alert_notification.is_default = ?)`)
+	params = append(params, dialect.BooleanStr(true))
 	if len(query.Ids) > 0 {
 		sql.WriteString(` OR alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
 		for _, v := range query.Ids {

+ 1 - 1
pkg/services/sqlstore/annotation.go

@@ -75,7 +75,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
 		query.Limit = 10
 	}
 
-	sql.WriteString(fmt.Sprintf("ORDER BY epoch DESC LIMIT %v", query.Limit))
+	sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
 
 	items := make([]*annotations.Item, 0)
 	if err := x.Sql(sql.String(), params...).Find(&items); err != nil {

+ 20 - 0
pkg/services/sqlstore/dashboard_snapshot.go

@@ -6,6 +6,7 @@ import (
 	"github.com/go-xorm/xorm"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
 )
 
 func init() {
@@ -13,6 +14,25 @@ func init() {
 	bus.AddHandler("sql", GetDashboardSnapshot)
 	bus.AddHandler("sql", DeleteDashboardSnapshot)
 	bus.AddHandler("sql", SearchDashboardSnapshots)
+	bus.AddHandler("sql", DeleteExpiredSnapshots)
+}
+
+func DeleteExpiredSnapshots(cmd *m.DeleteExpiredSnapshotsCommand) error {
+	return inTransaction(func(sess *xorm.Session) error {
+		var expiredCount int64 = 0
+
+		if setting.SnapShotRemoveExpired {
+			deleteExpiredSql := "DELETE FROM dashboard_snapshot WHERE expires < ?"
+			expiredResponse, err := x.Exec(deleteExpiredSql, time.Now)
+			if err != nil {
+				return err
+			}
+			expiredCount, _ = expiredResponse.RowsAffected()
+		}
+
+		sqlog.Debug("Deleted old/expired snaphots", "expired", expiredCount)
+		return nil
+	})
 }
 
 func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error {

+ 5 - 0
pkg/services/sqlstore/migrations/dashboard_mig.go

@@ -120,4 +120,9 @@ func addDashboardMigration(mg *Migrator) {
 	mg.AddMigration("Add index for plugin_id in dashboard", NewAddIndexMigration(dashboardV2, &Index{
 		Cols: []string{"org_id", "plugin_id"}, Type: IndexType,
 	}))
+
+	// dashboard_id index for dashboard_tag table
+	mg.AddMigration("Add index for dashboard_id in dashboard_tag", NewAddIndexMigration(dashboardTagV1, &Index{
+		Cols: []string{"dashboard_id"}, Type: IndexType,
+	}))
 }

+ 1 - 0
pkg/services/sqlstore/migrator/dialect.go

@@ -18,6 +18,7 @@ type Dialect interface {
 	SupportEngine() bool
 	LikeStr() string
 	Default(col *Column) string
+	BooleanStr(bool) string
 
 	CreateIndexSql(tableName string, index *Index) string
 	CreateTableSql(table *Table) string

+ 27 - 26
pkg/services/sqlstore/migrator/migrator.go

@@ -92,44 +92,45 @@ func (mg *Migrator) Start() error {
 
 		mg.Logger.Debug("Executing", "sql", sql)
 
-		if err := mg.exec(m); err != nil {
-			mg.Logger.Error("Exec failed", "error", err, "sql", sql)
-			record.Error = err.Error()
-			mg.x.Insert(&record)
+		err := mg.inTransaction(func(sess *xorm.Session) error {
+
+			if err := mg.exec(m, sess); err != nil {
+				mg.Logger.Error("Exec failed", "error", err, "sql", sql)
+				record.Error = err.Error()
+				sess.Insert(&record)
+				return err
+			} else {
+				record.Success = true
+				sess.Insert(&record)
+			}
+
+			return nil
+		})
+
+		if err != nil {
 			return err
-		} else {
-			record.Success = true
-			mg.x.Insert(&record)
 		}
 	}
 
 	return nil
 }
 
-func (mg *Migrator) exec(m Migration) error {
+func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
 	mg.Logger.Info("Executing migration", "id", m.Id())
 
-	err := mg.inTransaction(func(sess *xorm.Session) error {
-
-		condition := m.GetCondition()
-		if condition != nil {
-			sql, args := condition.Sql(mg.dialect)
-			results, err := sess.Query(sql, args...)
-			if err != nil || len(results) == 0 {
-				mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
-				return sess.Rollback()
-			}
+	condition := m.GetCondition()
+	if condition != nil {
+		sql, args := condition.Sql(mg.dialect)
+		results, err := sess.Query(sql, args...)
+		if err != nil || len(results) == 0 {
+			mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
+			return sess.Rollback()
 		}
+	}
 
-		_, err := sess.Exec(m.Sql(mg.dialect))
-		if err != nil {
-			mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
-			return err
-		}
-		return nil
-	})
-
+	_, err := sess.Exec(m.Sql(mg.dialect))
 	if err != nil {
+		mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
 		return err
 	}
 

+ 4 - 0
pkg/services/sqlstore/migrator/mysql_dialect.go

@@ -29,6 +29,10 @@ func (db *Mysql) AutoIncrStr() string {
 	return "AUTO_INCREMENT"
 }
 
+func (db *Mysql) BooleanStr(value bool) string {
+	return strconv.FormatBool(value)
+}
+
 func (db *Mysql) SqlType(c *Column) string {
 	var res string
 	switch c.Type {

+ 4 - 0
pkg/services/sqlstore/migrator/postgres_dialect.go

@@ -36,6 +36,10 @@ func (db *Postgres) AutoIncrStr() string {
 	return ""
 }
 
+func (db *Postgres) BooleanStr(value bool) string {
+	return strconv.FormatBool(value)
+}
+
 func (b *Postgres) Default(col *Column) string {
 	if col.Type == DB_Bool {
 		if col.Default == "0" {

+ 7 - 0
pkg/services/sqlstore/migrator/sqlite_dialect.go

@@ -29,6 +29,13 @@ func (db *Sqlite3) AutoIncrStr() string {
 	return "AUTOINCREMENT"
 }
 
+func (db *Sqlite3) BooleanStr(value bool) string {
+	if value {
+		return "1"
+	}
+	return "0"
+}
+
 func (db *Sqlite3) SqlType(c *Column) string {
 	switch c.Type {
 	case DB_Date, DB_DateTime, DB_TimeStamp, DB_Time:

+ 5 - 1
pkg/services/sqlstore/user.go

@@ -128,7 +128,11 @@ func CreateUser(cmd *m.CreateUserCommand) error {
 			}
 
 			if setting.AutoAssignOrg && !user.IsAdmin {
-				orgUser.Role = m.RoleType(setting.AutoAssignOrgRole)
+				if len(cmd.DefaultOrgRole) > 0 {
+					orgUser.Role = m.RoleType(cmd.DefaultOrgRole)
+				} else {
+					orgUser.Role = m.RoleType(setting.AutoAssignOrgRole)
+				}
 			}
 
 			if _, err = sess.Insert(&orgUser); err != nil {

+ 13 - 6
pkg/setting/setting.go

@@ -78,9 +78,11 @@ var (
 	DataProxyWhiteList    map[string]bool
 
 	// Snapshots
-	ExternalSnapshotUrl  string
-	ExternalSnapshotName string
-	ExternalEnabled      bool
+	ExternalSnapshotUrl   string
+	ExternalSnapshotName  string
+	ExternalEnabled       bool
+	SnapShotTTLDays       int
+	SnapShotRemoveExpired bool
 
 	// User settings
 	AllowUserSignUp    bool
@@ -90,7 +92,7 @@ var (
 	VerifyEmailEnabled bool
 	LoginHint          string
 	DefaultTheme       string
-	AllowUserPassLogin bool
+	DisableLoginForm   bool
 
 	// Http auth
 	AdminUser     string
@@ -495,6 +497,8 @@ func NewConfigContext(args *CommandLineArgs) error {
 	ExternalSnapshotUrl = snapshots.Key("external_snapshot_url").String()
 	ExternalSnapshotName = snapshots.Key("external_snapshot_name").String()
 	ExternalEnabled = snapshots.Key("external_enabled").MustBool(true)
+	SnapShotRemoveExpired = snapshots.Key("snapshot_remove_expired").MustBool(true)
+	SnapShotTTLDays = snapshots.Key("snapshot_TTL_days").MustInt(90)
 
 	//  read data source proxy white list
 	DataProxyWhiteList = make(map[string]bool)
@@ -514,7 +518,10 @@ func NewConfigContext(args *CommandLineArgs) error {
 	VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
 	LoginHint = users.Key("login_hint").String()
 	DefaultTheme = users.Key("default_theme").String()
-	AllowUserPassLogin = users.Key("allow_user_pass_login").MustBool(true)
+
+	// auth
+	auth := Cfg.Section("auth")
+	DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
 
 	// anonymous access
 	AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
@@ -556,7 +563,7 @@ func NewConfigContext(args *CommandLineArgs) error {
 		log.Warn("require_email_validation is enabled but smpt is disabled")
 	}
 
-	GrafanaNetUrl = Cfg.Section("grafana.net").Key("url").MustString("https://grafana.net")
+	GrafanaNetUrl = Cfg.Section("grafana_net").Key("url").MustString("https://grafana.net")
 
 	imageUploadingSection := Cfg.Section("external_image_storage")
 	ImageUploadProvider = imageUploadingSection.Key("provider").MustString("internal")

+ 2 - 3
pkg/setting/setting_oauth.go

@@ -8,12 +8,11 @@ type OAuthInfo struct {
 	AllowedDomains         []string
 	ApiUrl                 string
 	AllowSignup            bool
+	Name                   string
 }
 
 type OAuther struct {
-	GitHub, Google, Twitter, Generic bool
-	OAuthInfos                       map[string]*OAuthInfo
-	OAuthProviderName                string
+	OAuthInfos map[string]*OAuthInfo
 }
 
 var OAuthService *OAuther

+ 114 - 0
pkg/social/grafananet_oauth.go

@@ -0,0 +1,114 @@
+package social
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/grafana/grafana/pkg/models"
+
+	"golang.org/x/oauth2"
+)
+
+type SocialGrafanaNet struct {
+	*oauth2.Config
+	url                  string
+	allowedOrganizations []string
+	allowSignup          bool
+}
+
+func (s *SocialGrafanaNet) Type() int {
+	return int(models.GRAFANANET)
+}
+
+func (s *SocialGrafanaNet) IsEmailAllowed(email string) bool {
+	return true
+}
+
+func (s *SocialGrafanaNet) IsSignupAllowed() bool {
+	return s.allowSignup
+}
+
+func (s *SocialGrafanaNet) IsOrganizationMember(client *http.Client) bool {
+	if len(s.allowedOrganizations) == 0 {
+		return true
+	}
+
+	organizations, err := s.FetchOrganizations(client)
+	if err != nil {
+		return false
+	}
+
+	for _, allowedOrganization := range s.allowedOrganizations {
+		for _, organization := range organizations {
+			if organization == allowedOrganization {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+func (s *SocialGrafanaNet) FetchOrganizations(client *http.Client) ([]string, error) {
+	type Record struct {
+		Login string `json:"login"`
+	}
+
+	url := fmt.Sprintf(s.url + "/api/oauth2/user/orgs")
+	r, err := client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+
+	defer r.Body.Close()
+
+	var records []Record
+
+	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+		return nil, err
+	}
+
+	var logins = make([]string, len(records))
+	for i, record := range records {
+		logins[i] = record.Login
+	}
+
+	return logins, nil
+}
+
+func (s *SocialGrafanaNet) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
+	var data struct {
+		Id    int    `json:"id"`
+		Name  string `json:"login"`
+		Email string `json:"email"`
+		Role  string `json:"role"`
+	}
+
+	var err error
+	client := s.Client(oauth2.NoContext, token)
+	r, err := client.Get(s.url + "/api/oauth2/user")
+	if err != nil {
+		return nil, err
+	}
+
+	defer r.Body.Close()
+
+	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+		return nil, err
+	}
+
+	userInfo := &BasicUserInfo{
+		Identity: strconv.Itoa(data.Id),
+		Name:     data.Name,
+		Email:    data.Email,
+		Role:     data.Role,
+	}
+
+	if !s.IsOrganizationMember(client) {
+		return nil, ErrMissingOrganizationMembership
+	}
+
+	return userInfo, nil
+}

+ 27 - 13
pkg/social/social.go

@@ -15,6 +15,7 @@ type BasicUserInfo struct {
 	Email    string
 	Login    string
 	Company  string
+	Role     string
 }
 
 type SocialConnector interface {
@@ -36,7 +37,7 @@ func NewOAuthService() {
 	setting.OAuthService = &setting.OAuther{}
 	setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
 
-	allOauthes := []string{"github", "google", "generic_oauth"}
+	allOauthes := []string{"github", "google", "generic_oauth", "grafananet"}
 
 	for _, name := range allOauthes {
 		sec := setting.Cfg.Section("auth." + name)
@@ -50,6 +51,7 @@ func NewOAuthService() {
 			Enabled:        sec.Key("enabled").MustBool(),
 			AllowedDomains: sec.Key("allowed_domains").Strings(" "),
 			AllowSignup:    sec.Key("allow_sign_up").MustBool(),
+			Name:           sec.Key("name").MustString(name),
 		}
 
 		if !info.Enabled {
@@ -70,22 +72,18 @@ func NewOAuthService() {
 
 		// GitHub.
 		if name == "github" {
-			setting.OAuthService.GitHub = true
-			teamIds := sec.Key("team_ids").Ints(",")
-			allowedOrganizations := sec.Key("allowed_organizations").Strings(" ")
 			SocialMap["github"] = &SocialGithub{
 				Config:               &config,
 				allowedDomains:       info.AllowedDomains,
 				apiUrl:               info.ApiUrl,
 				allowSignup:          info.AllowSignup,
-				teamIds:              teamIds,
-				allowedOrganizations: allowedOrganizations,
+				teamIds:              sec.Key("team_ids").Ints(","),
+				allowedOrganizations: sec.Key("allowed_organizations").Strings(" "),
 			}
 		}
 
 		// Google.
 		if name == "google" {
-			setting.OAuthService.Google = true
 			SocialMap["google"] = &SocialGoogle{
 				Config: &config, allowedDomains: info.AllowedDomains,
 				apiUrl:      info.ApiUrl,
@@ -95,17 +93,33 @@ func NewOAuthService() {
 
 		// Generic - Uses the same scheme as Github.
 		if name == "generic_oauth" {
-			setting.OAuthService.Generic = true
-			setting.OAuthService.OAuthProviderName = sec.Key("oauth_provider_name").String()
-			teamIds := sec.Key("team_ids").Ints(",")
-			allowedOrganizations := sec.Key("allowed_organizations").Strings(" ")
 			SocialMap["generic_oauth"] = &GenericOAuth{
 				Config:               &config,
 				allowedDomains:       info.AllowedDomains,
 				apiUrl:               info.ApiUrl,
 				allowSignup:          info.AllowSignup,
-				teamIds:              teamIds,
-				allowedOrganizations: allowedOrganizations,
+				teamIds:              sec.Key("team_ids").Ints(","),
+				allowedOrganizations: sec.Key("allowed_organizations").Strings(" "),
+			}
+		}
+
+		if name == "grafananet" {
+			config := oauth2.Config{
+				ClientID:     info.ClientId,
+				ClientSecret: info.ClientSecret,
+				Endpoint: oauth2.Endpoint{
+					AuthURL:  setting.GrafanaNetUrl + "/oauth2/authorize",
+					TokenURL: setting.GrafanaNetUrl + "/api/oauth2/token",
+				},
+				RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name,
+				Scopes:      info.Scopes,
+			}
+
+			SocialMap["grafananet"] = &SocialGrafanaNet{
+				Config:               &config,
+				url:                  setting.GrafanaNetUrl,
+				allowSignup:          info.AllowSignup,
+				allowedOrganizations: sec.Key("allowed_organizations").Strings(" "),
 			}
 		}
 	}

+ 1 - 1
pkg/tsdb/batch.go

@@ -26,7 +26,7 @@ func (bg *Batch) process(context *QueryContext) {
 	if executor == nil {
 		bg.Done = true
 		result := &BatchResult{
-			Error:        errors.New("Could not find executor for data source type " + bg.Queries[0].DataSource.PluginId),
+			Error:        errors.New("Could not find executor for data source type: " + bg.Queries[0].DataSource.PluginId),
 			QueryResults: make(map[string]*QueryResult),
 		}
 		for _, query := range bg.Queries {

+ 7 - 6
pkg/tsdb/graphite/graphite.go

@@ -38,7 +38,7 @@ func init() {
 	}
 
 	HttpClient = http.Client{
-		Timeout:   time.Duration(10 * time.Second),
+		Timeout:   time.Duration(15 * time.Second),
 		Transport: tr,
 	}
 }
@@ -54,7 +54,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
 	}
 
 	for _, query := range queries {
-		formData["target"] = []string{query.Query}
+		formData["target"] = []string{query.Model.Get("target").MustString()}
 	}
 
 	if setting.Env == setting.DEV {
@@ -79,7 +79,8 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
 	}
 
 	result.QueryResults = make(map[string]*tsdb.QueryResult)
-	queryRes := &tsdb.QueryResult{}
+	queryRes := tsdb.NewQueryResult()
+
 	for _, series := range data {
 		queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
 			Name:   series.Target,
@@ -102,9 +103,9 @@ func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDT
 		return nil, err
 	}
 
-	if res.StatusCode == http.StatusUnauthorized {
-		glog.Info("Request is Unauthorized", "status", res.Status, "body", string(body))
-		return nil, fmt.Errorf("Request is Unauthorized status: %v body: %s", res.Status, string(body))
+	if res.StatusCode/100 != 2 {
+		glog.Info("Request failed", "status", res.Status, "body", string(body))
+		return nil, fmt.Errorf("Request failed status: %v", res.Status)
 	}
 
 	var data []TargetResponseDTO

+ 0 - 22
pkg/tsdb/graphite/graphite_test.go

@@ -1,23 +1 @@
 package graphite
-
-// func TestGraphite(t *testing.T) {
-//
-// 	Convey("When executing graphite query", t, func() {
-// 		executor := NewGraphiteExecutor(&tsdb.DataSourceInfo{
-// 			Url: "http://localhost:8080",
-// 		})
-//
-// 		queries := tsdb.QuerySlice{
-// 			&tsdb.Query{Query: "{\"target\": \"apps.backend.*.counters.requests.count\"}"},
-// 		}
-//
-// 		context := tsdb.NewQueryContext(queries, tsdb.TimeRange{})
-// 		result := executor.Execute(queries, context)
-// 		So(result.Error, ShouldBeNil)
-//
-// 		Convey("Should return series", func() {
-// 			So(result.QueryResults, ShouldNotBeEmpty)
-// 		})
-// 	})
-//
-// }

+ 4 - 2
pkg/tsdb/graphite/types.go

@@ -1,6 +1,8 @@
 package graphite
 
+import "github.com/grafana/grafana/pkg/tsdb"
+
 type TargetResponseDTO struct {
-	Target     string        `json:"target"`
-	DataPoints [][2]*float64 `json:"datapoints"`
+	Target     string                `json:"target"`
+	DataPoints tsdb.TimeSeriesPoints `json:"datapoints"`
 }

+ 48 - 14
pkg/tsdb/models.go

@@ -1,19 +1,31 @@
 package tsdb
 
-type TimeRange struct {
-	From string
-	To   string
+import (
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"gopkg.in/guregu/null.v3"
+)
+
+type Query struct {
+	RefId         string
+	Model         *simplejson.Json
+	Depends       []string
+	DataSource    *DataSourceInfo
+	Results       []*TimeSeries
+	Exclude       bool
+	MaxDataPoints int64
+	IntervalMs    int64
 }
 
+type QuerySlice []*Query
+
 type Request struct {
-	TimeRange     TimeRange
-	MaxDataPoints int
-	Queries       QuerySlice
+	TimeRange *TimeRange
+	Queries   QuerySlice
 }
 
 type Response struct {
-	BatchTimings []*BatchTiming
-	Results      map[string]*QueryResult
+	BatchTimings []*BatchTiming          `json:"timings"`
+	Results      map[string]*QueryResult `json:"results"`
 }
 
 type DataSourceInfo struct {
@@ -40,19 +52,41 @@ type BatchResult struct {
 }
 
 type QueryResult struct {
-	Error  error
-	RefId  string
-	Series TimeSeriesSlice
+	Error  error           `json:"error"`
+	RefId  string          `json:"refId"`
+	Series TimeSeriesSlice `json:"series"`
 }
 
 type TimeSeries struct {
-	Name   string        `json:"name"`
-	Points [][2]*float64 `json:"points"`
+	Name   string           `json:"name"`
+	Points TimeSeriesPoints `json:"points"`
 }
 
+type TimePoint [2]null.Float
+type TimeSeriesPoints []TimePoint
 type TimeSeriesSlice []*TimeSeries
 
-func NewTimeSeries(name string, points [][2]*float64) *TimeSeries {
+func NewQueryResult() *QueryResult {
+	return &QueryResult{
+		Series: make(TimeSeriesSlice, 0),
+	}
+}
+
+func NewTimePoint(value float64, timestamp float64) TimePoint {
+	return TimePoint{null.FloatFrom(value), null.FloatFrom(timestamp)}
+}
+
+func NewTimeSeriesPointsFromArgs(values ...float64) TimeSeriesPoints {
+	points := make(TimeSeriesPoints, 0)
+
+	for i := 0; i < len(values); i += 2 {
+		points = append(points, NewTimePoint(values[i], values[i+1]))
+	}
+
+	return points
+}
+
+func NewTimeSeries(name string, points TimeSeriesPoints) *TimeSeries {
 	return &TimeSeries{
 		Name:   name,
 		Points: points,

+ 161 - 0
pkg/tsdb/prometheus/prometheus.go

@@ -0,0 +1,161 @@
+package prometheus
+
+import (
+	"fmt"
+	"net/http"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/tsdb"
+	"github.com/prometheus/client_golang/api/prometheus"
+	pmodel "github.com/prometheus/common/model"
+	"golang.org/x/net/context"
+)
+
+type PrometheusExecutor struct {
+	*tsdb.DataSourceInfo
+}
+
+func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
+	return &PrometheusExecutor{dsInfo}
+}
+
+var (
+	plog       log.Logger
+	HttpClient http.Client
+)
+
+func init() {
+	plog = log.New("tsdb.prometheus")
+	tsdb.RegisterExecutor("prometheus", NewPrometheusExecutor)
+}
+
+func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) {
+	cfg := prometheus.Config{
+		Address: e.DataSourceInfo.Url,
+	}
+
+	client, err := prometheus.New(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	return prometheus.NewQueryAPI(client), nil
+}
+
+func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
+	result := &tsdb.BatchResult{}
+
+	client, err := e.getClient()
+	if err != nil {
+		return resultWithError(result, err)
+	}
+
+	query, err := parseQuery(queries, queryContext)
+	if err != nil {
+		return resultWithError(result, err)
+	}
+
+	timeRange := prometheus.Range{
+		Start: query.Start,
+		End:   query.End,
+		Step:  query.Step,
+	}
+
+	value, err := client.QueryRange(context.Background(), query.Expr, timeRange)
+
+	if err != nil {
+		return resultWithError(result, err)
+	}
+
+	queryResult, err := parseResponse(value, query)
+	if err != nil {
+		return resultWithError(result, err)
+	}
+	result.QueryResults = queryResult
+	return result
+}
+
+func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string {
+	reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`)
+
+	result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
+		ind := strings.Replace(strings.Replace(string(in), "{{", "", 1), "}}", "", 1)
+		if val, exists := metric[pmodel.LabelName(ind)]; exists {
+			return []byte(val)
+		}
+
+		return in
+	})
+
+	return string(result)
+}
+
+func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*PrometheusQuery, error) {
+	queryModel := queries[0]
+
+	expr, err := queryModel.Model.Get("expr").String()
+	if err != nil {
+		return nil, err
+	}
+
+	step, err := queryModel.Model.Get("step").Int64()
+	if err != nil {
+		return nil, err
+	}
+
+	format, err := queryModel.Model.Get("legendFormat").String()
+	if err != nil {
+		return nil, err
+	}
+
+	start, err := queryContext.TimeRange.ParseFrom()
+	if err != nil {
+		return nil, err
+	}
+
+	end, err := queryContext.TimeRange.ParseTo()
+	if err != nil {
+		return nil, err
+	}
+
+	return &PrometheusQuery{
+		Expr:         expr,
+		Step:         time.Second * time.Duration(step),
+		LegendFormat: format,
+		Start:        start,
+		End:          end,
+	}, nil
+}
+
+func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb.QueryResult, error) {
+	queryResults := make(map[string]*tsdb.QueryResult)
+	queryRes := tsdb.NewQueryResult()
+
+	data, ok := value.(pmodel.Matrix)
+	if !ok {
+		return queryResults, fmt.Errorf("Unsupported result format: %s", value.Type().String())
+	}
+
+	for _, v := range data {
+		series := tsdb.TimeSeries{
+			Name: formatLegend(v.Metric, query),
+		}
+
+		for _, k := range v.Values {
+			series.Points = append(series.Points, tsdb.NewTimePoint(float64(k.Value), float64(k.Timestamp.Unix()*1000)))
+		}
+
+		queryRes.Series = append(queryRes.Series, &series)
+	}
+
+	queryResults["A"] = queryRes
+	return queryResults, nil
+}
+
+func resultWithError(result *tsdb.BatchResult, err error) *tsdb.BatchResult {
+	result.Error = err
+	return result
+}

+ 26 - 0
pkg/tsdb/prometheus/prometheus_test.go

@@ -0,0 +1,26 @@
+package prometheus
+
+import (
+	"testing"
+
+	p "github.com/prometheus/common/model"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestPrometheus(t *testing.T) {
+	Convey("Prometheus", t, func() {
+
+		Convey("converting metric name", func() {
+			metric := map[p.LabelName]p.LabelValue{
+				p.LabelName("app"):    p.LabelValue("backend"),
+				p.LabelName("device"): p.LabelValue("mobile"),
+			}
+
+			query := &PrometheusQuery{
+				LegendFormat: "legend {{app}} {{device}} {{broken}}",
+			}
+
+			So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}")
+		})
+	})
+}

+ 11 - 0
pkg/tsdb/prometheus/types.go

@@ -0,0 +1,11 @@
+package prometheus
+
+import "time"
+
+type PrometheusQuery struct {
+	Expr         string
+	Step         time.Duration
+	LegendFormat string
+	Start        time.Time
+	End          time.Time
+}

+ 0 - 12
pkg/tsdb/query.go

@@ -1,12 +0,0 @@
-package tsdb
-
-type Query struct {
-	RefId      string
-	Query      string
-	Depends    []string
-	DataSource *DataSourceInfo
-	Results    []*TimeSeries
-	Exclude    bool
-}
-
-type QuerySlice []*Query

+ 2 - 2
pkg/tsdb/query_context.go

@@ -3,7 +3,7 @@ package tsdb
 import "sync"
 
 type QueryContext struct {
-	TimeRange   TimeRange
+	TimeRange   *TimeRange
 	Queries     QuerySlice
 	Results     map[string]*QueryResult
 	ResultsChan chan *BatchResult
@@ -11,7 +11,7 @@ type QueryContext struct {
 	BatchWaits  sync.WaitGroup
 }
 
-func NewQueryContext(queries QuerySlice, timeRange TimeRange) *QueryContext {
+func NewQueryContext(queries QuerySlice, timeRange *TimeRange) *QueryContext {
 	return &QueryContext{
 		TimeRange:   timeRange,
 		Queries:     queries,

+ 130 - 0
pkg/tsdb/testdata/scenarios.go

@@ -0,0 +1,130 @@
+package testdata
+
+import (
+	"math/rand"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/tsdb"
+)
+
+type ScenarioHandler func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult
+
+type Scenario struct {
+	Id          string          `json:"id"`
+	Name        string          `json:"name"`
+	StringInput string          `json:"stringOption"`
+	Description string          `json:"description"`
+	Handler     ScenarioHandler `json:"-"`
+}
+
+var ScenarioRegistry map[string]*Scenario
+
+func init() {
+	ScenarioRegistry = make(map[string]*Scenario)
+	logger := log.New("tsdb.testdata")
+
+	logger.Debug("Initializing TestData Scenario")
+
+	registerScenario(&Scenario{
+		Id:   "random_walk",
+		Name: "Random Walk",
+
+		Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
+			timeWalkerMs := context.TimeRange.GetFromAsMsEpoch()
+			to := context.TimeRange.GetToAsMsEpoch()
+
+			series := newSeriesForQuery(query)
+
+			points := make(tsdb.TimeSeriesPoints, 0)
+			walker := rand.Float64() * 100
+
+			for i := int64(0); i < 10000 && timeWalkerMs < to; i++ {
+				points = append(points, tsdb.NewTimePoint(walker, float64(timeWalkerMs)))
+
+				walker += rand.Float64() - 0.5
+				timeWalkerMs += query.IntervalMs
+			}
+
+			series.Points = points
+
+			queryRes := tsdb.NewQueryResult()
+			queryRes.Series = append(queryRes.Series, series)
+			return queryRes
+		},
+	})
+
+	registerScenario(&Scenario{
+		Id:   "no_data_points",
+		Name: "No Data Points",
+		Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
+			return tsdb.NewQueryResult()
+		},
+	})
+
+	registerScenario(&Scenario{
+		Id:   "datapoints_outside_range",
+		Name: "Datapoints Outside Range",
+		Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
+			queryRes := tsdb.NewQueryResult()
+
+			series := newSeriesForQuery(query)
+			outsideTime := context.TimeRange.MustGetFrom().Add(-1*time.Hour).Unix() * 1000
+
+			series.Points = append(series.Points, tsdb.NewTimePoint(10, float64(outsideTime)))
+			queryRes.Series = append(queryRes.Series, series)
+
+			return queryRes
+		},
+	})
+
+	registerScenario(&Scenario{
+		Id:          "csv_metric_values",
+		Name:        "CSV Metric Values",
+		StringInput: "1,20,90,30,5,0",
+		Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult {
+			queryRes := tsdb.NewQueryResult()
+
+			stringInput := query.Model.Get("stringInput").MustString()
+			values := []float64{}
+			for _, strVal := range strings.Split(stringInput, ",") {
+				if val, err := strconv.ParseFloat(strVal, 64); err == nil {
+					values = append(values, val)
+				}
+			}
+
+			if len(values) == 0 {
+				return queryRes
+			}
+
+			series := newSeriesForQuery(query)
+			startTime := context.TimeRange.GetFromAsMsEpoch()
+			endTime := context.TimeRange.GetToAsMsEpoch()
+			step := (endTime - startTime) / int64(len(values)-1)
+
+			for _, val := range values {
+				series.Points = append(series.Points, tsdb.NewTimePoint(val, float64(startTime)))
+				startTime += step
+			}
+
+			queryRes.Series = append(queryRes.Series, series)
+
+			return queryRes
+		},
+	})
+}
+
+func registerScenario(scenario *Scenario) {
+	ScenarioRegistry[scenario.Id] = scenario
+}
+
+func newSeriesForQuery(query *tsdb.Query) *tsdb.TimeSeries {
+	alias := query.Model.Get("alias").MustString("")
+	if alias == "" {
+		alias = query.RefId + "-series"
+	}
+
+	return &tsdb.TimeSeries{Name: alias}
+}

+ 39 - 0
pkg/tsdb/testdata/testdata.go

@@ -0,0 +1,39 @@
+package testdata
+
+import (
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/tsdb"
+)
+
+type TestDataExecutor struct {
+	*tsdb.DataSourceInfo
+	log log.Logger
+}
+
+func NewTestDataExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
+	return &TestDataExecutor{
+		DataSourceInfo: dsInfo,
+		log:            log.New("tsdb.testdata"),
+	}
+}
+
+func init() {
+	tsdb.RegisterExecutor("grafana-testdata-datasource", NewTestDataExecutor)
+}
+
+func (e *TestDataExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
+	result := &tsdb.BatchResult{}
+	result.QueryResults = make(map[string]*tsdb.QueryResult)
+
+	for _, query := range queries {
+		scenarioId := query.Model.Get("scenarioId").MustString("random_walk")
+		if scenario, exist := ScenarioRegistry[scenarioId]; exist {
+			result.QueryResults[query.RefId] = scenario.Handler(query, context)
+			result.QueryResults[query.RefId].RefId = query.RefId
+		} else {
+			e.log.Error("Scenario not found", "scenarioId", scenarioId)
+		}
+	}
+
+	return result
+}

+ 90 - 0
pkg/tsdb/time_range.go

@@ -0,0 +1,90 @@
+package tsdb
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+)
+
+func NewTimeRange(from, to string) *TimeRange {
+	return &TimeRange{
+		From: from,
+		To:   to,
+		Now:  time.Now(),
+	}
+}
+
+type TimeRange struct {
+	From string
+	To   string
+	Now  time.Time
+}
+
+func (tr *TimeRange) GetFromAsMsEpoch() int64 {
+	return tr.MustGetFrom().UnixNano() / int64(time.Millisecond)
+}
+
+func (tr *TimeRange) GetToAsMsEpoch() int64 {
+	return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
+}
+
+func (tr *TimeRange) MustGetFrom() time.Time {
+	if res, err := tr.ParseFrom(); err != nil {
+		return time.Unix(0, 0)
+	} else {
+		return res
+	}
+}
+
+func (tr *TimeRange) MustGetTo() time.Time {
+	if res, err := tr.ParseTo(); err != nil {
+		return time.Unix(0, 0)
+	} else {
+		return res
+	}
+}
+
+func tryParseUnixMsEpoch(val string) (time.Time, bool) {
+	if val, err := strconv.ParseInt(val, 10, 64); err == nil {
+		seconds := val / 1000
+		nano := (val - seconds*1000) * 1000000
+		return time.Unix(seconds, nano), true
+	}
+	return time.Time{}, false
+}
+
+func (tr *TimeRange) ParseFrom() (time.Time, error) {
+	if res, ok := tryParseUnixMsEpoch(tr.From); ok {
+		return res, nil
+	}
+
+	fromRaw := strings.Replace(tr.From, "now-", "", 1)
+	diff, err := time.ParseDuration("-" + fromRaw)
+	if err != nil {
+		return time.Time{}, err
+	}
+
+	return tr.Now.Add(diff), nil
+}
+
+func (tr *TimeRange) ParseTo() (time.Time, error) {
+	if tr.To == "now" {
+		return tr.Now, nil
+	} else if strings.HasPrefix(tr.To, "now-") {
+		withoutNow := strings.Replace(tr.To, "now-", "", 1)
+
+		diff, err := time.ParseDuration("-" + withoutNow)
+		if err != nil {
+			return time.Time{}, nil
+		}
+
+		return tr.Now.Add(diff), nil
+	}
+
+	if res, ok := tryParseUnixMsEpoch(tr.To); ok {
+		return res, nil
+	}
+
+	return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To)
+}

+ 95 - 0
pkg/tsdb/time_range_test.go

@@ -0,0 +1,95 @@
+package tsdb
+
+import (
+	"testing"
+	"time"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestTimeRange(t *testing.T) {
+	Convey("Time range", t, func() {
+
+		now := time.Now()
+
+		Convey("Can parse 5m, now", func() {
+			tr := TimeRange{
+				From: "5m",
+				To:   "now",
+				Now:  now,
+			}
+
+			Convey("5m ago ", func() {
+				fiveMinAgo, _ := time.ParseDuration("-5m")
+				expected := now.Add(fiveMinAgo)
+
+				res, err := tr.ParseFrom()
+				So(err, ShouldBeNil)
+				So(res.Unix(), ShouldEqual, expected.Unix())
+			})
+
+			Convey("now ", func() {
+				res, err := tr.ParseTo()
+				So(err, ShouldBeNil)
+				So(res.Unix(), ShouldEqual, now.Unix())
+			})
+		})
+
+		Convey("Can parse 5h, now-10m", func() {
+			tr := TimeRange{
+				From: "5h",
+				To:   "now-10m",
+				Now:  now,
+			}
+
+			Convey("5h ago ", func() {
+				fiveHourAgo, _ := time.ParseDuration("-5h")
+				expected := now.Add(fiveHourAgo)
+
+				res, err := tr.ParseFrom()
+				So(err, ShouldBeNil)
+				So(res.Unix(), ShouldEqual, expected.Unix())
+			})
+
+			Convey("now-10m ", func() {
+				fiveMinAgo, _ := time.ParseDuration("-10m")
+				expected := now.Add(fiveMinAgo)
+				res, err := tr.ParseTo()
+				So(err, ShouldBeNil)
+				So(res.Unix(), ShouldEqual, expected.Unix())
+			})
+		})
+
+		Convey("can parse unix epocs", func() {
+			var err error
+			tr := TimeRange{
+				From: "1474973725473",
+				To:   "1474975757930",
+				Now:  now,
+			}
+
+			res, err := tr.ParseFrom()
+			So(err, ShouldBeNil)
+			So(res.UnixNano()/int64(time.Millisecond), ShouldEqual, 1474973725473)
+
+			res, err = tr.ParseTo()
+			So(err, ShouldBeNil)
+			So(res.UnixNano()/int64(time.Millisecond), ShouldEqual, 1474975757930)
+		})
+
+		Convey("Cannot parse asdf", func() {
+			var err error
+			tr := TimeRange{
+				From: "asdf",
+				To:   "asdf",
+				Now:  now,
+			}
+
+			_, err = tr.ParseFrom()
+			So(err, ShouldNotBeNil)
+
+			_, err = tr.ParseTo()
+			So(err, ShouldNotBeNil)
+		})
+	})
+}

+ 15 - 15
pkg/tsdb/tsdb_test.go

@@ -14,9 +14,9 @@ func TestMetricQuery(t *testing.T) {
 		Convey("Given 3 queries for 2 data sources", func() {
 			request := &Request{
 				Queries: QuerySlice{
-					{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
-					{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
-					{RefId: "C", Query: "asd", DataSource: &DataSourceInfo{Id: 2}},
+					{RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
+					{RefId: "B", DataSource: &DataSourceInfo{Id: 1}},
+					{RefId: "C", DataSource: &DataSourceInfo{Id: 2}},
 				},
 			}
 
@@ -31,9 +31,9 @@ func TestMetricQuery(t *testing.T) {
 		Convey("Given query 2 depends on query 1", func() {
 			request := &Request{
 				Queries: QuerySlice{
-					{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1}},
-					{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 2}},
-					{RefId: "C", Query: "#A / #B", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}},
+					{RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
+					{RefId: "B", DataSource: &DataSourceInfo{Id: 2}},
+					{RefId: "C", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}},
 				},
 			}
 
@@ -55,7 +55,7 @@ func TestMetricQuery(t *testing.T) {
 	Convey("When executing request with one query", t, func() {
 		req := &Request{
 			Queries: QuerySlice{
-				{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
+				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 			},
 		}
 
@@ -74,8 +74,8 @@ func TestMetricQuery(t *testing.T) {
 	Convey("When executing one request with two queries from same data source", t, func() {
 		req := &Request{
 			Queries: QuerySlice{
-				{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
-				{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
+				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
+				{RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
 			},
 		}
 
@@ -100,9 +100,9 @@ func TestMetricQuery(t *testing.T) {
 	Convey("When executing one request with three queries from different datasources", t, func() {
 		req := &Request{
 			Queries: QuerySlice{
-				{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
-				{RefId: "B", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
-				{RefId: "C", Query: "asd", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}},
+				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
+				{RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
+				{RefId: "C", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}},
 			},
 		}
 
@@ -117,7 +117,7 @@ func TestMetricQuery(t *testing.T) {
 	Convey("When query uses data source of unknown type", t, func() {
 		req := &Request{
 			Queries: QuerySlice{
-				{RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}},
+				{RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}},
 			},
 		}
 
@@ -129,10 +129,10 @@ func TestMetricQuery(t *testing.T) {
 		req := &Request{
 			Queries: QuerySlice{
 				{
-					RefId: "A", Query: "asd", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"},
+					RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"},
 				},
 				{
-					RefId: "B", Query: "#A / 2", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"},
+					RefId: "B", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"},
 				},
 			},
 		}

+ 14 - 9
public/app/core/controllers/login_ctrl.js

@@ -1,11 +1,18 @@
 define([
   'angular',
+  'lodash',
   '../core_module',
   'app/core/config',
 ],
-function (angular, coreModule, config) {
+function (angular, _, coreModule, config) {
   'use strict';
 
+  var failCodes = {
+    "1000": "Required team membership not fulfilled",
+    "1001": "Required organization membership not fulfilled",
+    "1002": "Required email domain not fulfilled",
+  };
+
   coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) {
     $scope.formModel = {
       user: '',
@@ -15,12 +22,10 @@ function (angular, coreModule, config) {
 
     contextSrv.sidemenu = false;
 
-    $scope.googleAuthEnabled = config.googleAuthEnabled;
-    $scope.githubAuthEnabled = config.githubAuthEnabled;
-    $scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled || config.genericOAuthEnabled;
-    $scope.allowUserPassLogin = config.allowUserPassLogin;
-    $scope.genericOAuthEnabled = config.genericOAuthEnabled;
-    $scope.oauthProviderName = config.oauthProviderName;
+    $scope.oauth = config.oauth;
+    $scope.oauthEnabled = _.keys(config.oauth).length > 0;
+
+    $scope.disableLoginForm = config.disableLoginForm;
     $scope.disableUserSignUp = config.disableUserSignUp;
     $scope.loginHint     = config.loginHint;
 
@@ -31,8 +36,8 @@ function (angular, coreModule, config) {
       $scope.$watch("loginMode", $scope.loginModeChanged);
 
       var params = $location.search();
-      if (params.failedMsg) {
-        $scope.appEvent('alert-warning', ['Login Failed', params.failedMsg]);
+      if (params.failCode) {
+        $scope.appEvent('alert-warning', ['Login Failed', failCodes[params.failCode]]);
         delete params.failedMsg;
         $location.search(params);
       }

+ 2 - 0
public/app/core/core.ts

@@ -41,6 +41,7 @@ import 'app/core/routes/routes';
 import './filters/filters';
 import coreModule from './core_module';
 import appEvents from './app_events';
+import colors from './utils/colors';
 
 
 export {
@@ -60,4 +61,5 @@ export {
   dashboardSelector,
   queryPartEditorDirective,
   WizardFlow,
+  colors,
 };

+ 16 - 6
public/app/core/directives/metric_segment.js

@@ -23,10 +23,10 @@ function (_, $, coreModule) {
         getOptions: "&",
         onChange: "&",
       },
-      link: function($scope, elem, attrs) {
+      link: function($scope, elem) {
         var $input = $(inputTemplate);
-        var $button = $(attrs.styleMode === 'select' ? selectTemplate : linkTemplate);
         var segment = $scope.segment;
+        var $button = $(segment.selectMode ? selectTemplate : linkTemplate);
         var options = null;
         var cancelBlur = null;
         var linkMode = true;
@@ -136,7 +136,7 @@ function (_, $, coreModule) {
 
         $button.click(function() {
           options = null;
-          $input.css('width', ($button.width() + 16) + 'px');
+          $input.css('width', (Math.max($button.width(), 80) + 16) + 'px');
 
           $button.hide();
           $input.show();
@@ -170,6 +170,7 @@ function (_, $, coreModule) {
       },
       link: {
         pre: function postLink($scope, elem, attrs) {
+          var cachedOptions;
 
           $scope.valueToSegment = function(value) {
             var option = _.find($scope.options, {value: value});
@@ -177,7 +178,9 @@ function (_, $, coreModule) {
               cssClass: attrs.cssClass,
               custom: attrs.custom,
               value: option ? option.text : value,
+              selectMode: attrs.selectMode,
             };
+
             return uiSegmentSrv.newSegment(segment);
           };
 
@@ -188,13 +191,20 @@ function (_, $, coreModule) {
               });
               return $q.when(optionSegments);
             } else {
-              return $scope.getOptions();
+              return $scope.getOptions().then(function(options) {
+                cachedOptions = options;
+                return _.map(options, function(option) {
+                  return uiSegmentSrv.newSegment({value: option.text});
+                });
+              });
             }
           };
 
           $scope.onSegmentChange = function() {
-            if ($scope.options) {
-              var option = _.find($scope.options, {text: $scope.segment.value});
+            var options = $scope.options || cachedOptions;
+
+            if (options) {
+              var option = _.find(options, {text: $scope.segment.value});
               if (option && option.value !== $scope.property) {
                 $scope.property = option.value;
               } else if (attrs.custom !== 'false') {

+ 1 - 1
public/app/core/directives/value_select_dropdown.js

@@ -236,7 +236,7 @@ function (angular, _, coreModule) {
         var inputEl = elem.find('input');
 
         function openDropdown() {
-          inputEl.css('width', Math.max(linkEl.width(), 30) + 'px');
+          inputEl.css('width', Math.max(linkEl.width(), 80) + 'px');
 
           inputEl.show();
           linkEl.hide();

+ 4 - 0
public/app/core/services/backend_srv.ts

@@ -114,6 +114,10 @@ export class BackendSrv {
     var requestIsLocal = options.url.indexOf('/') === 0;
     var firstAttempt = options.retry === 0;
 
+    if (requestIsLocal && !options.hasSubUrl && options.retry === 0) {
+      options.url = config.appSubUrl + options.url;
+    }
+
     if (requestIsLocal && options.headers && options.headers.Authorization) {
       options.headers['X-DS-Authorization'] = options.headers.Authorization;
       delete options.headers.Authorization;

+ 1 - 0
public/app/core/services/context_srv.ts

@@ -9,6 +9,7 @@ export class User {
   isGrafanaAdmin: any;
   isSignedIn: any;
   orgRole: any;
+  timezone: string;
 
   constructor() {
     if (config.bootData.user) {

+ 1 - 0
public/app/core/services/segment_srv.js

@@ -28,6 +28,7 @@ function (angular, _, coreModule) {
       this.type = options.type;
       this.fake = options.fake;
       this.value = options.value;
+      this.selectMode = options.selectMode;
       this.type = options.type;
       this.expandable = options.expandable;
       this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));

+ 3 - 0
public/app/core/time_series2.ts

@@ -31,6 +31,8 @@ export default class TimeSeries {
   allIsZero: boolean;
   decimals: number;
   scaledDecimals: number;
+  hasMsResolution: boolean;
+  isOutsideRange: boolean;
 
   lines: any;
   bars: any;
@@ -54,6 +56,7 @@ export default class TimeSeries {
     this.stats = {};
     this.legend = true;
     this.unit = opts.unit;
+    this.hasMsResolution = this.isMsResolutionNeeded();
   }
 
   applySeriesOverrides(overrides) {

+ 12 - 0
public/app/core/utils/colors.ts

@@ -0,0 +1,12 @@
+
+
+export default [
+  "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0",
+  "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
+  "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
+  "#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93",
+  "#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7",
+  "#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B",
+  "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
+];
+

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