Просмотр исходного кода

Merge branch 'master' into feature/add_es_alerting

Penghan Wang 7 лет назад
Родитель
Сommit
a21afbe9a6
100 измененных файлов с 8183 добавлено и 1628 удалено
  1. 0 0
      .circleci/config.yml
  2. 9 1
      CHANGELOG.md
  3. 4 3
      Gopkg.lock
  4. 2 4
      Gopkg.toml
  5. 6 0
      conf/defaults.ini
  6. 3 0
      conf/sample.ini
  7. 323 57
      docker/blocks/mssql_tests/dashboard.json
  8. 2350 0
      docker/blocks/mysql_tests/dashboard.json
  9. 2324 0
      docker/blocks/postgres_tests/dashboard.json
  10. 22 13
      docs/sources/administration/provisioning.md
  11. 5 0
      docs/sources/installation/configuration.md
  12. 68 1
      docs/sources/reference/templating.md
  13. 1 1
      emails/templates/layouts/default.html
  14. 2 2
      package.json
  15. 9 0
      pkg/api/admin.go
  16. 7 7
      pkg/api/admin_users.go
  17. 39 39
      pkg/api/alerting.go
  18. 47 47
      pkg/api/annotations.go
  19. 4 4
      pkg/api/annotations_test.go
  20. 24 24
      pkg/api/api.go
  21. 10 10
      pkg/api/apikey.go
  22. 2 2
      pkg/api/app_routes.go
  23. 13 9
      pkg/api/common.go
  24. 1 1
      pkg/api/common_test.go
  25. 52 55
      pkg/api/dashboard.go
  26. 16 16
      pkg/api/dashboard_permission.go
  27. 10 10
      pkg/api/dashboard_snapshot.go
  28. 13 13
      pkg/api/dashboard_test.go
  29. 5 5
      pkg/api/dataproxy.go
  30. 39 40
      pkg/api/datasources.go
  31. 2 2
      pkg/api/dtos/prefs.go
  32. 15 15
      pkg/api/folder.go
  33. 10 10
      pkg/api/folder_permission.go
  34. 5 5
      pkg/api/folder_permission_test.go
  35. 11 11
      pkg/api/folder_test.go
  36. 15 12
      pkg/api/http_server.go
  37. 15 14
      pkg/api/index.go
  38. 7 7
      pkg/api/login.go
  39. 14 14
      pkg/api/metrics.go
  40. 28 28
      pkg/api/org.go
  41. 39 40
      pkg/api/org_invite.go
  42. 23 23
      pkg/api/org_users.go
  43. 8 8
      pkg/api/password.go
  44. 17 17
      pkg/api/playlist.go
  45. 31 33
      pkg/api/playlist_play.go
  46. 14 14
      pkg/api/pluginproxy/ds_proxy.go
  47. 8 8
      pkg/api/pluginproxy/ds_proxy_test.go
  48. 9 9
      pkg/api/pluginproxy/pluginproxy.go
  49. 55 53
      pkg/api/plugins.go
  50. 13 13
      pkg/api/preferences.go
  51. 14 14
      pkg/api/quota.go
  52. 8 8
      pkg/api/search.go
  53. 13 13
      pkg/api/signup.go
  54. 7 7
      pkg/api/stars.go
  55. 16 16
      pkg/api/team.go
  56. 10 10
      pkg/api/team_members.go
  57. 51 51
      pkg/api/user.go
  58. 2 2
      pkg/cmd/grafana-server/server.go
  59. 3 3
      pkg/middleware/auth.go
  60. 52 49
      pkg/middleware/auth_proxy.go
  61. 6 6
      pkg/middleware/dashboard_redirect.go
  62. 10 10
      pkg/middleware/dashboard_redirect_test.go
  63. 1 1
      pkg/middleware/middleware_test.go
  64. 7 7
      pkg/middleware/recovery_test.go
  65. 7 7
      pkg/middleware/render_auth.go
  66. 2 2
      pkg/middleware/session.go
  67. 64 26
      pkg/services/alerting/engine.go
  68. 118 0
      pkg/services/alerting/engine_test.go
  69. 31 0
      pkg/services/alerting/eval_context.go
  70. 68 1
      pkg/services/alerting/eval_context_test.go
  71. 0 34
      pkg/services/alerting/eval_handler.go
  72. 0 70
      pkg/services/alerting/eval_handler_test.go
  73. 1 0
      pkg/services/alerting/test_rule.go
  74. 4 4
      pkg/services/dashboards/folder_service.go
  75. 4 4
      pkg/services/dashboards/folder_service_test.go
  76. 26 3
      pkg/services/session/mysql.go
  77. 3 2
      pkg/services/session/session.go
  78. 1 1
      pkg/services/sqlstore/alert.go
  79. 1 0
      pkg/services/sqlstore/dashboard_folder_test.go
  80. 22 44
      pkg/services/sqlstore/dashboard_version.go
  81. 21 1
      pkg/services/sqlstore/dashboard_version_test.go
  82. 1 1
      pkg/services/sqlstore/migrator/migrator.go
  83. 3 0
      pkg/services/sqlstore/org_test.go
  84. 9 4
      pkg/services/sqlstore/quota.go
  85. 2 2
      pkg/services/sqlstore/quota_test.go
  86. 20 12
      pkg/services/sqlstore/sqlstore.go
  87. 1 0
      pkg/services/sqlstore/user.go
  88. 4 1
      pkg/setting/setting.go
  89. 5 10
      pkg/tsdb/mssql/macros.go
  90. 9 16
      pkg/tsdb/mssql/macros_test.go
  91. 4 9
      pkg/tsdb/mssql/mssql.go
  92. 388 198
      pkg/tsdb/mssql/mssql_test.go
  93. 1 1
      pkg/tsdb/mysql/macros.go
  94. 99 96
      pkg/tsdb/mysql/mysql.go
  95. 624 96
      pkg/tsdb/mysql/mysql_test.go
  96. 4 11
      pkg/tsdb/postgres/postgres.go
  97. 603 75
      pkg/tsdb/postgres/postgres_test.go
  98. 28 0
      pkg/tsdb/sql_engine.go
  99. 46 0
      pkg/tsdb/sql_engine_test.go
  100. 10 0
      pkg/tsdb/time_range.go

+ 0 - 0
circle.yml → .circleci/config.yml


+ 9 - 1
CHANGELOG.md

@@ -1,20 +1,28 @@
 # 5.1.0 (unreleased)
 # 5.1.0 (unreleased)
 
 
+* **MSSQL**: New Microsoft SQL Server data source [#10093](https://github.com/grafana/grafana/pull/10093), [#11298](https://github.com/grafana/grafana/pull/11298), thx [@linuxchips](https://github.com/linuxchips)
 * **Prometheus**: The heatmap panel now support Prometheus histograms [#10009](https://github.com/grafana/grafana/issues/10009)
 * **Prometheus**: The heatmap panel now support Prometheus histograms [#10009](https://github.com/grafana/grafana/issues/10009)
 * **Postgres/MySQL**: Ability to insert 0s or nulls for missing intervals [#9487](https://github.com/grafana/grafana/issues/9487), thanks [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL**: Ability to insert 0s or nulls for missing intervals [#9487](https://github.com/grafana/grafana/issues/9487), thanks [@svenklemm](https://github.com/svenklemm)
+* **Graph**: Align left and right Y-axes to one level [#1271](https://github.com/grafana/grafana/issues/1271) &  [#2740](https://github.com/grafana/grafana/issues/2740) thx [@ilgizar](https://github.com/ilgizar)
 * **Graph**: Thresholds for Right Y axis [#7107](https://github.com/grafana/grafana/issues/7107), thx [@ilgizar](https://github.com/ilgizar)
 * **Graph**: Thresholds for Right Y axis [#7107](https://github.com/grafana/grafana/issues/7107), thx [@ilgizar](https://github.com/ilgizar)
 * **Graph**: Support multiple series stacking in histogram mode [#8151](https://github.com/grafana/grafana/issues/8151), thx [@mtanda](https://github.com/mtanda)
 * **Graph**: Support multiple series stacking in histogram mode [#8151](https://github.com/grafana/grafana/issues/8151), thx [@mtanda](https://github.com/mtanda)
 * **Alerting**: Pausing/un alerts now updates new_state_date [#10942](https://github.com/grafana/grafana/pull/10942)
 * **Alerting**: Pausing/un alerts now updates new_state_date [#10942](https://github.com/grafana/grafana/pull/10942)
 * **Alerting**: Support Pagerduty notification channel using Pagerduty V2 API [#10531](https://github.com/grafana/grafana/issues/10531), thx [@jbaublitz](https://github.com/jbaublitz)
 * **Alerting**: Support Pagerduty notification channel using Pagerduty V2 API [#10531](https://github.com/grafana/grafana/issues/10531), thx [@jbaublitz](https://github.com/jbaublitz)
 * **Templating**: Add comma templating format [#10632](https://github.com/grafana/grafana/issues/10632), thx [@mtanda](https://github.com/mtanda)
 * **Templating**: Add comma templating format [#10632](https://github.com/grafana/grafana/issues/10632), thx [@mtanda](https://github.com/mtanda)
 * **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda)
 * **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda)
+* **Alerting**: Add support for retries on alert queries [#5855](https://github.com/grafana/grafana/issues/5855), thx [@Thib17](https://github.com/Thib17)
+* **Server**: Adjust permissions of unix socket [#11343](https://github.com/grafana/grafana/pull/11343), thx [@corny](https://github.com/corny)
 
 
 ### Minor
 ### Minor
 * **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes)
 * **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes)
 * **Cloudwatch**: Support high resolution metrics [#10925](https://github.com/grafana/grafana/pull/10925), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: Support high resolution metrics [#10925](https://github.com/grafana/grafana/pull/10925), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: Add dimension filtering to CloudWatch `dimension_values()` [#10029](https://github.com/grafana/grafana/issues/10029), thx [@willyhutw](https://github.com/willyhutw)
 * **Cloudwatch**: Add dimension filtering to CloudWatch `dimension_values()` [#10029](https://github.com/grafana/grafana/issues/10029), thx [@willyhutw](https://github.com/willyhutw)
-* **Units**: Second to HH:mm:ss formatter [#11107](https://github.com/grafana/grafana/issues/11107), thx [@gladdiologist](https://github.com/gladdiologist) 
+* **Units**: Second to HH:mm:ss formatter [#11107](https://github.com/grafana/grafana/issues/11107), thx [@gladdiologist](https://github.com/gladdiologist)
 * **Singlestat**: Add color to prefix and postfix in singlestat panel [#11143](https://github.com/grafana/grafana/pull/11143), thx [@ApsOps](https://github.com/ApsOps)
 * **Singlestat**: Add color to prefix and postfix in singlestat panel [#11143](https://github.com/grafana/grafana/pull/11143), thx [@ApsOps](https://github.com/ApsOps)
+* **Dashboards**: Version cleanup fails on old databases with many entries [#11278](https://github.com/grafana/grafana/issues/11278)
+
+# 5.0.4 (unreleased)
+* **Dashboard** Fixed bug where collapsed panels could not be directly linked to/renderer [#11114](https://github.com/grafana/grafana/issues/11114) & [#11086](https://github.com/grafana/grafana/issues/11086)
 
 
 # 5.0.3 (2018-03-16)
 # 5.0.3 (2018-03-16)
 * **Mysql**: Mysql panic occurring occasionally upon Grafana dashboard access (a bigger patch than the one in 5.0.2) [#11155](https://github.com/grafana/grafana/issues/11155)
 * **Mysql**: Mysql panic occurring occasionally upon Grafana dashboard access (a bigger patch than the one in 5.0.2) [#11155](https://github.com/grafana/grafana/issues/11155)

+ 4 - 3
Gopkg.lock

@@ -153,7 +153,6 @@
   packages = [
   packages = [
     ".",
     ".",
     "memcache",
     "memcache",
-    "mysql",
     "postgres",
     "postgres",
     "redis"
     "redis"
   ]
   ]
@@ -179,12 +178,14 @@
 [[projects]]
 [[projects]]
   name = "github.com/go-xorm/core"
   name = "github.com/go-xorm/core"
   packages = ["."]
   packages = ["."]
-  revision = "e8409d73255791843585964791443dbad877058c"
+  revision = "da1adaf7a28ca792961721a34e6e04945200c890"
+  version = "v0.5.7"
 
 
 [[projects]]
 [[projects]]
   name = "github.com/go-xorm/xorm"
   name = "github.com/go-xorm/xorm"
   packages = ["."]
   packages = ["."]
-  revision = "6687a2b4e824f4d87f2d65060ec5cb0d896dff1e"
+  revision = "1933dd69e294c0a26c0266637067f24dbb25770c"
+  version = "v0.6.4"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"

+ 2 - 4
Gopkg.toml

@@ -85,13 +85,11 @@ ignored = [
 
 
 [[constraint]]
 [[constraint]]
   name = "github.com/go-xorm/core"
   name = "github.com/go-xorm/core"
-  revision = "e8409d73255791843585964791443dbad877058c"
-  #version = "0.5.7" //keeping this since we would rather depend on version then commit
+  version = "0.5.7"
 
 
 [[constraint]]
 [[constraint]]
   name = "github.com/go-xorm/xorm"
   name = "github.com/go-xorm/xorm"
-  revision = "6687a2b4e824f4d87f2d65060ec5cb0d896dff1e"
-  #version = "0.6.4" //keeping this since we would rather depend on version then commit
+  version = "0.6.4"
 
 
 [[constraint]]
 [[constraint]]
   name = "github.com/gorilla/websocket"
   name = "github.com/gorilla/websocket"

+ 6 - 0
conf/defaults.ini

@@ -82,6 +82,9 @@ max_idle_conn = 2
 # Max conn setting default is 0 (mean not set)
 # Max conn setting default is 0 (mean not set)
 max_open_conn =
 max_open_conn =
 
 
+# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours)
+conn_max_lifetime = 14400
+
 # Set to true to log the sql calls and execution times.
 # Set to true to log the sql calls and execution times.
 log_queries =
 log_queries =
 
 
@@ -125,6 +128,9 @@ cookie_secure = false
 session_life_time = 86400
 session_life_time = 86400
 gc_interval_time = 86400
 gc_interval_time = 86400
 
 
+# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours)
+conn_max_lifetime = 14400
+
 #################################### Data proxy ###########################
 #################################### Data proxy ###########################
 [dataproxy]
 [dataproxy]
 
 

+ 3 - 0
conf/sample.ini

@@ -90,6 +90,9 @@
 # Max conn setting default is 0 (mean not set)
 # Max conn setting default is 0 (mean not set)
 ;max_open_conn =
 ;max_open_conn =
 
 
+# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours)
+;conn_max_lifetime = 14400
+
 # Set to true to log the sql calls and execution times.
 # Set to true to log the sql calls and execution times.
 log_queries =
 log_queries =
 
 

+ 323 - 57
docker/blocks/mssql_tests/dashboard.json

@@ -53,7 +53,7 @@
         "iconColor": "#6ed0e0",
         "iconColor": "#6ed0e0",
         "limit": 100,
         "limit": 100,
         "name": "Deploys",
         "name": "Deploys",
-        "rawQuery": "SELECT\n   time_sec as time,\n   description as [text],\n   tags\n  FROM [event]\n  WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n  ORDER BY 1 ASC\n  ",
+        "rawQuery": "SELECT\n   $__time(time_sec),\n   description as [text],\n   tags\n  FROM [event]\n  WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n  ORDER BY 1 ASC\n  ",
         "showIn": 0,
         "showIn": 0,
         "tags": [],
         "tags": [],
         "type": "tags"
         "type": "tags"
@@ -65,7 +65,7 @@
         "iconColor": "rgba(255, 96, 96, 1)",
         "iconColor": "rgba(255, 96, 96, 1)",
         "limit": 100,
         "limit": 100,
         "name": "Tickets",
         "name": "Tickets",
-        "rawQuery": "SELECT\n   time_sec as time,\n   description as [text],\n   tags\n  FROM [event]\n  WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n  ORDER BY 1 ASC\n  ",
+        "rawQuery": "SELECT\n   $__time(time_sec),\n   description as [text],\n   tags\n  FROM [event]\n  WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n  ORDER BY 1 ASC\n  ",
         "showIn": 0,
         "showIn": 0,
         "tags": [],
         "tags": [],
         "type": "tags"
         "type": "tags"
@@ -76,8 +76,20 @@
         "hide": false,
         "hide": false,
         "iconColor": "#7eb26d",
         "iconColor": "#7eb26d",
         "limit": 100,
         "limit": 100,
-        "name": "Metric Values",
-        "rawQuery": "SELECT \n  time, \n  measurement as text, \n  '' as tags\nFROM\n  metric_values \nORDER BY 1",
+        "name": "Metric Values timeEpoch macro",
+        "rawQuery": "SELECT \n  $__timeEpoch(time), \n  measurement as text, \n  '' as tags\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time)\nORDER BY 1",
+        "showIn": 0,
+        "tags": [],
+        "type": "tags"
+      },
+      {
+        "datasource": "${DS_MSSQL_TEST}",
+        "enable": false,
+        "hide": false,
+        "iconColor": "#1f78c1",
+        "limit": 100,
+        "name": "Metric Values native time",
+        "rawQuery": "SELECT \n  time, \n  measurement as text, \n  '' as tags\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time)\nORDER BY 1",
         "showIn": 0,
         "showIn": 0,
         "tags": [],
         "tags": [],
         "type": "tags"
         "type": "tags"
@@ -88,7 +100,7 @@
   "gnetId": null,
   "gnetId": null,
   "graphTooltip": 0,
   "graphTooltip": 0,
   "id": null,
   "id": null,
-  "iteration": 1521481503341,
+  "iteration": 1521715844826,
   "links": [],
   "links": [],
   "panels": [
   "panels": [
     {
     {
@@ -138,6 +150,222 @@
       "transform": "table",
       "transform": "table",
       "type": "table"
       "type": "table"
     },
     },
+    {
+      "columns": [],
+      "datasource": "${DS_MSSQL_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 0,
+        "y": 4
+      },
+      "id": 32,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT cast(null as bigint) as time",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "cast(null as bigint) as time",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_MSSQL_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 6,
+        "y": 4
+      },
+      "id": 33,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT cast(null as datetime) as time",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "cast(null as datetime) as time",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_MSSQL_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 12,
+        "y": 4
+      },
+      "id": 34,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT GETDATE() as time",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "GETDATE() as time",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_MSSQL_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 18,
+        "y": 4
+      },
+      "id": 35,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT GETUTCDATE() as time",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "GETUTCDATE() as time",
+      "transform": "table",
+      "type": "table"
+    },
     {
     {
       "aliasColors": {},
       "aliasColors": {},
       "bars": false,
       "bars": false,
@@ -149,7 +377,7 @@
         "h": 9,
         "h": 9,
         "w": 8,
         "w": 8,
         "x": 0,
         "x": 0,
-        "y": 4
+        "y": 7
       },
       },
       "id": 7,
       "id": 7,
       "legend": {
       "legend": {
@@ -228,7 +456,7 @@
         "h": 9,
         "h": 9,
         "w": 8,
         "w": 8,
         "x": 8,
         "x": 8,
-        "y": 4
+        "y": 7
       },
       },
       "id": 9,
       "id": 9,
       "legend": {
       "legend": {
@@ -307,7 +535,7 @@
         "h": 9,
         "h": 9,
         "w": 8,
         "w": 8,
         "x": 16,
         "x": 16,
-        "y": 4
+        "y": 7
       },
       },
       "id": 10,
       "id": 10,
       "legend": {
       "legend": {
@@ -386,7 +614,7 @@
         "h": 9,
         "h": 9,
         "w": 8,
         "w": 8,
         "x": 0,
         "x": 0,
-        "y": 13
+        "y": 16
       },
       },
       "id": 16,
       "id": 16,
       "legend": {
       "legend": {
@@ -465,7 +693,7 @@
         "h": 9,
         "h": 9,
         "w": 8,
         "w": 8,
         "x": 8,
         "x": 8,
-        "y": 13
+        "y": 16
       },
       },
       "id": 12,
       "id": 12,
       "legend": {
       "legend": {
@@ -544,7 +772,7 @@
         "h": 9,
         "h": 9,
         "w": 8,
         "w": 8,
         "x": 16,
         "x": 16,
-        "y": 13
+        "y": 16
       },
       },
       "id": 13,
       "id": 13,
       "legend": {
       "legend": {
@@ -623,7 +851,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 0,
         "x": 0,
-        "y": 22
+        "y": 25
       },
       },
       "id": 27,
       "id": 27,
       "legend": {
       "legend": {
@@ -655,13 +883,13 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  measurement + ' - value one' as metric, \n  avg(valueOne) as valueOne\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time)\nGROUP BY \n  $__timeGroup(time, '$summarize'), \n  measurement \nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  measurement + ' - value one' as metric, \n  avg(valueOne) as valueOne\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__timeGroup(time, '$summarize'), \n  measurement \nORDER BY 1",
           "refId": "A"
           "refId": "A"
         },
         },
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  measurement + ' - value two' as metric, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values \nGROUP BY \n  $__timeGroup(time, '$summarize'), \n  measurement \nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  measurement + ' - value two' as metric, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values\nWHERE\n  $__timeFilter(time) AND\n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__timeGroup(time, '$summarize'), \n  measurement \nORDER BY 1",
           "refId": "B"
           "refId": "B"
         }
         }
       ],
       ],
@@ -712,7 +940,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 12,
         "x": 12,
-        "y": 22
+        "y": 25
       },
       },
       "id": 5,
       "id": 5,
       "legend": {
       "legend": {
@@ -734,7 +962,19 @@
       "pointradius": 3,
       "pointradius": 3,
       "points": false,
       "points": false,
       "renderer": "flot",
       "renderer": "flot",
-      "seriesOverrides": [],
+      "seriesOverrides": [
+        {
+          "alias": "MovingAverageValueOne",
+          "dashes": true,
+          "lines": false
+        },
+        {
+          "alias": "MovingAverageValueTwo",
+          "dashes": true,
+          "lines": false,
+          "yaxis": 1
+        }
+      ],
       "spaceLength": 10,
       "spaceLength": 10,
       "stack": false,
       "stack": false,
       "steppedLine": false,
       "steppedLine": false,
@@ -742,8 +982,14 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  avg(valueOne) as valueOne, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values \nGROUP BY \n  $__timeGroup(time, '$summarize')\nORDER BY 1",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  avg(valueOne) as valueOne, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values \nWHERE \n  $__timeFilter(time) AND \n  ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n  $__timeGroup(time, '$summarize')\nORDER BY 1",
           "refId": "A"
           "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  time,\n  avg(valueOne) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueOne,\n  avg(valueTwo) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueTwo\nFROM\n  metric_values \nWHERE \n  $__timeFilter(time) AND \n  ($metric = 'ALL' OR measurement = $metric)\nORDER BY 1",
+          "refId": "B"
         }
         }
       ],
       ],
       "thresholds": [],
       "thresholds": [],
@@ -793,7 +1039,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 0,
         "x": 0,
-        "y": 30
+        "y": 33
       },
       },
       "id": 4,
       "id": 4,
       "legend": {
       "legend": {
@@ -825,13 +1071,13 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         },
         },
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "B"
           "refId": "B"
         }
         }
       ],
       ],
@@ -882,7 +1128,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 12,
         "x": 12,
-        "y": 30
+        "y": 33
       },
       },
       "id": 28,
       "id": 28,
       "legend": {
       "legend": {
@@ -963,7 +1209,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 0,
         "x": 0,
-        "y": 38
+        "y": 41
       },
       },
       "id": 19,
       "id": 19,
       "legend": {
       "legend": {
@@ -995,13 +1241,13 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         },
         },
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "B"
           "refId": "B"
         }
         }
       ],
       ],
@@ -1052,7 +1298,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 12,
         "x": 12,
-        "y": 38
+        "y": 41
       },
       },
       "id": 18,
       "id": 18,
       "legend": {
       "legend": {
@@ -1082,7 +1328,7 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         }
         }
       ],
       ],
@@ -1133,7 +1379,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 0,
         "x": 0,
-        "y": 46
+        "y": 49
       },
       },
       "id": 17,
       "id": 17,
       "legend": {
       "legend": {
@@ -1165,13 +1411,13 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         },
         },
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "B"
           "refId": "B"
         }
         }
       ],
       ],
@@ -1222,7 +1468,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 12,
         "x": 12,
-        "y": 46
+        "y": 49
       },
       },
       "id": 20,
       "id": 20,
       "legend": {
       "legend": {
@@ -1252,7 +1498,7 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         }
         }
       ],
       ],
@@ -1303,7 +1549,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 0,
         "x": 0,
-        "y": 54
+        "y": 57
       },
       },
       "id": 29,
       "id": 29,
       "legend": {
       "legend": {
@@ -1335,7 +1581,7 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "DECLARE \n  @from int = $__unixEpochFrom(),\n  @to int = $__unixEpochTo()\n  \nEXEC dbo.sp_test_epoch @from, @to",
+          "rawSql": "DECLARE\n  @from int = $__unixEpochFrom(), \n  @to int = $__unixEpochTo(), \n  @interval nvarchar(50) = '$summarize', \n  @metric nvarchar(200) = $metric\n  \nEXEC dbo.sp_test_epoch @from, @to, @interval, @metric",
           "refId": "A"
           "refId": "A"
         }
         }
       ],
       ],
@@ -1386,7 +1632,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 12,
         "x": 12,
-        "y": 54
+        "y": 57
       },
       },
       "id": 30,
       "id": 30,
       "legend": {
       "legend": {
@@ -1418,7 +1664,7 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "DECLARE \n  @from datetime = $__timeFrom(),\n  @to datetime = $__timeTo()\n  \nEXEC dbo.sp_test_datetime @from, @to",
+          "rawSql": "DECLARE\n  @from datetime = $__timeFrom(), \n  @to datetime = $__timeTo(), \n  @interval nvarchar(50) = '$summarize', \n  @metric nvarchar(200) = $metric\n  \nEXEC dbo.sp_test_datetime @from, @to, @interval, @metric",
           "refId": "A"
           "refId": "A"
         }
         }
       ],
       ],
@@ -1469,7 +1715,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 0,
         "x": 0,
-        "y": 62
+        "y": 65
       },
       },
       "id": 14,
       "id": 14,
       "legend": {
       "legend": {
@@ -1499,13 +1745,13 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         },
         },
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "B"
           "refId": "B"
         }
         }
       ],
       ],
@@ -1559,7 +1805,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 12,
         "x": 12,
-        "y": 62
+        "y": 65
       },
       },
       "id": 15,
       "id": 15,
       "legend": {
       "legend": {
@@ -1589,7 +1835,7 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         }
         }
       ],
       ],
@@ -1642,7 +1888,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 0,
         "x": 0,
-        "y": 70
+        "y": 73
       },
       },
       "id": 25,
       "id": 25,
       "legend": {
       "legend": {
@@ -1672,13 +1918,13 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         },
         },
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "B"
           "refId": "B"
         }
         }
       ],
       ],
@@ -1732,7 +1978,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 12,
         "x": 12,
-        "y": 70
+        "y": 73
       },
       },
       "id": 22,
       "id": 22,
       "legend": {
       "legend": {
@@ -1762,7 +2008,7 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         }
         }
       ],
       ],
@@ -1815,7 +2061,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 0,
         "x": 0,
-        "y": 78
+        "y": 81
       },
       },
       "id": 21,
       "id": 21,
       "legend": {
       "legend": {
@@ -1845,13 +2091,13 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         },
         },
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "B"
           "refId": "B"
         }
         }
       ],
       ],
@@ -1905,7 +2151,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 12,
         "x": 12,
-        "y": 78
+        "y": 81
       },
       },
       "id": 26,
       "id": 26,
       "legend": {
       "legend": {
@@ -1935,7 +2181,7 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         }
         }
       ],
       ],
@@ -1988,7 +2234,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 0,
         "x": 0,
-        "y": 86
+        "y": 89
       },
       },
       "id": 23,
       "id": 23,
       "legend": {
       "legend": {
@@ -2018,13 +2264,13 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         },
         },
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "B"
           "refId": "B"
         }
         }
       ],
       ],
@@ -2078,7 +2324,7 @@
         "h": 8,
         "h": 8,
         "w": 12,
         "w": 12,
         "x": 12,
         "x": 12,
-        "y": 86
+        "y": 89
       },
       },
       "id": 24,
       "id": 24,
       "legend": {
       "legend": {
@@ -2108,7 +2354,7 @@
         {
         {
           "alias": "",
           "alias": "",
           "format": "time_series",
           "format": "time_series",
-          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1",
           "refId": "A"
           "refId": "A"
         }
         }
       ],
       ],
@@ -2157,6 +2403,26 @@
   "tags": [],
   "tags": [],
   "templating": {
   "templating": {
     "list": [
     "list": [
+      {
+        "allValue": "'ALL'",
+        "current": {},
+        "datasource": "${DS_MSSQL_TEST}",
+        "hide": 0,
+        "includeAll": true,
+        "label": "Metric",
+        "multi": false,
+        "name": "metric",
+        "options": [],
+        "query": "SELECT DISTINCT measurement FROM metric_values",
+        "refresh": 1,
+        "regex": "",
+        "sort": 0,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
       {
       {
         "auto": false,
         "auto": false,
         "auto_count": 30,
         "auto_count": 30,
@@ -2208,7 +2474,7 @@
   },
   },
   "time": {
   "time": {
     "from": "2018-03-15T12:30:00.000Z",
     "from": "2018-03-15T12:30:00.000Z",
-    "to": "2018-03-15T13:55:00.000Z"
+    "to": "2018-03-15T13:55:01.000Z"
   },
   },
   "timepicker": {
   "timepicker": {
     "refresh_intervals": [
     "refresh_intervals": [
@@ -2238,5 +2504,5 @@
   "timezone": "",
   "timezone": "",
   "title": "Microsoft SQL Server Data Source Test",
   "title": "Microsoft SQL Server Data Source Test",
   "uid": "GlAqcPgmz",
   "uid": "GlAqcPgmz",
-  "version": 37
+  "version": 57
 }
 }

+ 2350 - 0
docker/blocks/mysql_tests/dashboard.json

@@ -0,0 +1,2350 @@
+{
+  "__inputs": [
+    {
+      "name": "DS_MYSQL_TEST",
+      "label": "MySQL TEST",
+      "description": "",
+      "type": "datasource",
+      "pluginId": "mysql",
+      "pluginName": "MySQL"
+    },
+    {
+      "name": "DS_MSSQL_TEST",
+      "label": "MSSQL Test",
+      "description": "",
+      "type": "datasource",
+      "pluginId": "mssql",
+      "pluginName": "Microsoft SQL Server"
+    }
+  ],
+  "__requires": [
+    {
+      "type": "grafana",
+      "id": "grafana",
+      "name": "Grafana",
+      "version": "5.0.0"
+    },
+    {
+      "type": "panel",
+      "id": "graph",
+      "name": "Graph",
+      "version": "5.0.0"
+    },
+    {
+      "type": "datasource",
+      "id": "mssql",
+      "name": "Microsoft SQL Server",
+      "version": "1.0.0"
+    },
+    {
+      "type": "datasource",
+      "id": "mysql",
+      "name": "MySQL",
+      "version": "5.0.0"
+    },
+    {
+      "type": "panel",
+      "id": "table",
+      "name": "Table",
+      "version": "5.0.0"
+    }
+  ],
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      },
+      {
+        "datasource": "${DS_MYSQL_TEST}",
+        "enable": false,
+        "hide": false,
+        "iconColor": "#6ed0e0",
+        "limit": 100,
+        "name": "Deploys",
+        "rawQuery": "SELECT\n   time_sec,\n   description as text,\n   tags\n  FROM event\n  WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n  ORDER BY 1 ASC\n  ",
+        "showIn": 0,
+        "tags": [],
+        "type": "tags"
+      },
+      {
+        "datasource": "${DS_MYSQL_TEST}",
+        "enable": false,
+        "hide": false,
+        "iconColor": "rgba(255, 96, 96, 1)",
+        "limit": 100,
+        "name": "Tickets",
+        "rawQuery": "SELECT\n   time_sec as time,\n   description as text,\n   tags\n  FROM event\n  WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n  ORDER BY 1 ASC\n  ",
+        "showIn": 0,
+        "tags": [],
+        "type": "tags"
+      },
+      {
+        "datasource": "${DS_MYSQL_TEST}",
+        "enable": false,
+        "hide": false,
+        "iconColor": "#7eb26d",
+        "limit": 100,
+        "name": "Metric Values timeEpoch macro",
+        "rawQuery": "SELECT \n  $__timeEpoch(time), \n  measurement as text, \n  '' as tags\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time)\nORDER BY 1",
+        "showIn": 0,
+        "tags": [],
+        "type": "tags"
+      },
+      {
+        "datasource": "${DS_MYSQL_TEST}",
+        "enable": false,
+        "hide": false,
+        "iconColor": "#1f78c1",
+        "limit": 100,
+        "name": "Metric Values native time",
+        "rawQuery": "SELECT \n  time, \n  measurement as text, \n  '' as tags\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time)\nORDER BY 1",
+        "showIn": 0,
+        "tags": [],
+        "type": "tags"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "id": null,
+  "iteration": 1521715720483,
+  "links": [],
+  "panels": [
+    {
+      "columns": [],
+      "datasource": "${DS_MYSQL_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 4,
+        "w": 24,
+        "x": 0,
+        "y": 0
+      },
+      "id": 2,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "string",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT * from mysql_types",
+          "refId": "A"
+        }
+      ],
+      "title": "Data types",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_MYSQL_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 0,
+        "y": 4
+      },
+      "id": 32,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time_sec",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT cast(null as unsigned integer) as time_sec",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "cast(null as unsigned integer) as time",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_MYSQL_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 6,
+        "y": 4
+      },
+      "id": 33,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time_sec",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT cast(null as datetime) as time_sec",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "cast(null as datetime) as time",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_MYSQL_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 12,
+        "y": 4
+      },
+      "id": 34,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time_sec",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT cast(NOW() as datetime) as time_sec",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "cast()NOW() as datetime) as time",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_MYSQL_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 18,
+        "y": 4
+      },
+      "id": 35,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time_sec",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT NOW() as time",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "NOW() as time",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 0,
+        "y": 7
+      },
+      "id": 7,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": true,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "timeGroup macro 5m without fill",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 8,
+        "y": 7
+      },
+      "id": 9,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null as zero",
+      "percentage": false,
+      "pointradius": 3,
+      "points": true,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "timeGroup macro 5m with fill(NULL) and null as zero",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 16,
+        "y": 7
+      },
+      "id": 10,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": true,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "timeGroup macro 5m with fill(10.0)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 0,
+        "y": 16
+      },
+      "id": 16,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": false,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Metrics - timeGroup macro $summarize without fill",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 8,
+        "y": 16
+      },
+      "id": 12,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": false,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null as zero",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Metrics - timeGroup macro $summarize with fill(NULL)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 16,
+        "y": 16
+      },
+      "id": 13,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": false,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Metrics - timeGroup macro $summarize with fill(100.0)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 25
+      },
+      "id": 27,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  CONCAT(measurement, ' - value one') as metric, \n  avg(valueOne) as valueOne\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  CONCAT(measurement, ' - value two') as metric, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values\nWHERE\n  $__timeFilter(time) AND\n  measurement IN($metric)\nGROUP BY 1,2\nORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column using timeGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 25
+      },
+      "id": 5,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [
+        {
+          "alias": "MovingAverageValueOne",
+          "dashes": true,
+          "lines": false
+        },
+        {
+          "alias": "MovingAverageValueTwo",
+          "dashes": true,
+          "lines": false,
+          "yaxis": 1
+        }
+      ],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize') as time, \n  avg(valueOne) as valueOne, \n  avg(valueTwo) as valueTwo \nFROM\n  metric_values \nWHERE \n  $__timeFilter(time) AND \n  measurement IN($metric)\nGROUP BY 1\nORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column using timeGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 33
+      },
+      "id": 4,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 33
+      },
+      "id": 28,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__time(time), valueOne, valueTwo FROM metric_values ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 41
+      },
+      "id": 19,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - stacked",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 41
+      },
+      "id": 18,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - stacked",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 49
+      },
+      "id": 17,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": true,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - stacked percent",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 49
+      },
+      "id": 20,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": true,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - stacked percent",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 57
+      },
+      "id": 14,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - series mode",
+      "tooltip": {
+        "shared": false,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "series",
+        "name": null,
+        "show": true,
+        "values": [
+          "total"
+        ]
+      },
+      "yaxes": [
+        {
+          "decimals": null,
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MSSQL_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 57
+      },
+      "id": 15,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - series mode",
+      "tooltip": {
+        "shared": false,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "series",
+        "name": null,
+        "show": true,
+        "values": [
+          "total"
+        ]
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 65
+      },
+      "id": 25,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - histogram",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 50,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "current"
+        ]
+      },
+      "yaxes": [
+        {
+          "decimals": null,
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 65
+      },
+      "id": 22,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - histogram",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 100,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "total"
+        ]
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 73
+      },
+      "id": 21,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - histogram stacked",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 20,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "current"
+        ]
+      },
+      "yaxes": [
+        {
+          "decimals": null,
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 73
+      },
+      "id": 26,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - histogram stacked",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 20,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "total"
+        ]
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 81
+      },
+      "id": 23,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": true,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - histogram stacked percent",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 20,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "current"
+        ]
+      },
+      "yaxes": [
+        {
+          "decimals": null,
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_MYSQL_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 81
+      },
+      "id": 24,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": true,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - histogram stacked percent",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 20,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "total"
+        ]
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    }
+  ],
+  "refresh": false,
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": [
+      {
+        "allValue": "",
+        "current": {},
+        "datasource": "${DS_MYSQL_TEST}",
+        "hide": 0,
+        "includeAll": true,
+        "label": "Metric",
+        "multi": true,
+        "name": "metric",
+        "options": [],
+        "query": "SELECT DISTINCT measurement FROM metric_values",
+        "refresh": 1,
+        "regex": "",
+        "sort": 0,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "auto": false,
+        "auto_count": 30,
+        "auto_min": "10s",
+        "current": {
+          "text": "10m",
+          "value": "10m"
+        },
+        "hide": 0,
+        "label": "Interval",
+        "name": "summarize",
+        "options": [
+          {
+            "selected": false,
+            "text": "1s",
+            "value": "1s"
+          },
+          {
+            "selected": false,
+            "text": "10s",
+            "value": "10s"
+          },
+          {
+            "selected": false,
+            "text": "30s",
+            "value": "30s"
+          },
+          {
+            "selected": false,
+            "text": "1m",
+            "value": "1m"
+          },
+          {
+            "selected": false,
+            "text": "5m",
+            "value": "5m"
+          },
+          {
+            "selected": true,
+            "text": "10m",
+            "value": "10m"
+          }
+        ],
+        "query": "1s,10s,30s,1m,5m,10m",
+        "refresh": 2,
+        "type": "interval"
+      }
+    ]
+  },
+  "time": {
+    "from": "2018-03-15T11:30:00.000Z",
+    "to": "2018-03-15T12:55:01.000Z"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "",
+  "title": "MySQL Data Source Test",
+  "uid": "Hmf8FDkmz",
+  "version": 9
+}

+ 2324 - 0
docker/blocks/postgres_tests/dashboard.json

@@ -0,0 +1,2324 @@
+{
+  "__inputs": [
+    {
+      "name": "DS_POSTGRES_TEST",
+      "label": "Postgres TEST",
+      "description": "",
+      "type": "datasource",
+      "pluginId": "postgres",
+      "pluginName": "PostgreSQL"
+    }
+  ],
+  "__requires": [
+    {
+      "type": "grafana",
+      "id": "grafana",
+      "name": "Grafana",
+      "version": "5.0.0"
+    },
+    {
+      "type": "panel",
+      "id": "graph",
+      "name": "Graph",
+      "version": "5.0.0"
+    },
+    {
+      "type": "datasource",
+      "id": "postgres",
+      "name": "PostgreSQL",
+      "version": "5.0.0"
+    },
+    {
+      "type": "panel",
+      "id": "table",
+      "name": "Table",
+      "version": "5.0.0"
+    }
+  ],
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      },
+      {
+        "datasource": "${DS_POSTGRES_TEST}",
+        "enable": false,
+        "hide": false,
+        "iconColor": "#6ed0e0",
+        "limit": 100,
+        "name": "Deploys",
+        "rawQuery": "SELECT \"time_sec\" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC",
+        "showIn": 0,
+        "tags": [],
+        "type": "tags"
+      },
+      {
+        "datasource": "${DS_POSTGRES_TEST}",
+        "enable": false,
+        "hide": false,
+        "iconColor": "rgba(255, 96, 96, 1)",
+        "limit": 100,
+        "name": "Tickets",
+        "rawQuery": "SELECT \"time_sec\" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC",
+        "showIn": 0,
+        "tags": [],
+        "type": "tags"
+      },
+      {
+        "datasource": "${DS_POSTGRES_TEST}",
+        "enable": false,
+        "hide": false,
+        "iconColor": "#7eb26d",
+        "limit": 100,
+        "name": "Metric Values timeEpoch macro",
+        "rawQuery": "SELECT \n  $__timeEpoch(time), \n  measurement as text, \n  '' as tags\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time)\nORDER BY 1",
+        "showIn": 0,
+        "tags": [],
+        "type": "tags"
+      },
+      {
+        "datasource": "${DS_POSTGRES_TEST}",
+        "enable": false,
+        "hide": false,
+        "iconColor": "#1f78c1",
+        "limit": 100,
+        "name": "Metric Values native time",
+        "rawQuery": "SELECT \n  time, \n  measurement as text, \n  '' as tags\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time)\nORDER BY 1",
+        "showIn": 0,
+        "tags": [],
+        "type": "tags"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "id": null,
+  "iteration": 1521725946837,
+  "links": [],
+  "panels": [
+    {
+      "columns": [],
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 4,
+        "w": 24,
+        "x": 0,
+        "y": 0
+      },
+      "id": 2,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 1,
+        "desc": false
+      },
+      "styles": [
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "string",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT * FROM postgres_types",
+          "refId": "A"
+        }
+      ],
+      "title": "Data types",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 0,
+        "y": 4
+      },
+      "id": 32,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT cast(null as bigint) as time",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "cast(null as bigint) as time",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 6,
+        "y": 4
+      },
+      "id": 33,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT cast(null as timestamp) as time",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "cast(null as datetime) as time",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 12,
+        "y": 4
+      },
+      "id": 34,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT localtimestamp as time",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "localtimestamp as time",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 3,
+        "w": 6,
+        "x": 18,
+        "y": 4
+      },
+      "id": 35,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "",
+          "format": "table",
+          "rawSql": "SELECT NOW() as time",
+          "refId": "A",
+          "target": ""
+        }
+      ],
+      "title": "NOW() as time",
+      "transform": "table",
+      "type": "table"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 0,
+        "y": 7
+      },
+      "id": 7,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": true,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "timeGroup macro 5m without fill",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 8,
+        "y": 7
+      },
+      "id": 9,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null as zero",
+      "percentage": false,
+      "pointradius": 3,
+      "points": true,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "timeGroup macro 5m with fill(NULL) and null as zero",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 16,
+        "y": 7
+      },
+      "id": 10,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": true,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "timeGroup macro 5m with fill(10.0)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 0,
+        "y": 16
+      },
+      "id": 16,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": false,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Metrics - timeGroup macro $summarize without fill",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 8,
+        "y": 16
+      },
+      "id": 12,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": false,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null as zero",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Metrics - timeGroup macro $summarize with fill(NULL)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 9,
+        "w": 8,
+        "x": 16,
+        "y": 16
+      },
+      "id": 13,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": false,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": true,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Metrics - timeGroup macro $summarize with fill(100.0)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 25
+      },
+      "id": 27,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize'), \n  measurement || ' - value one' as metric, \n  avg(\"valueOne\") as \"valueOne\"\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize'), \n  measurement || ' - value two' as metric, \n  avg(\"valueTwo\") as \"valueTwo\"\nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column using timeGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 25
+      },
+      "id": 5,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT \n  $__timeGroup(time, '$summarize'), \n  avg(\"valueOne\") as \"valueOne\", \n  avg(\"valueTwo\") as \"valueTwo\" \nFROM\n  metric_values \nWHERE\n  $__timeFilter(time) AND\n  measurement in($metric)\nGROUP BY 1\nORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column using timeGroup macro ($summarize)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 33
+      },
+      "id": 4,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 33
+      },
+      "id": 28,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 41
+      },
+      "id": 19,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - stacked",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 41
+      },
+      "id": 18,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - stacked",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 49
+      },
+      "id": 17,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "hideEmpty": false,
+        "hideZero": false,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": true,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - stacked percent",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 49
+      },
+      "id": 20,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": true,
+      "pointradius": 3,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - stacked percent",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 57
+      },
+      "id": 14,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - series mode",
+      "tooltip": {
+        "shared": false,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "series",
+        "name": null,
+        "show": true,
+        "values": [
+          "total"
+        ]
+      },
+      "yaxes": [
+        {
+          "decimals": null,
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 57
+      },
+      "id": 15,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": true,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - series mode",
+      "tooltip": {
+        "shared": false,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "series",
+        "name": null,
+        "show": true,
+        "values": [
+          "total"
+        ]
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 65
+      },
+      "id": 25,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - histogram",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 50,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "current"
+        ]
+      },
+      "yaxes": [
+        {
+          "decimals": null,
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 65
+      },
+      "id": 22,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values\nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - histogram",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 100,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "total"
+        ]
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 73
+      },
+      "id": 21,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - histogram stacked",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 20,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "current"
+        ]
+      },
+      "yaxes": [
+        {
+          "decimals": null,
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 73
+      },
+      "id": 26,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - histogram stacked",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 20,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "total"
+        ]
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 81
+      },
+      "id": 23,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": true,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        },
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series with metric column - histogram stacked percent",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 20,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "current"
+        ]
+      },
+      "yaxes": [
+        {
+          "decimals": null,
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    },
+    {
+      "aliasColors": {},
+      "bars": true,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "${DS_POSTGRES_TEST}",
+      "fill": 1,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 81
+      },
+      "id": 24,
+      "legend": {
+        "alignAsTable": true,
+        "avg": true,
+        "current": true,
+        "max": true,
+        "min": true,
+        "rightSide": true,
+        "show": false,
+        "total": true,
+        "values": true
+      },
+      "lines": false,
+      "linewidth": 1,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": true,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": true,
+      "steppedLine": false,
+      "targets": [
+        {
+          "alias": "",
+          "format": "time_series",
+          "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Multiple series without metric column - histogram stacked percent",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": 20,
+        "mode": "histogram",
+        "name": null,
+        "show": true,
+        "values": [
+          "total"
+        ]
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ]
+    }
+  ],
+  "refresh": false,
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": [
+      {
+        "allValue": null,
+        "current": {},
+        "datasource": "${DS_POSTGRES_TEST}",
+        "hide": 0,
+        "includeAll": true,
+        "label": "Metric",
+        "multi": true,
+        "name": "metric",
+        "options": [],
+        "query": "SELECT DISTINCT measurement FROM metric_values",
+        "refresh": 1,
+        "regex": "",
+        "sort": 1,
+        "tagValuesQuery": "",
+        "tags": [],
+        "tagsQuery": "",
+        "type": "query",
+        "useTags": false
+      },
+      {
+        "auto": false,
+        "auto_count": 30,
+        "auto_min": "10s",
+        "current": {
+          "text": "10m",
+          "value": "10m"
+        },
+        "hide": 0,
+        "label": "Interval",
+        "name": "summarize",
+        "options": [
+          {
+            "selected": false,
+            "text": "1s",
+            "value": "1s"
+          },
+          {
+            "selected": false,
+            "text": "10s",
+            "value": "10s"
+          },
+          {
+            "selected": false,
+            "text": "30s",
+            "value": "30s"
+          },
+          {
+            "selected": false,
+            "text": "1m",
+            "value": "1m"
+          },
+          {
+            "selected": false,
+            "text": "5m",
+            "value": "5m"
+          },
+          {
+            "selected": true,
+            "text": "10m",
+            "value": "10m"
+          }
+        ],
+        "query": "1s,10s,30s,1m,5m,10m",
+        "refresh": 2,
+        "type": "interval"
+      }
+    ]
+  },
+  "time": {
+    "from": "2018-03-15T12:30:00.000Z",
+    "to": "2018-03-15T13:55:01.000Z"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "",
+  "title": "Postgres Data Source Test",
+  "uid": "vHQdlVziz",
+  "version": 14
+}

+ 22 - 13
docs/sources/administration/provisioning.md

@@ -11,11 +11,13 @@ weight = 8
 
 
 # Provisioning Grafana
 # Provisioning Grafana
 
 
-## Config file
+In previous versions of Grafana, you could only use the API for provisioning data sources and dashboards. But that required the service to be running before you started creating dashboards and you also needed to set up credentials for the HTTP API. In v5.0 we decided to improve this experience by adding a new active provisioning system that uses config files. This will make GitOps more natural as data sources and dashboards can be defined via files that can be version controlled. We hope to extend this system to later add support for users, orgs and alerts as well.
+
+## Config File
 
 
 Checkout the [configuration](/installation/configuration) page for more information on what you can configure in `grafana.ini`
 Checkout the [configuration](/installation/configuration) page for more information on what you can configure in `grafana.ini`
 
 
-### Config file locations
+### Config File Locations
 
 
 - Default configuration from `$WORKING_DIR/conf/defaults.ini`
 - Default configuration from `$WORKING_DIR/conf/defaults.ini`
 - Custom configuration from `$WORKING_DIR/conf/custom.ini`
 - Custom configuration from `$WORKING_DIR/conf/custom.ini`
@@ -26,7 +28,7 @@ Checkout the [configuration](/installation/configuration) page for more informat
 > `/etc/grafana/grafana.ini`. This path is specified in the Grafana
 > `/etc/grafana/grafana.ini`. This path is specified in the Grafana
 > init.d script using `--config` file parameter.
 > init.d script using `--config` file parameter.
 
 
-### Using environment variables
+### Using Environment Variables
 
 
 All options in the configuration file (listed below) can be overridden
 All options in the configuration file (listed below) can be overridden
 using environment variables using the syntax:
 using environment variables using the syntax:
@@ -59,7 +61,7 @@ export GF_AUTH_GOOGLE_CLIENT_SECRET=newS3cretKey
 
 
 <hr />
 <hr />
 
 
-## Configuration management tools
+## Configuration Management Tools
 
 
 Currently we do not provide any scripts/manifests for configuring Grafana. Rather than spending time learning and creating scripts/manifests for each tool, we think our time is better spent making Grafana easier to provision. Therefore, we heavily relay on the expertise of the community.
 Currently we do not provide any scripts/manifests for configuring Grafana. Rather than spending time learning and creating scripts/manifests for each tool, we think our time is better spent making Grafana easier to provision. Therefore, we heavily relay on the expertise of the community.
 
 
@@ -76,10 +78,12 @@ Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://gith
 
 
 It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`provisioning/datasources`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list.
 It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`provisioning/datasources`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list.
 
 
-### Running multiple Grafana instances.
+### Running Multiple Grafana Instances
+
 If you are running multiple instances of Grafana you might run into problems if they have different versions of the `datasource.yaml` configuration file. The best way to solve this problem is to add a version number to each datasource in the configuration and increase it when you update the config. Grafana will only update datasources with the same or lower version number than specified in the config. That way, old configs cannot overwrite newer configs if they restart at the same time.
 If you are running multiple instances of Grafana you might run into problems if they have different versions of the `datasource.yaml` configuration file. The best way to solve this problem is to add a version number to each datasource in the configuration and increase it when you update the config. Grafana will only update datasources with the same or lower version number than specified in the config. That way, old configs cannot overwrite newer configs if they restart at the same time.
 
 
-### Example datasource config file
+### Example Datasource Config File
+
 ```yaml
 ```yaml
 # config file version
 # config file version
 apiVersion: 1
 apiVersion: 1
@@ -133,14 +137,20 @@ datasources:
   editable: false
   editable: false
 ```
 ```
 
 
-#### Json data
+#### Custom Settings per Datasource
+
+| Datasource | Misc |
+| ---- | ---- |
+| Elasticsearch | Elasticsearch uses the `database` property to configure the index for a datasource |
+
+#### Json Data
 
 
 Since not all datasources have the same configuration settings we only have the most common ones as fields. The rest should be stored as a json blob in the `json_data` field. Here are the most common settings that the core datasources use.
 Since not all datasources have the same configuration settings we only have the most common ones as fields. The rest should be stored as a json blob in the `json_data` field. Here are the most common settings that the core datasources use.
 
 
-| Name | Type | Datasource |Description |
-| ----| ---- | ---- | --- |
+| Name | Type | Datasource | Description |
+| ---- | ---- | ---- | ---- |
 | tlsAuth | boolean | *All* |  Enable TLS authentication using client cert configured in secure json data |
 | tlsAuth | boolean | *All* |  Enable TLS authentication using client cert configured in secure json data |
-| tlsAuthWithCACert | boolean | *All* | Enable TLS authtication using CA cert |
+| tlsAuthWithCACert | boolean | *All* | Enable TLS authentication using CA cert |
 | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
 | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
 | graphiteVersion | string | Graphite |  Graphite version  |
 | graphiteVersion | string | Graphite |  Graphite version  |
 | timeInterval | string | Elastic, Influxdb & Prometheus | Lowest interval/step value that should be used for this data source |
 | timeInterval | string | Elastic, Influxdb & Prometheus | Lowest interval/step value that should be used for this data source |
@@ -155,8 +165,7 @@ Since not all datasources have the same configuration settings we only have the
 | tsdbResolution | string | OpenTsdb | Resolution |
 | tsdbResolution | string | OpenTsdb | Resolution |
 | sslmode | string | Postgre | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' |
 | sslmode | string | Postgre | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' |
 
 
-
-#### Secure Json data
+#### Secure Json Data
 
 
 `{"authType":"keys","defaultRegion":"us-west-2","timeField":"@timestamp"}`
 `{"authType":"keys","defaultRegion":"us-west-2","timeField":"@timestamp"}`
 
 
@@ -194,7 +203,7 @@ providers:
 
 
 When Grafana starts, it will update/insert all dashboards available in the configured path. Then later on poll that path and look for updated json files and insert those update/insert those into the database.
 When Grafana starts, it will update/insert all dashboards available in the configured path. Then later on poll that path and look for updated json files and insert those update/insert those into the database.
 
 
-### Reuseable dashboard urls
+### Reuseable Dashboard Urls
 
 
 If the dashboard in the json file contains an [uid](/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifer.
 If the dashboard in the json file contains an [uid](/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifer.
 When Grafana starts, it will update/insert all dashboards available in the configured folders. If you modify the file, the dashboard will also be updated.
 When Grafana starts, it will update/insert all dashboards available in the configured folders. If you modify the file, the dashboard will also be updated.

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

@@ -234,7 +234,12 @@ The maximum number of connections in the idle connection pool.
 ### max_open_conn
 ### max_open_conn
 The maximum number of open connections to the database.
 The maximum number of open connections to the database.
 
 
+### conn_max_lifetime
+
+Sets the maximum amount of time a connection may be reused. The default is 14400 (which means 14400 seconds or 4 hours). For MySQL, this setting should be shorter than the [`wait_timeout`](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_wait_timeout) variable.
+
 ### log_queries
 ### log_queries
+
 Set to `true` to log the sql calls and execution times.
 Set to `true` to log the sql calls and execution times.
 
 
 <hr />
 <hr />

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

@@ -1,6 +1,6 @@
 +++
 +++
 title = "Variables"
 title = "Variables"
-keywords = ["grafana", "templating", "documentation", "guide"]
+keywords = ["grafana", "templating", "documentation", "guide", "template", "variable"]
 type = "docs"
 type = "docs"
 [menu.docs]
 [menu.docs]
 name = "Variables"
 name = "Variables"
@@ -80,6 +80,73 @@ Option | Description
 *Regex* | Regex to filter or capture specific parts of the names return by your data source query. Optional.
 *Regex* | Regex to filter or capture specific parts of the names return by your data source query. Optional.
 *Sort* | Define sort order for options in dropdown. **Disabled** means that the order of options returned by your data source query will be used.
 *Sort* | Define sort order for options in dropdown. **Disabled** means that the order of options returned by your data source query will be used.
 
 
+#### Using regex to filter/modify values in the Variable dropdown
+
+Using the Regex Query Option, you filter the list of options returned by the Variable query or modify the options returned.
+
+Examples of filtering on the following list of options:
+
+```text
+backend_01
+backend_02
+backend_03
+backend_04
+```
+
+##### Filter so that only the options that end with `01` or `02` are returned:
+
+Regex:
+
+```regex
+/.*[01|02]/
+```
+
+Result:
+
+```text
+backend_01
+backend_02
+```
+
+##### Filter and modify the options using a regex capture group to return part of the text:
+
+Regex:
+
+```regex
+/.*(01|02)/
+```
+
+Result:
+
+```text
+01
+02
+```
+
+#### Filter and modify - Prometheus Example
+
+List of options:
+
+```text
+up{instance="demo.robustperception.io:9090",job="prometheus"} 1 1521630638000
+up{instance="demo.robustperception.io:9093",job="alertmanager"} 1 1521630638000
+up{instance="demo.robustperception.io:9100",job="node"} 1 1521630638000
+```
+
+Regex:
+
+```regex
+/.*instance="([^"]*).*/
+```
+
+Result:
+
+```text
+demo.robustperception.io:9090
+demo.robustperception.io:9093
+demo.robustperception.io:9100
+```
+
 ### Query expressions
 ### Query expressions
 
 
 The query expressions are different for each data source.
 The query expressions are different for each data source.

+ 1 - 1
emails/templates/layouts/default.html

@@ -143,7 +143,7 @@ td[class="stack-column-center"] {
 											<center>
 											<center>
 												<p style="text-align: center; font-size: 12px; color: #999999;">
 												<p style="text-align: center; font-size: 12px; color: #999999;">
 													Sent by <a href="[[.AppUrl]]">Grafana v[[.BuildVersion]]</a>
 													Sent by <a href="[[.AppUrl]]">Grafana v[[.BuildVersion]]</a>
-													<br />&copy; 2016 Grafana and raintank
+													<br />&copy; 2018 Grafana Labs
 												</p>
 												</p>
 											</center>
 											</center>
 										</td>
 										</td>

+ 2 - 2
package.json

@@ -118,8 +118,8 @@
       "prettier --write",
       "prettier --write",
       "git add"
       "git add"
     ],
     ],
-    "*.go": [
-      "gofmt -w -s pkg",
+    "*pkg/**/*.go": [
+      "gofmt -w -s",
       "git add"
       "git add"
     ]
     ]
   },
   },

+ 9 - 0
pkg/api/admin.go

@@ -1,6 +1,7 @@
 package api
 package api
 
 
 import (
 import (
+	"regexp"
 	"strings"
 	"strings"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
@@ -21,6 +22,14 @@ func AdminGetSettings(c *m.ReqContext) {
 			if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config")) {
 			if strings.Contains(keyName, "secret") || strings.Contains(keyName, "password") || (strings.Contains(keyName, "provider_config")) {
 				value = "************"
 				value = "************"
 			}
 			}
+			if strings.Contains(keyName, "url") {
+				var rgx = regexp.MustCompile(`.*:\/\/([^:]*):([^@]*)@.*?$`)
+				var subs = rgx.FindAllSubmatch([]byte(value), -1)
+				if subs != nil && len(subs[0]) == 3 {
+					value = strings.Replace(value, string(subs[0][1]), "******", 1)
+					value = strings.Replace(value, string(subs[0][2]), "******", 1)
+				}
+			}
 
 
 			jsonSec[keyName] = value
 			jsonSec[keyName] = value
 		}
 		}

+ 7 - 7
pkg/api/admin_users.go

@@ -47,14 +47,14 @@ func AdminCreateUser(c *m.ReqContext, form dtos.AdminCreateUserForm) {
 }
 }
 
 
 func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordForm) {
 func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordForm) {
-	userId := c.ParamsInt64(":id")
+	userID := c.ParamsInt64(":id")
 
 
 	if len(form.Password) < 4 {
 	if len(form.Password) < 4 {
 		c.JsonApiErr(400, "New password too short", nil)
 		c.JsonApiErr(400, "New password too short", nil)
 		return
 		return
 	}
 	}
 
 
-	userQuery := m.GetUserByIdQuery{Id: userId}
+	userQuery := m.GetUserByIdQuery{Id: userID}
 
 
 	if err := bus.Dispatch(&userQuery); err != nil {
 	if err := bus.Dispatch(&userQuery); err != nil {
 		c.JsonApiErr(500, "Could not read user from database", err)
 		c.JsonApiErr(500, "Could not read user from database", err)
@@ -64,7 +64,7 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF
 	passwordHashed := util.EncodePassword(form.Password, userQuery.Result.Salt)
 	passwordHashed := util.EncodePassword(form.Password, userQuery.Result.Salt)
 
 
 	cmd := m.ChangeUserPasswordCommand{
 	cmd := m.ChangeUserPasswordCommand{
-		UserId:      userId,
+		UserId:      userID,
 		NewPassword: passwordHashed,
 		NewPassword: passwordHashed,
 	}
 	}
 
 
@@ -77,10 +77,10 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF
 }
 }
 
 
 func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
 func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
-	userId := c.ParamsInt64(":id")
+	userID := c.ParamsInt64(":id")
 
 
 	cmd := m.UpdateUserPermissionsCommand{
 	cmd := m.UpdateUserPermissionsCommand{
-		UserId:         userId,
+		UserId:         userID,
 		IsGrafanaAdmin: form.IsGrafanaAdmin,
 		IsGrafanaAdmin: form.IsGrafanaAdmin,
 	}
 	}
 
 
@@ -93,9 +93,9 @@ func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermis
 }
 }
 
 
 func AdminDeleteUser(c *m.ReqContext) {
 func AdminDeleteUser(c *m.ReqContext) {
-	userId := c.ParamsInt64(":id")
+	userID := c.ParamsInt64(":id")
 
 
-	cmd := m.DeleteUserCommand{UserId: userId}
+	cmd := m.DeleteUserCommand{UserId: userID}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		c.JsonApiErr(500, "Failed to delete user", err)
 		c.JsonApiErr(500, "Failed to delete user", err)

+ 39 - 39
pkg/api/alerting.go

@@ -26,10 +26,10 @@ func ValidateOrgAlert(c *m.ReqContext) {
 }
 }
 
 
 func GetAlertStatesForDashboard(c *m.ReqContext) Response {
 func GetAlertStatesForDashboard(c *m.ReqContext) Response {
-	dashboardId := c.QueryInt64("dashboardId")
+	dashboardID := c.QueryInt64("dashboardId")
 
 
-	if dashboardId == 0 {
-		return ApiError(400, "Missing query parameter dashboardId", nil)
+	if dashboardID == 0 {
+		return Error(400, "Missing query parameter dashboardId", nil)
 	}
 	}
 
 
 	query := m.GetAlertStatesForDashboardQuery{
 	query := m.GetAlertStatesForDashboardQuery{
@@ -38,10 +38,10 @@ func GetAlertStatesForDashboard(c *m.ReqContext) Response {
 	}
 	}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to fetch alert states", err)
+		return Error(500, "Failed to fetch alert states", err)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
 // GET /api/alerts
 // GET /api/alerts
@@ -60,20 +60,20 @@ func GetAlerts(c *m.ReqContext) Response {
 	}
 	}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "List alerts failed", err)
+		return Error(500, "List alerts failed", err)
 	}
 	}
 
 
 	for _, alert := range query.Result {
 	for _, alert := range query.Result {
 		alert.Url = m.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug)
 		alert.Url = m.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
 // POST /api/alerts/test
 // POST /api/alerts/test
 func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
 func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
 	if _, idErr := dto.Dashboard.Get("id").Int64(); idErr != nil {
 	if _, idErr := dto.Dashboard.Get("id").Int64(); idErr != nil {
-		return ApiError(400, "The dashboard needs to be saved at least once before you can test an alert rule", nil)
+		return Error(400, "The dashboard needs to be saved at least once before you can test an alert rule", nil)
 	}
 	}
 
 
 	backendCmd := alerting.AlertTestCommand{
 	backendCmd := alerting.AlertTestCommand{
@@ -84,9 +84,9 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
 
 
 	if err := bus.Dispatch(&backendCmd); err != nil {
 	if err := bus.Dispatch(&backendCmd); err != nil {
 		if validationErr, ok := err.(alerting.ValidationError); ok {
 		if validationErr, ok := err.(alerting.ValidationError); ok {
-			return ApiError(422, validationErr.Error(), nil)
+			return Error(422, validationErr.Error(), nil)
 		}
 		}
-		return ApiError(500, "Failed to test rule", err)
+		return Error(500, "Failed to test rule", err)
 	}
 	}
 
 
 	res := backendCmd.Result
 	res := backendCmd.Result
@@ -109,7 +109,7 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
 
 
 	dtoRes.TimeMs = fmt.Sprintf("%1.3fms", res.GetDurationMs())
 	dtoRes.TimeMs = fmt.Sprintf("%1.3fms", res.GetDurationMs())
 
 
-	return Json(200, dtoRes)
+	return JSON(200, dtoRes)
 }
 }
 
 
 // GET /api/alerts/:id
 // GET /api/alerts/:id
@@ -118,21 +118,21 @@ func GetAlert(c *m.ReqContext) Response {
 	query := m.GetAlertByIdQuery{Id: id}
 	query := m.GetAlertByIdQuery{Id: id}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "List alerts failed", err)
+		return Error(500, "List alerts failed", err)
 	}
 	}
 
 
-	return Json(200, &query.Result)
+	return JSON(200, &query.Result)
 }
 }
 
 
 func GetAlertNotifiers(c *m.ReqContext) Response {
 func GetAlertNotifiers(c *m.ReqContext) Response {
-	return Json(200, alerting.GetNotifiers())
+	return JSON(200, alerting.GetNotifiers())
 }
 }
 
 
 func GetAlertNotifications(c *m.ReqContext) Response {
 func GetAlertNotifications(c *m.ReqContext) Response {
 	query := &m.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
 	query := &m.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
 
 
 	if err := bus.Dispatch(query); err != nil {
 	if err := bus.Dispatch(query); err != nil {
-		return ApiError(500, "Failed to get alert notifications", err)
+		return Error(500, "Failed to get alert notifications", err)
 	}
 	}
 
 
 	result := make([]*dtos.AlertNotification, 0)
 	result := make([]*dtos.AlertNotification, 0)
@@ -148,40 +148,40 @@ func GetAlertNotifications(c *m.ReqContext) Response {
 		})
 		})
 	}
 	}
 
 
-	return Json(200, result)
+	return JSON(200, result)
 }
 }
 
 
-func GetAlertNotificationById(c *m.ReqContext) Response {
+func GetAlertNotificationByID(c *m.ReqContext) Response {
 	query := &m.GetAlertNotificationsQuery{
 	query := &m.GetAlertNotificationsQuery{
 		OrgId: c.OrgId,
 		OrgId: c.OrgId,
 		Id:    c.ParamsInt64("notificationId"),
 		Id:    c.ParamsInt64("notificationId"),
 	}
 	}
 
 
 	if err := bus.Dispatch(query); err != nil {
 	if err := bus.Dispatch(query); err != nil {
-		return ApiError(500, "Failed to get alert notifications", err)
+		return Error(500, "Failed to get alert notifications", err)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
 func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response {
 func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to create alert notification", err)
+		return Error(500, "Failed to create alert notification", err)
 	}
 	}
 
 
-	return Json(200, cmd.Result)
+	return JSON(200, cmd.Result)
 }
 }
 
 
 func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response {
 func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to update alert notification", err)
+		return Error(500, "Failed to update alert notification", err)
 	}
 	}
 
 
-	return Json(200, cmd.Result)
+	return JSON(200, cmd.Result)
 }
 }
 
 
 func DeleteAlertNotification(c *m.ReqContext) Response {
 func DeleteAlertNotification(c *m.ReqContext) Response {
@@ -191,10 +191,10 @@ func DeleteAlertNotification(c *m.ReqContext) Response {
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to delete alert notification", err)
+		return Error(500, "Failed to delete alert notification", err)
 	}
 	}
 
 
-	return ApiSuccess("Notification deleted")
+	return Success("Notification deleted")
 }
 }
 
 
 //POST /api/alert-notifications/test
 //POST /api/alert-notifications/test
@@ -207,41 +207,41 @@ func NotificationTest(c *m.ReqContext, dto dtos.NotificationTestCommand) Respons
 
 
 	if err := bus.Dispatch(cmd); err != nil {
 	if err := bus.Dispatch(cmd); err != nil {
 		if err == m.ErrSmtpNotEnabled {
 		if err == m.ErrSmtpNotEnabled {
-			return ApiError(412, err.Error(), err)
+			return Error(412, err.Error(), err)
 		}
 		}
-		return ApiError(500, "Failed to send alert notifications", err)
+		return Error(500, "Failed to send alert notifications", err)
 	}
 	}
 
 
-	return ApiSuccess("Test notification sent")
+	return Success("Test notification sent")
 }
 }
 
 
 //POST /api/alerts/:alertId/pause
 //POST /api/alerts/:alertId/pause
 func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
 func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
-	alertId := c.ParamsInt64("alertId")
+	alertID := c.ParamsInt64("alertId")
 
 
-	query := m.GetAlertByIdQuery{Id: alertId}
+	query := m.GetAlertByIdQuery{Id: alertID}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Get Alert failed", err)
+		return Error(500, "Get Alert failed", err)
 	}
 	}
 
 
 	guardian := guardian.New(query.Result.DashboardId, c.OrgId, c.SignedInUser)
 	guardian := guardian.New(query.Result.DashboardId, c.OrgId, c.SignedInUser)
 	if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
 	if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
 		if err != nil {
 		if err != nil {
-			return ApiError(500, "Error while checking permissions for Alert", err)
+			return Error(500, "Error while checking permissions for Alert", err)
 		}
 		}
 
 
-		return ApiError(403, "Access denied to this dashboard and alert", nil)
+		return Error(403, "Access denied to this dashboard and alert", nil)
 	}
 	}
 
 
 	cmd := m.PauseAlertCommand{
 	cmd := m.PauseAlertCommand{
 		OrgId:    c.OrgId,
 		OrgId:    c.OrgId,
-		AlertIds: []int64{alertId},
+		AlertIds: []int64{alertID},
 		Paused:   dto.Paused,
 		Paused:   dto.Paused,
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "", err)
+		return Error(500, "", err)
 	}
 	}
 
 
 	var response m.AlertStateType = m.AlertStatePending
 	var response m.AlertStateType = m.AlertStatePending
@@ -252,12 +252,12 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
 	}
 	}
 
 
 	result := map[string]interface{}{
 	result := map[string]interface{}{
-		"alertId": alertId,
+		"alertId": alertID,
 		"state":   response,
 		"state":   response,
 		"message": "Alert " + pausedState,
 		"message": "Alert " + pausedState,
 	}
 	}
 
 
-	return Json(200, result)
+	return JSON(200, result)
 }
 }
 
 
 //POST /api/admin/pause-all-alerts
 //POST /api/admin/pause-all-alerts
@@ -267,7 +267,7 @@ func PauseAllAlerts(c *m.ReqContext, dto dtos.PauseAllAlertsCommand) Response {
 	}
 	}
 
 
 	if err := bus.Dispatch(&updateCmd); err != nil {
 	if err := bus.Dispatch(&updateCmd); err != nil {
-		return ApiError(500, "Failed to pause alerts", err)
+		return Error(500, "Failed to pause alerts", err)
 	}
 	}
 
 
 	var response m.AlertStateType = m.AlertStatePending
 	var response m.AlertStateType = m.AlertStatePending
@@ -283,5 +283,5 @@ func PauseAllAlerts(c *m.ReqContext, dto dtos.PauseAllAlertsCommand) Response {
 		"alertsAffected": updateCmd.ResultCount,
 		"alertsAffected": updateCmd.ResultCount,
 	}
 	}
 
 
-	return Json(200, result)
+	return JSON(200, result)
 }
 }

+ 47 - 47
pkg/api/annotations.go

@@ -30,7 +30,7 @@ func GetAnnotations(c *m.ReqContext) Response {
 
 
 	items, err := repo.Find(query)
 	items, err := repo.Find(query)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to get annotations", err)
+		return Error(500, "Failed to get annotations", err)
 	}
 	}
 
 
 	for _, item := range items {
 	for _, item := range items {
@@ -40,7 +40,7 @@ func GetAnnotations(c *m.ReqContext) Response {
 		item.Time = item.Time * 1000
 		item.Time = item.Time * 1000
 	}
 	}
 
 
-	return Json(200, items)
+	return JSON(200, items)
 }
 }
 
 
 type CreateAnnotationError struct {
 type CreateAnnotationError struct {
@@ -52,7 +52,7 @@ func (e *CreateAnnotationError) Error() string {
 }
 }
 
 
 func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
 func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
-	if canSave, err := canSaveByDashboardId(c, cmd.DashboardId); err != nil || !canSave {
+	if canSave, err := canSaveByDashboardID(c, cmd.DashboardId); err != nil || !canSave {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
 
 
@@ -60,7 +60,7 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
 
 
 	if cmd.Text == "" {
 	if cmd.Text == "" {
 		err := &CreateAnnotationError{"text field should not be empty"}
 		err := &CreateAnnotationError{"text field should not be empty"}
-		return ApiError(500, "Failed to save annotation", err)
+		return Error(500, "Failed to save annotation", err)
 	}
 	}
 
 
 	item := annotations.Item{
 	item := annotations.Item{
@@ -79,7 +79,7 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
 	}
 	}
 
 
 	if err := repo.Save(&item); err != nil {
 	if err := repo.Save(&item); err != nil {
-		return ApiError(500, "Failed to save annotation", err)
+		return Error(500, "Failed to save annotation", err)
 	}
 	}
 
 
 	startID := item.Id
 	startID := item.Id
@@ -93,24 +93,24 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
 		}
 		}
 
 
 		if err := repo.Update(&item); err != nil {
 		if err := repo.Update(&item); err != nil {
-			return ApiError(500, "Failed set regionId on annotation", err)
+			return Error(500, "Failed set regionId on annotation", err)
 		}
 		}
 
 
 		item.Id = 0
 		item.Id = 0
 		item.Epoch = cmd.TimeEnd / 1000
 		item.Epoch = cmd.TimeEnd / 1000
 
 
 		if err := repo.Save(&item); err != nil {
 		if err := repo.Save(&item); err != nil {
-			return ApiError(500, "Failed save annotation for region end time", err)
+			return Error(500, "Failed save annotation for region end time", err)
 		}
 		}
 
 
-		return Json(200, util.DynMap{
+		return JSON(200, util.DynMap{
 			"message": "Annotation added",
 			"message": "Annotation added",
 			"id":      startID,
 			"id":      startID,
 			"endId":   item.Id,
 			"endId":   item.Id,
 		})
 		})
 	}
 	}
 
 
-	return Json(200, util.DynMap{
+	return JSON(200, util.DynMap{
 		"message": "Annotation added",
 		"message": "Annotation added",
 		"id":      startID,
 		"id":      startID,
 	})
 	})
@@ -129,7 +129,7 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
 
 
 	if cmd.What == "" {
 	if cmd.What == "" {
 		err := &CreateAnnotationError{"what field should not be empty"}
 		err := &CreateAnnotationError{"what field should not be empty"}
-		return ApiError(500, "Failed to save Graphite annotation", err)
+		return Error(500, "Failed to save Graphite annotation", err)
 	}
 	}
 
 
 	if cmd.When == 0 {
 	if cmd.When == 0 {
@@ -152,12 +152,12 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
 				tagsArray = append(tagsArray, tagStr)
 				tagsArray = append(tagsArray, tagStr)
 			} else {
 			} else {
 				err := &CreateAnnotationError{"tag should be a string"}
 				err := &CreateAnnotationError{"tag should be a string"}
-				return ApiError(500, "Failed to save Graphite annotation", err)
+				return Error(500, "Failed to save Graphite annotation", err)
 			}
 			}
 		}
 		}
 	default:
 	default:
 		err := &CreateAnnotationError{"unsupported tags format"}
 		err := &CreateAnnotationError{"unsupported tags format"}
-		return ApiError(500, "Failed to save Graphite annotation", err)
+		return Error(500, "Failed to save Graphite annotation", err)
 	}
 	}
 
 
 	item := annotations.Item{
 	item := annotations.Item{
@@ -169,35 +169,35 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
 	}
 	}
 
 
 	if err := repo.Save(&item); err != nil {
 	if err := repo.Save(&item); err != nil {
-		return ApiError(500, "Failed to save Graphite annotation", err)
+		return Error(500, "Failed to save Graphite annotation", err)
 	}
 	}
 
 
-	return Json(200, util.DynMap{
+	return JSON(200, util.DynMap{
 		"message": "Graphite annotation added",
 		"message": "Graphite annotation added",
 		"id":      item.Id,
 		"id":      item.Id,
 	})
 	})
 }
 }
 
 
 func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
 func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
-	annotationId := c.ParamsInt64(":annotationId")
+	annotationID := c.ParamsInt64(":annotationId")
 
 
 	repo := annotations.GetRepository()
 	repo := annotations.GetRepository()
 
 
-	if resp := canSave(c, repo, annotationId); resp != nil {
+	if resp := canSave(c, repo, annotationID); resp != nil {
 		return resp
 		return resp
 	}
 	}
 
 
 	item := annotations.Item{
 	item := annotations.Item{
 		OrgId:  c.OrgId,
 		OrgId:  c.OrgId,
 		UserId: c.UserId,
 		UserId: c.UserId,
-		Id:     annotationId,
+		Id:     annotationID,
 		Epoch:  cmd.Time / 1000,
 		Epoch:  cmd.Time / 1000,
 		Text:   cmd.Text,
 		Text:   cmd.Text,
 		Tags:   cmd.Tags,
 		Tags:   cmd.Tags,
 	}
 	}
 
 
 	if err := repo.Update(&item); err != nil {
 	if err := repo.Update(&item); err != nil {
-		return ApiError(500, "Failed to update annotation", err)
+		return Error(500, "Failed to update annotation", err)
 	}
 	}
 
 
 	if cmd.IsRegion {
 	if cmd.IsRegion {
@@ -210,11 +210,11 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
 		itemRight.Id = 0
 		itemRight.Id = 0
 
 
 		if err := repo.Update(&itemRight); err != nil {
 		if err := repo.Update(&itemRight); err != nil {
-			return ApiError(500, "Failed to update annotation for region end time", err)
+			return Error(500, "Failed to update annotation for region end time", err)
 		}
 		}
 	}
 	}
 
 
-	return ApiSuccess("Annotation updated")
+	return Success("Annotation updated")
 }
 }
 
 
 func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response {
 func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response {
@@ -227,57 +227,57 @@ func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response
 	})
 	})
 
 
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to delete annotations", err)
+		return Error(500, "Failed to delete annotations", err)
 	}
 	}
 
 
-	return ApiSuccess("Annotations deleted")
+	return Success("Annotations deleted")
 }
 }
 
 
-func DeleteAnnotationById(c *m.ReqContext) Response {
+func DeleteAnnotationByID(c *m.ReqContext) Response {
 	repo := annotations.GetRepository()
 	repo := annotations.GetRepository()
-	annotationId := c.ParamsInt64(":annotationId")
+	annotationID := c.ParamsInt64(":annotationId")
 
 
-	if resp := canSave(c, repo, annotationId); resp != nil {
+	if resp := canSave(c, repo, annotationID); resp != nil {
 		return resp
 		return resp
 	}
 	}
 
 
 	err := repo.Delete(&annotations.DeleteParams{
 	err := repo.Delete(&annotations.DeleteParams{
-		Id: annotationId,
+		Id: annotationID,
 	})
 	})
 
 
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to delete annotation", err)
+		return Error(500, "Failed to delete annotation", err)
 	}
 	}
 
 
-	return ApiSuccess("Annotation deleted")
+	return Success("Annotation deleted")
 }
 }
 
 
 func DeleteAnnotationRegion(c *m.ReqContext) Response {
 func DeleteAnnotationRegion(c *m.ReqContext) Response {
 	repo := annotations.GetRepository()
 	repo := annotations.GetRepository()
-	regionId := c.ParamsInt64(":regionId")
+	regionID := c.ParamsInt64(":regionId")
 
 
-	if resp := canSave(c, repo, regionId); resp != nil {
+	if resp := canSave(c, repo, regionID); resp != nil {
 		return resp
 		return resp
 	}
 	}
 
 
 	err := repo.Delete(&annotations.DeleteParams{
 	err := repo.Delete(&annotations.DeleteParams{
-		RegionId: regionId,
+		RegionId: regionID,
 	})
 	})
 
 
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to delete annotation region", err)
+		return Error(500, "Failed to delete annotation region", err)
 	}
 	}
 
 
-	return ApiSuccess("Annotation region deleted")
+	return Success("Annotation region deleted")
 }
 }
 
 
-func canSaveByDashboardId(c *m.ReqContext, dashboardId int64) (bool, error) {
-	if dashboardId == 0 && !c.SignedInUser.HasRole(m.ROLE_EDITOR) {
+func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) {
+	if dashboardID == 0 && !c.SignedInUser.HasRole(m.ROLE_EDITOR) {
 		return false, nil
 		return false, nil
 	}
 	}
 
 
-	if dashboardId > 0 {
-		guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser)
+	if dashboardID > 0 {
+		guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
 		if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
 		if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
 			return false, err
 			return false, err
 		}
 		}
@@ -286,32 +286,32 @@ func canSaveByDashboardId(c *m.ReqContext, dashboardId int64) (bool, error) {
 	return true, nil
 	return true, nil
 }
 }
 
 
-func canSave(c *m.ReqContext, repo annotations.Repository, annotationId int64) Response {
-	items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationId, OrgId: c.OrgId})
+func canSave(c *m.ReqContext, repo annotations.Repository, annotationID int64) Response {
+	items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId})
 
 
 	if err != nil || len(items) == 0 {
 	if err != nil || len(items) == 0 {
-		return ApiError(500, "Could not find annotation to update", err)
+		return Error(500, "Could not find annotation to update", err)
 	}
 	}
 
 
-	dashboardId := items[0].DashboardId
+	dashboardID := items[0].DashboardId
 
 
-	if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave {
+	if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
 
 
 	return nil
 	return nil
 }
 }
 
 
-func canSaveByRegionId(c *m.ReqContext, repo annotations.Repository, regionId int64) Response {
-	items, err := repo.Find(&annotations.ItemQuery{RegionId: regionId, OrgId: c.OrgId})
+func canSaveByRegionID(c *m.ReqContext, repo annotations.Repository, regionID int64) Response {
+	items, err := repo.Find(&annotations.ItemQuery{RegionId: regionID, OrgId: c.OrgId})
 
 
 	if err != nil || len(items) == 0 {
 	if err != nil || len(items) == 0 {
-		return ApiError(500, "Could not find annotation to update", err)
+		return Error(500, "Could not find annotation to update", err)
 	}
 	}
 
 
-	dashboardId := items[0].DashboardId
+	dashboardID := items[0].DashboardId
 
 
-	if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave {
+	if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
 
 

+ 4 - 4
pkg/api/annotations_test.go

@@ -41,7 +41,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
 				})
 				})
 
 
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
-					sc.handlerFunc = DeleteAnnotationById
+					sc.handlerFunc = DeleteAnnotationByID
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 					So(sc.resp.Code, ShouldEqual, 403)
 					So(sc.resp.Code, ShouldEqual, 403)
 				})
 				})
@@ -68,7 +68,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
 				})
 				})
 
 
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
-					sc.handlerFunc = DeleteAnnotationById
+					sc.handlerFunc = DeleteAnnotationByID
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 					So(sc.resp.Code, ShouldEqual, 200)
 					So(sc.resp.Code, ShouldEqual, 200)
 				})
 				})
@@ -132,7 +132,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
 				})
 				})
 
 
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
-					sc.handlerFunc = DeleteAnnotationById
+					sc.handlerFunc = DeleteAnnotationByID
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 					So(sc.resp.Code, ShouldEqual, 403)
 					So(sc.resp.Code, ShouldEqual, 403)
 				})
 				})
@@ -159,7 +159,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
 				})
 				})
 
 
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
-					sc.handlerFunc = DeleteAnnotationById
+					sc.handlerFunc = DeleteAnnotationByID
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 					So(sc.resp.Code, ShouldEqual, 200)
 					So(sc.resp.Code, ShouldEqual, 200)
 				})
 				})

+ 24 - 24
pkg/api/api.go

@@ -9,14 +9,14 @@ import (
 )
 )
 
 
 // Register adds http routes
 // Register adds http routes
-func (hs *HttpServer) registerRoutes() {
+func (hs *HTTPServer) registerRoutes() {
 	macaronR := hs.macaron
 	macaronR := hs.macaron
 	reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
 	reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
 	reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
 	reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
 	reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
 	reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
 	reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
 	reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
-	redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardUrl()
-	redirectFromLegacyDashboardSoloUrl := middleware.RedirectFromLegacyDashboardSoloUrl()
+	redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
+	redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
 	quota := middleware.Quota
 	quota := middleware.Quota
 	bind := binding.Bind
 	bind := binding.Bind
 
 
@@ -67,11 +67,11 @@ func (hs *HttpServer) registerRoutes() {
 
 
 	r.Get("/d/:uid/:slug", reqSignedIn, Index)
 	r.Get("/d/:uid/:slug", reqSignedIn, Index)
 	r.Get("/d/:uid", reqSignedIn, Index)
 	r.Get("/d/:uid", reqSignedIn, Index)
-	r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardUrl, Index)
+	r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardURL, Index)
 	r.Get("/dashboard/script/*", reqSignedIn, Index)
 	r.Get("/dashboard/script/*", reqSignedIn, Index)
 	r.Get("/dashboard-solo/snapshot/*", Index)
 	r.Get("/dashboard-solo/snapshot/*", Index)
 	r.Get("/d-solo/:uid/:slug", reqSignedIn, Index)
 	r.Get("/d-solo/:uid/:slug", reqSignedIn, Index)
-	r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloUrl, Index)
+	r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloURL, Index)
 	r.Get("/dashboard-solo/script/*", reqSignedIn, Index)
 	r.Get("/dashboard-solo/script/*", reqSignedIn, Index)
 	r.Get("/import/dashboard", reqSignedIn, Index)
 	r.Get("/import/dashboard", reqSignedIn, Index)
 	r.Get("/dashboards/", reqSignedIn, Index)
 	r.Get("/dashboards/", reqSignedIn, Index)
@@ -110,7 +110,7 @@ func (hs *HttpServer) registerRoutes() {
 	r.Get("/api/snapshots-delete/:key", reqEditorRole, wrap(DeleteDashboardSnapshot))
 	r.Get("/api/snapshots-delete/:key", reqEditorRole, wrap(DeleteDashboardSnapshot))
 
 
 	// api renew session based on remember cookie
 	// api renew session based on remember cookie
-	r.Get("/api/login/ping", quota("session"), LoginApiPing)
+	r.Get("/api/login/ping", quota("session"), LoginAPIPing)
 
 
 	// authed api
 	// authed api
 	r.Group("/api", func(apiRoute RouteRegister) {
 	r.Group("/api", func(apiRoute RouteRegister) {
@@ -139,7 +139,7 @@ func (hs *HttpServer) registerRoutes() {
 		apiRoute.Group("/users", func(usersRoute RouteRegister) {
 		apiRoute.Group("/users", func(usersRoute RouteRegister) {
 			usersRoute.Get("/", wrap(SearchUsers))
 			usersRoute.Get("/", wrap(SearchUsers))
 			usersRoute.Get("/search", wrap(SearchUsersWithPaging))
 			usersRoute.Get("/search", wrap(SearchUsersWithPaging))
-			usersRoute.Get("/:id", wrap(GetUserById))
+			usersRoute.Get("/:id", wrap(GetUserByID))
 			usersRoute.Get("/:id/orgs", wrap(GetUserOrgList))
 			usersRoute.Get("/:id/orgs", wrap(GetUserOrgList))
 			// query parameters /users/lookup?loginOrEmail=admin@example.com
 			// query parameters /users/lookup?loginOrEmail=admin@example.com
 			usersRoute.Get("/lookup", wrap(GetUserByLoginOrEmail))
 			usersRoute.Get("/lookup", wrap(GetUserByLoginOrEmail))
@@ -149,11 +149,11 @@ func (hs *HttpServer) registerRoutes() {
 
 
 		// team (admin permission required)
 		// team (admin permission required)
 		apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
 		apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
-			teamsRoute.Get("/:teamId", wrap(GetTeamById))
+			teamsRoute.Get("/:teamId", wrap(GetTeamByID))
 			teamsRoute.Get("/search", wrap(SearchTeams))
 			teamsRoute.Get("/search", wrap(SearchTeams))
 			teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
 			teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
 			teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
 			teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
-			teamsRoute.Delete("/:teamId", wrap(DeleteTeamById))
+			teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID))
 			teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
 			teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
 			teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
 			teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
 			teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
 			teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
@@ -192,10 +192,10 @@ func (hs *HttpServer) registerRoutes() {
 
 
 		// orgs (admin routes)
 		// orgs (admin routes)
 		apiRoute.Group("/orgs/:orgId", func(orgsRoute RouteRegister) {
 		apiRoute.Group("/orgs/:orgId", func(orgsRoute RouteRegister) {
-			orgsRoute.Get("/", wrap(GetOrgById))
+			orgsRoute.Get("/", wrap(GetOrgByID))
 			orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrg))
 			orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrg))
 			orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddress))
 			orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddress))
-			orgsRoute.Delete("/", wrap(DeleteOrgById))
+			orgsRoute.Delete("/", wrap(DeleteOrgByID))
 			orgsRoute.Get("/users", wrap(GetOrgUsers))
 			orgsRoute.Get("/users", wrap(GetOrgUsers))
 			orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser))
 			orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser))
 			orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser))
 			orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser))
@@ -211,9 +211,9 @@ func (hs *HttpServer) registerRoutes() {
 
 
 		// auth api keys
 		// auth api keys
 		apiRoute.Group("/auth/keys", func(keysRoute RouteRegister) {
 		apiRoute.Group("/auth/keys", func(keysRoute RouteRegister) {
-			keysRoute.Get("/", wrap(GetApiKeys))
-			keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey))
-			keysRoute.Delete("/:id", wrap(DeleteApiKey))
+			keysRoute.Get("/", wrap(GetAPIKeys))
+			keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddAPIKey))
+			keysRoute.Delete("/:id", wrap(DeleteAPIKey))
 		}, reqOrgAdmin)
 		}, reqOrgAdmin)
 
 
 		// Preferences
 		// Preferences
@@ -226,16 +226,16 @@ func (hs *HttpServer) registerRoutes() {
 			datasourceRoute.Get("/", wrap(GetDataSources))
 			datasourceRoute.Get("/", wrap(GetDataSources))
 			datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), wrap(AddDataSource))
 			datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), wrap(AddDataSource))
 			datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
 			datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
-			datasourceRoute.Delete("/:id", wrap(DeleteDataSourceById))
+			datasourceRoute.Delete("/:id", wrap(DeleteDataSourceByID))
 			datasourceRoute.Delete("/name/:name", wrap(DeleteDataSourceByName))
 			datasourceRoute.Delete("/name/:name", wrap(DeleteDataSourceByName))
-			datasourceRoute.Get("/:id", wrap(GetDataSourceById))
+			datasourceRoute.Get("/:id", wrap(GetDataSourceByID))
 			datasourceRoute.Get("/name/:name", wrap(GetDataSourceByName))
 			datasourceRoute.Get("/name/:name", wrap(GetDataSourceByName))
 		}, reqOrgAdmin)
 		}, reqOrgAdmin)
 
 
-		apiRoute.Get("/datasources/id/:name", wrap(GetDataSourceIdByName), reqSignedIn)
+		apiRoute.Get("/datasources/id/:name", wrap(GetDataSourceIDByName), reqSignedIn)
 
 
 		apiRoute.Get("/plugins", wrap(GetPluginList))
 		apiRoute.Get("/plugins", wrap(GetPluginList))
-		apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
+		apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingByID))
 		apiRoute.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
 		apiRoute.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
 
 
 		apiRoute.Group("/plugins", func(pluginRoute RouteRegister) {
 		apiRoute.Group("/plugins", func(pluginRoute RouteRegister) {
@@ -250,11 +250,11 @@ func (hs *HttpServer) registerRoutes() {
 		// Folders
 		// Folders
 		apiRoute.Group("/folders", func(folderRoute RouteRegister) {
 		apiRoute.Group("/folders", func(folderRoute RouteRegister) {
 			folderRoute.Get("/", wrap(GetFolders))
 			folderRoute.Get("/", wrap(GetFolders))
-			folderRoute.Get("/id/:id", wrap(GetFolderById))
+			folderRoute.Get("/id/:id", wrap(GetFolderByID))
 			folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder))
 			folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder))
 
 
 			folderRoute.Group("/:uid", func(folderUidRoute RouteRegister) {
 			folderRoute.Group("/:uid", func(folderUidRoute RouteRegister) {
-				folderUidRoute.Get("/", wrap(GetFolderByUid))
+				folderUidRoute.Get("/", wrap(GetFolderByUID))
 				folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder))
 				folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder))
 				folderUidRoute.Delete("/", wrap(DeleteFolder))
 				folderUidRoute.Delete("/", wrap(DeleteFolder))
 
 
@@ -268,7 +268,7 @@ func (hs *HttpServer) registerRoutes() {
 		// Dashboard
 		// Dashboard
 		apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
 		apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
 			dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
 			dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
-			dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUid))
+			dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUID))
 
 
 			dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
 			dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
 			dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard))
 			dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard))
@@ -314,7 +314,7 @@ func (hs *HttpServer) registerRoutes() {
 		// metrics
 		// metrics
 		apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
 		apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
 		apiRoute.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
 		apiRoute.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
-		apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSqlTestData))
+		apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSQLTestData))
 		apiRoute.Get("/tsdb/testdata/random-walk", wrap(GetTestDataRandomWalk))
 		apiRoute.Get("/tsdb/testdata/random-walk", wrap(GetTestDataRandomWalk))
 
 
 		apiRoute.Group("/alerts", func(alertsRoute RouteRegister) {
 		apiRoute.Group("/alerts", func(alertsRoute RouteRegister) {
@@ -332,7 +332,7 @@ func (hs *HttpServer) registerRoutes() {
 			alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
 			alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
 			alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
 			alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
 			alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
 			alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
-			alertNotifications.Get("/:notificationId", wrap(GetAlertNotificationById))
+			alertNotifications.Get("/:notificationId", wrap(GetAlertNotificationByID))
 			alertNotifications.Delete("/:notificationId", wrap(DeleteAlertNotification))
 			alertNotifications.Delete("/:notificationId", wrap(DeleteAlertNotification))
 		}, reqEditorRole)
 		}, reqEditorRole)
 
 
@@ -341,7 +341,7 @@ func (hs *HttpServer) registerRoutes() {
 
 
 		apiRoute.Group("/annotations", func(annotationsRoute RouteRegister) {
 		apiRoute.Group("/annotations", func(annotationsRoute RouteRegister) {
 			annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
 			annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
-			annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById))
+			annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationByID))
 			annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
 			annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
 			annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
 			annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
 			annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
 			annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))

+ 10 - 10
pkg/api/apikey.go

@@ -7,11 +7,11 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 )
 )
 
 
-func GetApiKeys(c *m.ReqContext) Response {
+func GetAPIKeys(c *m.ReqContext) Response {
 	query := m.GetApiKeysQuery{OrgId: c.OrgId}
 	query := m.GetApiKeysQuery{OrgId: c.OrgId}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to list api keys", err)
+		return Error(500, "Failed to list api keys", err)
 	}
 	}
 
 
 	result := make([]*m.ApiKeyDTO, len(query.Result))
 	result := make([]*m.ApiKeyDTO, len(query.Result))
@@ -23,25 +23,25 @@ func GetApiKeys(c *m.ReqContext) Response {
 		}
 		}
 	}
 	}
 
 
-	return Json(200, result)
+	return JSON(200, result)
 }
 }
 
 
-func DeleteApiKey(c *m.ReqContext) Response {
+func DeleteAPIKey(c *m.ReqContext) Response {
 	id := c.ParamsInt64(":id")
 	id := c.ParamsInt64(":id")
 
 
 	cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId}
 	cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId}
 
 
 	err := bus.Dispatch(cmd)
 	err := bus.Dispatch(cmd)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to delete API key", err)
+		return Error(500, "Failed to delete API key", err)
 	}
 	}
 
 
-	return ApiSuccess("API key deleted")
+	return Success("API key deleted")
 }
 }
 
 
-func AddApiKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response {
+func AddAPIKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response {
 	if !cmd.Role.IsValid() {
 	if !cmd.Role.IsValid() {
-		return ApiError(400, "Invalid role specified", nil)
+		return Error(400, "Invalid role specified", nil)
 	}
 	}
 
 
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
@@ -50,12 +50,12 @@ func AddApiKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response {
 	cmd.Key = newKeyInfo.HashedKey
 	cmd.Key = newKeyInfo.HashedKey
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to add API key", err)
+		return Error(500, "Failed to add API key", err)
 	}
 	}
 
 
 	result := &dtos.NewApiKeyResult{
 	result := &dtos.NewApiKeyResult{
 		Name: cmd.Result.Name,
 		Name: cmd.Result.Name,
 		Key:  newKeyInfo.ClientSecret}
 		Key:  newKeyInfo.ClientSecret}
 
 
-	return Json(200, result)
+	return JSON(200, result)
 }
 }

+ 2 - 2
pkg/api/app_routes.go

@@ -55,11 +55,11 @@ func InitAppPluginRoutes(r *macaron.Macaron) {
 	}
 	}
 }
 }
 
 
-func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler {
+func AppPluginRoute(route *plugins.AppPluginRoute, appID string) macaron.Handler {
 	return func(c *m.ReqContext) {
 	return func(c *m.ReqContext) {
 		path := c.Params("*")
 		path := c.Params("*")
 
 
-		proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId)
+		proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID)
 		proxy.Transport = pluginProxyTransport
 		proxy.Transport = pluginProxyTransport
 		proxy.ServeHTTP(c.Resp, c.Req.Request)
 		proxy.ServeHTTP(c.Resp, c.Req.Request)
 	}
 	}

+ 13 - 9
pkg/api/common.go

@@ -11,10 +11,10 @@ import (
 
 
 var (
 var (
 	NotFound = func() Response {
 	NotFound = func() Response {
-		return ApiError(404, "Not found", nil)
+		return Error(404, "Not found", nil)
 	}
 	}
 	ServerError = func(err error) Response {
 	ServerError = func(err error) Response {
-		return ApiError(500, "Server error", err)
+		return Error(500, "Server error", err)
 	}
 	}
 )
 )
 
 
@@ -67,22 +67,25 @@ func (r *NormalResponse) Header(key, value string) *NormalResponse {
 	return r
 	return r
 }
 }
 
 
-// functions to create responses
+// Empty create an empty response
 func Empty(status int) *NormalResponse {
 func Empty(status int) *NormalResponse {
 	return Respond(status, nil)
 	return Respond(status, nil)
 }
 }
 
 
-func Json(status int, body interface{}) *NormalResponse {
+// JSON create a JSON response
+func JSON(status int, body interface{}) *NormalResponse {
 	return Respond(status, body).Header("Content-Type", "application/json")
 	return Respond(status, body).Header("Content-Type", "application/json")
 }
 }
 
 
-func ApiSuccess(message string) *NormalResponse {
+// Success create a successful response
+func Success(message string) *NormalResponse {
 	resp := make(map[string]interface{})
 	resp := make(map[string]interface{})
 	resp["message"] = message
 	resp["message"] = message
-	return Json(200, resp)
+	return JSON(200, resp)
 }
 }
 
 
-func ApiError(status int, message string, err error) *NormalResponse {
+// Error create a erroneous response
+func Error(status int, message string, err error) *NormalResponse {
 	data := make(map[string]interface{})
 	data := make(map[string]interface{})
 
 
 	switch status {
 	switch status {
@@ -102,7 +105,7 @@ func ApiError(status int, message string, err error) *NormalResponse {
 		}
 		}
 	}
 	}
 
 
-	resp := Json(status, data)
+	resp := JSON(status, data)
 
 
 	if err != nil {
 	if err != nil {
 		resp.errMessage = message
 		resp.errMessage = message
@@ -112,6 +115,7 @@ func ApiError(status int, message string, err error) *NormalResponse {
 	return resp
 	return resp
 }
 }
 
 
+// Respond create a response
 func Respond(status int, body interface{}) *NormalResponse {
 func Respond(status int, body interface{}) *NormalResponse {
 	var b []byte
 	var b []byte
 	var err error
 	var err error
@@ -122,7 +126,7 @@ func Respond(status int, body interface{}) *NormalResponse {
 		b = []byte(t)
 		b = []byte(t)
 	default:
 	default:
 		if b, err = json.Marshal(body); err != nil {
 		if b, err = json.Marshal(body); err != nil {
-			return ApiError(500, "body json marshal", err)
+			return Error(500, "body json marshal", err)
 		}
 		}
 	}
 	}
 	return &NormalResponse{
 	return &NormalResponse{

+ 1 - 1
pkg/api/common_test.go

@@ -99,7 +99,7 @@ func setupScenarioContext(url string) *scenarioContext {
 	}))
 	}))
 
 
 	sc.m.Use(middleware.GetContextHandler())
 	sc.m.Use(middleware.GetContextHandler())
-	sc.m.Use(middleware.Sessioner(&session.Options{}))
+	sc.m.Use(middleware.Sessioner(&session.Options{}, 0))
 
 
 	return sc
 	return sc
 }
 }

+ 52 - 55
pkg/api/dashboard.go

@@ -22,12 +22,12 @@ import (
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
-func isDashboardStarredByUser(c *m.ReqContext, dashId int64) (bool, error) {
+func isDashboardStarredByUser(c *m.ReqContext, dashID int64) (bool, error) {
 	if !c.IsSignedIn {
 	if !c.IsSignedIn {
 		return false, nil
 		return false, nil
 	}
 	}
 
 
-	query := m.IsStarredByUserQuery{UserId: c.UserId, DashboardId: dashId}
+	query := m.IsStarredByUserQuery{UserId: c.UserId, DashboardId: dashID}
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		return false, err
 		return false, err
 	}
 	}
@@ -37,10 +37,10 @@ func isDashboardStarredByUser(c *m.ReqContext, dashId int64) (bool, error) {
 
 
 func dashboardGuardianResponse(err error) Response {
 func dashboardGuardianResponse(err error) Response {
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Error while checking dashboard permissions", err)
+		return Error(500, "Error while checking dashboard permissions", err)
 	}
 	}
 
 
-	return ApiError(403, "Access denied to this dashboard", nil)
+	return Error(403, "Access denied to this dashboard", nil)
 }
 }
 
 
 func GetDashboard(c *m.ReqContext) Response {
 func GetDashboard(c *m.ReqContext) Response {
@@ -60,7 +60,7 @@ func GetDashboard(c *m.ReqContext) Response {
 
 
 	isStarred, err := isDashboardStarredByUser(c, dash.Id)
 	isStarred, err := isDashboardStarredByUser(c, dash.Id)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Error while checking if dashboard was starred by user", err)
+		return Error(500, "Error while checking if dashboard was starred by user", err)
 	}
 	}
 
 
 	// Finding creator and last updater of the dashboard
 	// Finding creator and last updater of the dashboard
@@ -96,7 +96,7 @@ func GetDashboard(c *m.ReqContext) Response {
 	if dash.FolderId > 0 {
 	if dash.FolderId > 0 {
 		query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
 		query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
 		if err := bus.Dispatch(&query); err != nil {
 		if err := bus.Dispatch(&query); err != nil {
-			return ApiError(500, "Dashboard folder could not be read", err)
+			return Error(500, "Dashboard folder could not be read", err)
 		}
 		}
 		meta.FolderTitle = query.Result.Title
 		meta.FolderTitle = query.Result.Title
 		meta.FolderUrl = query.Result.GetUrl()
 		meta.FolderUrl = query.Result.GetUrl()
@@ -111,31 +111,29 @@ func GetDashboard(c *m.ReqContext) Response {
 	}
 	}
 
 
 	c.TimeRequest(metrics.M_Api_Dashboard_Get)
 	c.TimeRequest(metrics.M_Api_Dashboard_Get)
-	return Json(200, dto)
+	return JSON(200, dto)
 }
 }
 
 
-func getUserLogin(userId int64) string {
-	query := m.GetUserByIdQuery{Id: userId}
+func getUserLogin(userID int64) string {
+	query := m.GetUserByIdQuery{Id: userID}
 	err := bus.Dispatch(&query)
 	err := bus.Dispatch(&query)
 	if err != nil {
 	if err != nil {
 		return "Anonymous"
 		return "Anonymous"
-	} else {
-		user := query.Result
-		return user.Login
 	}
 	}
+	return query.Result.Login
 }
 }
 
 
-func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dashboard, Response) {
+func getDashboardHelper(orgID int64, slug string, id int64, uid string) (*m.Dashboard, Response) {
 	var query m.GetDashboardQuery
 	var query m.GetDashboardQuery
 
 
 	if len(uid) > 0 {
 	if len(uid) > 0 {
-		query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgId}
+		query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgID}
 	} else {
 	} else {
-		query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId}
+		query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgID}
 	}
 	}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return nil, ApiError(404, "Dashboard not found", err)
+		return nil, Error(404, "Dashboard not found", err)
 	}
 	}
 
 
 	return query.Result, nil
 	return query.Result, nil
@@ -145,11 +143,11 @@ func DeleteDashboard(c *m.ReqContext) Response {
 	query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")}
 	query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to retrieve dashboards by slug", err)
+		return Error(500, "Failed to retrieve dashboards by slug", err)
 	}
 	}
 
 
 	if len(query.Result) > 1 {
 	if len(query.Result) > 1 {
-		return Json(412, util.DynMap{"status": "multiple-slugs-exists", "message": m.ErrDashboardsWithSameSlugExists.Error()})
+		return JSON(412, util.DynMap{"status": "multiple-slugs-exists", "message": m.ErrDashboardsWithSameSlugExists.Error()})
 	}
 	}
 
 
 	dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "")
 	dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "")
@@ -164,16 +162,16 @@ func DeleteDashboard(c *m.ReqContext) Response {
 
 
 	cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
 	cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to delete dashboard", err)
+		return Error(500, "Failed to delete dashboard", err)
 	}
 	}
 
 
-	return Json(200, util.DynMap{
+	return JSON(200, util.DynMap{
 		"title":   dash.Title,
 		"title":   dash.Title,
 		"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
 		"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
 	})
 	})
 }
 }
 
 
-func DeleteDashboardByUid(c *m.ReqContext) Response {
+func DeleteDashboardByUID(c *m.ReqContext) Response {
 	dash, rsp := getDashboardHelper(c.OrgId, "", 0, c.Params(":uid"))
 	dash, rsp := getDashboardHelper(c.OrgId, "", 0, c.Params(":uid"))
 	if rsp != nil {
 	if rsp != nil {
 		return rsp
 		return rsp
@@ -186,10 +184,10 @@ func DeleteDashboardByUid(c *m.ReqContext) Response {
 
 
 	cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
 	cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to delete dashboard", err)
+		return Error(500, "Failed to delete dashboard", err)
 	}
 	}
 
 
-	return Json(200, util.DynMap{
+	return JSON(200, util.DynMap{
 		"title":   dash.Title,
 		"title":   dash.Title,
 		"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
 		"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
 	})
 	})
@@ -204,10 +202,10 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
 	if dash.Id == 0 && dash.Uid == "" {
 	if dash.Id == 0 && dash.Uid == "" {
 		limitReached, err := quota.QuotaReached(c, "dashboard")
 		limitReached, err := quota.QuotaReached(c, "dashboard")
 		if err != nil {
 		if err != nil {
-			return ApiError(500, "failed to get quota", err)
+			return Error(500, "failed to get quota", err)
 		}
 		}
 		if limitReached {
 		if limitReached {
-			return ApiError(403, "Quota reached", nil)
+			return Error(403, "Quota reached", nil)
 		}
 		}
 	}
 	}
 
 
@@ -231,23 +229,23 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
 		err == m.ErrFolderNotFound ||
 		err == m.ErrFolderNotFound ||
 		err == m.ErrDashboardFolderCannotHaveParent ||
 		err == m.ErrDashboardFolderCannotHaveParent ||
 		err == m.ErrDashboardFolderNameExists {
 		err == m.ErrDashboardFolderNameExists {
-		return ApiError(400, err.Error(), nil)
+		return Error(400, err.Error(), nil)
 	}
 	}
 
 
 	if err == m.ErrDashboardUpdateAccessDenied {
 	if err == m.ErrDashboardUpdateAccessDenied {
-		return ApiError(403, err.Error(), err)
+		return Error(403, err.Error(), err)
 	}
 	}
 
 
 	if err == m.ErrDashboardContainsInvalidAlertData {
 	if err == m.ErrDashboardContainsInvalidAlertData {
-		return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
+		return Error(500, "Invalid alert data. Cannot save dashboard", err)
 	}
 	}
 
 
 	if err != nil {
 	if err != nil {
 		if err == m.ErrDashboardWithSameNameInFolderExists {
 		if err == m.ErrDashboardWithSameNameInFolderExists {
-			return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
+			return JSON(412, util.DynMap{"status": "name-exists", "message": err.Error()})
 		}
 		}
 		if err == m.ErrDashboardVersionMismatch {
 		if err == m.ErrDashboardVersionMismatch {
-			return Json(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
+			return JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
 		}
 		}
 		if pluginErr, ok := err.(m.UpdatePluginDashboardError); ok {
 		if pluginErr, ok := err.(m.UpdatePluginDashboardError); ok {
 			message := "The dashboard belongs to plugin " + pluginErr.PluginId + "."
 			message := "The dashboard belongs to plugin " + pluginErr.PluginId + "."
@@ -255,20 +253,20 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
 			if pluginDef, exist := plugins.Plugins[pluginErr.PluginId]; exist {
 			if pluginDef, exist := plugins.Plugins[pluginErr.PluginId]; exist {
 				message = "The dashboard belongs to plugin " + pluginDef.Name + "."
 				message = "The dashboard belongs to plugin " + pluginDef.Name + "."
 			}
 			}
-			return Json(412, util.DynMap{"status": "plugin-dashboard", "message": message})
+			return JSON(412, util.DynMap{"status": "plugin-dashboard", "message": message})
 		}
 		}
 		if err == m.ErrDashboardNotFound {
 		if err == m.ErrDashboardNotFound {
-			return Json(404, util.DynMap{"status": "not-found", "message": err.Error()})
+			return JSON(404, util.DynMap{"status": "not-found", "message": err.Error()})
 		}
 		}
-		return ApiError(500, "Failed to save dashboard", err)
+		return Error(500, "Failed to save dashboard", err)
 	}
 	}
 
 
 	if err == m.ErrDashboardFailedToUpdateAlertData {
 	if err == m.ErrDashboardFailedToUpdateAlertData {
-		return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
+		return Error(500, "Invalid alert data. Cannot save dashboard", err)
 	}
 	}
 
 
 	c.TimeRequest(metrics.M_Api_Dashboard_Save)
 	c.TimeRequest(metrics.M_Api_Dashboard_Save)
-	return Json(200, util.DynMap{
+	return JSON(200, util.DynMap{
 		"status":  "success",
 		"status":  "success",
 		"slug":    dashboard.Slug,
 		"slug":    dashboard.Slug,
 		"version": dashboard.Version,
 		"version": dashboard.Version,
@@ -281,7 +279,7 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
 func GetHomeDashboard(c *m.ReqContext) Response {
 func GetHomeDashboard(c *m.ReqContext) Response {
 	prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
 	prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
 	if err := bus.Dispatch(&prefsQuery); err != nil {
 	if err := bus.Dispatch(&prefsQuery); err != nil {
-		return ApiError(500, "Failed to get preferences", err)
+		return Error(500, "Failed to get preferences", err)
 	}
 	}
 
 
 	if prefsQuery.Result.HomeDashboardId != 0 {
 	if prefsQuery.Result.HomeDashboardId != 0 {
@@ -290,16 +288,15 @@ func GetHomeDashboard(c *m.ReqContext) Response {
 		if err == nil {
 		if err == nil {
 			url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug)
 			url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug)
 			dashRedirect := dtos.DashboardRedirect{RedirectUri: url}
 			dashRedirect := dtos.DashboardRedirect{RedirectUri: url}
-			return Json(200, &dashRedirect)
-		} else {
-			log.Warn("Failed to get slug from database, %s", err.Error())
+			return JSON(200, &dashRedirect)
 		}
 		}
+		log.Warn("Failed to get slug from database, %s", err.Error())
 	}
 	}
 
 
 	filePath := path.Join(setting.StaticRootPath, "dashboards/home.json")
 	filePath := path.Join(setting.StaticRootPath, "dashboards/home.json")
 	file, err := os.Open(filePath)
 	file, err := os.Open(filePath)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to load home dashboard", err)
+		return Error(500, "Failed to load home dashboard", err)
 	}
 	}
 
 
 	dash := dtos.DashboardFullWithMeta{}
 	dash := dtos.DashboardFullWithMeta{}
@@ -309,14 +306,14 @@ func GetHomeDashboard(c *m.ReqContext) Response {
 
 
 	jsonParser := json.NewDecoder(file)
 	jsonParser := json.NewDecoder(file)
 	if err := jsonParser.Decode(&dash.Dashboard); err != nil {
 	if err := jsonParser.Decode(&dash.Dashboard); err != nil {
-		return ApiError(500, "Failed to load home dashboard", err)
+		return Error(500, "Failed to load home dashboard", err)
 	}
 	}
 
 
 	if c.HasUserRole(m.ROLE_ADMIN) && !c.HasHelpFlag(m.HelpFlagGettingStartedPanelDismissed) {
 	if c.HasUserRole(m.ROLE_ADMIN) && !c.HasHelpFlag(m.HelpFlagGettingStartedPanelDismissed) {
 		addGettingStartedPanelToHomeDashboard(dash.Dashboard)
 		addGettingStartedPanelToHomeDashboard(dash.Dashboard)
 	}
 	}
 
 
-	return Json(200, &dash)
+	return JSON(200, &dash)
 }
 }
 
 
 func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
 func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
@@ -339,22 +336,22 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
 
 
 // GetDashboardVersions returns all dashboard versions as JSON
 // GetDashboardVersions returns all dashboard versions as JSON
 func GetDashboardVersions(c *m.ReqContext) Response {
 func GetDashboardVersions(c *m.ReqContext) Response {
-	dashId := c.ParamsInt64(":dashboardId")
+	dashID := c.ParamsInt64(":dashboardId")
 
 
-	guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(dashID, c.OrgId, c.SignedInUser)
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
 
 
 	query := m.GetDashboardVersionsQuery{
 	query := m.GetDashboardVersionsQuery{
 		OrgId:       c.OrgId,
 		OrgId:       c.OrgId,
-		DashboardId: dashId,
+		DashboardId: dashID,
 		Limit:       c.QueryInt("limit"),
 		Limit:       c.QueryInt("limit"),
 		Start:       c.QueryInt("start"),
 		Start:       c.QueryInt("start"),
 	}
 	}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashId), err)
+		return Error(404, fmt.Sprintf("No versions found for dashboardId %d", dashID), err)
 	}
 	}
 
 
 	for _, version := range query.Result {
 	for _, version := range query.Result {
@@ -373,26 +370,26 @@ func GetDashboardVersions(c *m.ReqContext) Response {
 		}
 		}
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
 // GetDashboardVersion returns the dashboard version with the given ID.
 // GetDashboardVersion returns the dashboard version with the given ID.
 func GetDashboardVersion(c *m.ReqContext) Response {
 func GetDashboardVersion(c *m.ReqContext) Response {
-	dashId := c.ParamsInt64(":dashboardId")
+	dashID := c.ParamsInt64(":dashboardId")
 
 
-	guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(dashID, c.OrgId, c.SignedInUser)
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 	if canSave, err := guardian.CanSave(); err != nil || !canSave {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
 
 
 	query := m.GetDashboardVersionQuery{
 	query := m.GetDashboardVersionQuery{
 		OrgId:       c.OrgId,
 		OrgId:       c.OrgId,
-		DashboardId: dashId,
+		DashboardId: dashID,
 		Version:     c.ParamsInt(":id"),
 		Version:     c.ParamsInt(":id"),
 	}
 	}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashId), err)
+		return Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err)
 	}
 	}
 
 
 	creator := "Anonymous"
 	creator := "Anonymous"
@@ -405,7 +402,7 @@ func GetDashboardVersion(c *m.ReqContext) Response {
 		CreatedBy:        creator,
 		CreatedBy:        creator,
 	}
 	}
 
 
-	return Json(200, dashVersionMeta)
+	return JSON(200, dashVersionMeta)
 }
 }
 
 
 // POST /api/dashboards/calculate-diff performs diffs on two dashboards
 // POST /api/dashboards/calculate-diff performs diffs on two dashboards
@@ -441,9 +438,9 @@ func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOption
 	result, err := dashdiffs.CalculateDiff(&options)
 	result, err := dashdiffs.CalculateDiff(&options)
 	if err != nil {
 	if err != nil {
 		if err == m.ErrDashboardVersionNotFound {
 		if err == m.ErrDashboardVersionNotFound {
-			return ApiError(404, "Dashboard version not found", err)
+			return Error(404, "Dashboard version not found", err)
 		}
 		}
-		return ApiError(500, "Unable to compute diff", err)
+		return Error(500, "Unable to compute diff", err)
 	}
 	}
 
 
 	if options.DiffType == dashdiffs.DiffDelta {
 	if options.DiffType == dashdiffs.DiffDelta {
@@ -467,7 +464,7 @@ func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersio
 
 
 	versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId}
 	versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId}
 	if err := bus.Dispatch(&versionQuery); err != nil {
 	if err := bus.Dispatch(&versionQuery); err != nil {
-		return ApiError(404, "Dashboard version not found", nil)
+		return Error(404, "Dashboard version not found", nil)
 	}
 	}
 
 
 	version := versionQuery.Result
 	version := versionQuery.Result

+ 16 - 16
pkg/api/dashboard_permission.go

@@ -10,14 +10,14 @@ import (
 )
 )
 
 
 func GetDashboardPermissionList(c *m.ReqContext) Response {
 func GetDashboardPermissionList(c *m.ReqContext) Response {
-	dashId := c.ParamsInt64(":dashboardId")
+	dashID := c.ParamsInt64(":dashboardId")
 
 
-	_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
+	_, rsp := getDashboardHelper(c.OrgId, "", dashID, "")
 	if rsp != nil {
 	if rsp != nil {
 		return rsp
 		return rsp
 	}
 	}
 
 
-	g := guardian.New(dashId, c.OrgId, c.SignedInUser)
+	g := guardian.New(dashID, c.OrgId, c.SignedInUser)
 
 
 	if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
 	if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
@@ -25,7 +25,7 @@ func GetDashboardPermissionList(c *m.ReqContext) Response {
 
 
 	acl, err := g.GetAcl()
 	acl, err := g.GetAcl()
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to get dashboard permissions", err)
+		return Error(500, "Failed to get dashboard permissions", err)
 	}
 	}
 
 
 	for _, perm := range acl {
 	for _, perm := range acl {
@@ -34,29 +34,29 @@ func GetDashboardPermissionList(c *m.ReqContext) Response {
 		}
 		}
 	}
 	}
 
 
-	return Json(200, acl)
+	return JSON(200, acl)
 }
 }
 
 
 func UpdateDashboardPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
 func UpdateDashboardPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
-	dashId := c.ParamsInt64(":dashboardId")
+	dashID := c.ParamsInt64(":dashboardId")
 
 
-	_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
+	_, rsp := getDashboardHelper(c.OrgId, "", dashID, "")
 	if rsp != nil {
 	if rsp != nil {
 		return rsp
 		return rsp
 	}
 	}
 
 
-	g := guardian.New(dashId, c.OrgId, c.SignedInUser)
+	g := guardian.New(dashID, c.OrgId, c.SignedInUser)
 	if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
 	if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
 	}
 	}
 
 
 	cmd := m.UpdateDashboardAclCommand{}
 	cmd := m.UpdateDashboardAclCommand{}
-	cmd.DashboardId = dashId
+	cmd.DashboardId = dashID
 
 
 	for _, item := range apiCmd.Items {
 	for _, item := range apiCmd.Items {
 		cmd.Items = append(cmd.Items, &m.DashboardAcl{
 		cmd.Items = append(cmd.Items, &m.DashboardAcl{
 			OrgId:       c.OrgId,
 			OrgId:       c.OrgId,
-			DashboardId: dashId,
+			DashboardId: dashID,
 			UserId:      item.UserId,
 			UserId:      item.UserId,
 			TeamId:      item.TeamId,
 			TeamId:      item.TeamId,
 			Role:        item.Role,
 			Role:        item.Role,
@@ -70,21 +70,21 @@ func UpdateDashboardPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclC
 		if err != nil {
 		if err != nil {
 			if err == guardian.ErrGuardianPermissionExists ||
 			if err == guardian.ErrGuardianPermissionExists ||
 				err == guardian.ErrGuardianOverride {
 				err == guardian.ErrGuardianOverride {
-				return ApiError(400, err.Error(), err)
+				return Error(400, err.Error(), err)
 			}
 			}
 
 
-			return ApiError(500, "Error while checking dashboard permissions", err)
+			return Error(500, "Error while checking dashboard permissions", err)
 		}
 		}
 
 
-		return ApiError(403, "Cannot remove own admin permission for a folder", nil)
+		return Error(403, "Cannot remove own admin permission for a folder", nil)
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		if err == m.ErrDashboardAclInfoMissing || err == m.ErrDashboardPermissionDashboardEmpty {
 		if err == m.ErrDashboardAclInfoMissing || err == m.ErrDashboardPermissionDashboardEmpty {
-			return ApiError(409, err.Error(), err)
+			return Error(409, err.Error(), err)
 		}
 		}
-		return ApiError(500, "Failed to create permission", err)
+		return Error(500, "Failed to create permission", err)
 	}
 	}
 
 
-	return ApiSuccess("Dashboard permissions updated")
+	return Success("Dashboard permissions updated")
 }
 }

+ 10 - 10
pkg/api/dashboard_snapshot.go

@@ -99,32 +99,32 @@ func DeleteDashboardSnapshot(c *m.ReqContext) Response {
 
 
 	err := bus.Dispatch(query)
 	err := bus.Dispatch(query)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to get dashboard snapshot", err)
+		return Error(500, "Failed to get dashboard snapshot", err)
 	}
 	}
 
 
 	if query.Result == nil {
 	if query.Result == nil {
-		return ApiError(404, "Failed to get dashboard snapshot", nil)
+		return Error(404, "Failed to get dashboard snapshot", nil)
 	}
 	}
 	dashboard := query.Result.Dashboard
 	dashboard := query.Result.Dashboard
-	dashboardId := dashboard.Get("id").MustInt64()
+	dashboardID := dashboard.Get("id").MustInt64()
 
 
-	guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser)
+	guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
 	canEdit, err := guardian.CanEdit()
 	canEdit, err := guardian.CanEdit()
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Error while checking permissions for snapshot", err)
+		return Error(500, "Error while checking permissions for snapshot", err)
 	}
 	}
 
 
 	if !canEdit && query.Result.UserId != c.SignedInUser.UserId {
 	if !canEdit && query.Result.UserId != c.SignedInUser.UserId {
-		return ApiError(403, "Access denied to this snapshot", nil)
+		return Error(403, "Access denied to this snapshot", nil)
 	}
 	}
 
 
 	cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: key}
 	cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: key}
 
 
 	if err := bus.Dispatch(cmd); err != nil {
 	if err := bus.Dispatch(cmd); err != nil {
-		return ApiError(500, "Failed to delete dashboard snapshot", err)
+		return Error(500, "Failed to delete dashboard snapshot", err)
 	}
 	}
 
 
-	return Json(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."})
+	return JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."})
 }
 }
 
 
 // GET /api/dashboard/snapshots
 // GET /api/dashboard/snapshots
@@ -145,7 +145,7 @@ func SearchDashboardSnapshots(c *m.ReqContext) Response {
 
 
 	err := bus.Dispatch(&searchQuery)
 	err := bus.Dispatch(&searchQuery)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Search failed", err)
+		return Error(500, "Search failed", err)
 	}
 	}
 
 
 	dtos := make([]*m.DashboardSnapshotDTO, len(searchQuery.Result))
 	dtos := make([]*m.DashboardSnapshotDTO, len(searchQuery.Result))
@@ -165,5 +165,5 @@ func SearchDashboardSnapshots(c *m.ReqContext) Response {
 		}
 		}
 	}
 	}
 
 
-	return Json(200, dtos)
+	return JSON(200, dtos)
 }
 }

+ 13 - 13
pkg/api/dashboard_test.go

@@ -105,7 +105,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			})
 			})
 
 
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
-				CallDeleteDashboardByUid(sc)
+				CallDeleteDashboardByUID(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
 
 
 				Convey("Should lookup dashboard by uid", func() {
 				Convey("Should lookup dashboard by uid", func() {
@@ -165,7 +165,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			})
 			})
 
 
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
-				CallDeleteDashboardByUid(sc)
+				CallDeleteDashboardByUID(sc)
 				So(sc.resp.Code, ShouldEqual, 200)
 				So(sc.resp.Code, ShouldEqual, 200)
 
 
 				Convey("Should lookup dashboard by uid", func() {
 				Convey("Should lookup dashboard by uid", func() {
@@ -271,7 +271,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			})
 			})
 
 
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
-				CallDeleteDashboardByUid(sc)
+				CallDeleteDashboardByUID(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
 
 
 				Convey("Should lookup dashboard by uid", func() {
 				Convey("Should lookup dashboard by uid", func() {
@@ -329,7 +329,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			})
 			})
 
 
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
-				CallDeleteDashboardByUid(sc)
+				CallDeleteDashboardByUID(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
 
 
 				Convey("Should lookup dashboard by uid", func() {
 				Convey("Should lookup dashboard by uid", func() {
@@ -398,7 +398,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			})
 			})
 
 
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
-				CallDeleteDashboardByUid(sc)
+				CallDeleteDashboardByUID(sc)
 				So(sc.resp.Code, ShouldEqual, 200)
 				So(sc.resp.Code, ShouldEqual, 200)
 
 
 				Convey("Should lookup dashboard by uid", func() {
 				Convey("Should lookup dashboard by uid", func() {
@@ -468,7 +468,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			})
 			})
 
 
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
-				CallDeleteDashboardByUid(sc)
+				CallDeleteDashboardByUID(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
 
 
 				Convey("Should lookup dashboard by uid", func() {
 				Convey("Should lookup dashboard by uid", func() {
@@ -527,7 +527,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			})
 			})
 
 
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
-				CallDeleteDashboardByUid(sc)
+				CallDeleteDashboardByUID(sc)
 				So(sc.resp.Code, ShouldEqual, 200)
 				So(sc.resp.Code, ShouldEqual, 200)
 
 
 				Convey("Should lookup dashboard by uid", func() {
 				Convey("Should lookup dashboard by uid", func() {
@@ -594,7 +594,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			})
 			})
 
 
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
 			loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
-				CallDeleteDashboardByUid(sc)
+				CallDeleteDashboardByUID(sc)
 				So(sc.resp.Code, ShouldEqual, 403)
 				So(sc.resp.Code, ShouldEqual, 403)
 
 
 				Convey("Should lookup dashboard by uid", func() {
 				Convey("Should lookup dashboard by uid", func() {
@@ -638,7 +638,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 
 
 			Convey("Should result in 412 Precondition failed", func() {
 			Convey("Should result in 412 Precondition failed", func() {
 				So(sc.resp.Code, ShouldEqual, 412)
 				So(sc.resp.Code, ShouldEqual, 412)
-				result := sc.ToJson()
+				result := sc.ToJSON()
 				So(result.Get("status").MustString(), ShouldEqual, "multiple-slugs-exists")
 				So(result.Get("status").MustString(), ShouldEqual, "multiple-slugs-exists")
 				So(result.Get("message").MustString(), ShouldEqual, m.ErrDashboardsWithSameSlugExists.Error())
 				So(result.Get("message").MustString(), ShouldEqual, m.ErrDashboardsWithSameSlugExists.Error())
 			})
 			})
@@ -686,7 +686,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				})
 				})
 
 
 				Convey("It should return correct response data", func() {
 				Convey("It should return correct response data", func() {
-					result := sc.ToJson()
+					result := sc.ToJSON()
 					So(result.Get("status").MustString(), ShouldEqual, "success")
 					So(result.Get("status").MustString(), ShouldEqual, "success")
 					So(result.Get("id").MustInt64(), ShouldEqual, 2)
 					So(result.Get("id").MustInt64(), ShouldEqual, 2)
 					So(result.Get("uid").MustString(), ShouldEqual, "uid")
 					So(result.Get("uid").MustString(), ShouldEqual, "uid")
@@ -837,12 +837,12 @@ func CallDeleteDashboard(sc *scenarioContext) {
 	sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 	sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 }
 }
 
 
-func CallDeleteDashboardByUid(sc *scenarioContext) {
+func CallDeleteDashboardByUID(sc *scenarioContext) {
 	bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
 	bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
 		return nil
 		return nil
 	})
 	})
 
 
-	sc.handlerFunc = DeleteDashboardByUid
+	sc.handlerFunc = DeleteDashboardByUID
 	sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 	sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
 }
 }
 
 
@@ -903,7 +903,7 @@ func postDiffScenario(desc string, url string, routePattern string, cmd dtos.Cal
 	})
 	})
 }
 }
 
 
-func (sc *scenarioContext) ToJson() *simplejson.Json {
+func (sc *scenarioContext) ToJSON() *simplejson.Json {
 	var result *simplejson.Json
 	var result *simplejson.Json
 	err := json.NewDecoder(sc.resp.Body).Decode(&result)
 	err := json.NewDecoder(sc.resp.Body).Decode(&result)
 	So(err, ShouldBeNil)
 	So(err, ShouldBeNil)

+ 5 - 5
pkg/api/dataproxy.go

@@ -13,19 +13,19 @@ import (
 
 
 const HeaderNameNoBackendCache = "X-Grafana-NoCache"
 const HeaderNameNoBackendCache = "X-Grafana-NoCache"
 
 
-func (hs *HttpServer) getDatasourceById(id int64, orgId int64, nocache bool) (*m.DataSource, error) {
+func (hs *HTTPServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m.DataSource, error) {
 	cacheKey := fmt.Sprintf("ds-%d", id)
 	cacheKey := fmt.Sprintf("ds-%d", id)
 
 
 	if !nocache {
 	if !nocache {
 		if cached, found := hs.cache.Get(cacheKey); found {
 		if cached, found := hs.cache.Get(cacheKey); found {
 			ds := cached.(*m.DataSource)
 			ds := cached.(*m.DataSource)
-			if ds.OrgId == orgId {
+			if ds.OrgId == orgID {
 				return ds, nil
 				return ds, nil
 			}
 			}
 		}
 		}
 	}
 	}
 
 
-	query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgId}
+	query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgID}
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -34,12 +34,12 @@ func (hs *HttpServer) getDatasourceById(id int64, orgId int64, nocache bool) (*m
 	return query.Result, nil
 	return query.Result, nil
 }
 }
 
 
-func (hs *HttpServer) ProxyDataSourceRequest(c *m.ReqContext) {
+func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
 	c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
 	c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
 
 
 	nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
 	nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
 
 
-	ds, err := hs.getDatasourceById(c.ParamsInt64(":id"), c.OrgId, nocache)
+	ds, err := hs.getDatasourceByID(c.ParamsInt64(":id"), c.OrgId, nocache)
 
 
 	if err != nil {
 	if err != nil {
 		c.JsonApiErr(500, "Unable to load datasource meta data", err)
 		c.JsonApiErr(500, "Unable to load datasource meta data", err)

+ 39 - 40
pkg/api/datasources.go

@@ -14,7 +14,7 @@ func GetDataSources(c *m.ReqContext) Response {
 	query := m.GetDataSourcesQuery{OrgId: c.OrgId}
 	query := m.GetDataSourcesQuery{OrgId: c.OrgId}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to query datasources", err)
+		return Error(500, "Failed to query datasources", err)
 	}
 	}
 
 
 	result := make(dtos.DataSourceList, 0)
 	result := make(dtos.DataSourceList, 0)
@@ -46,10 +46,10 @@ func GetDataSources(c *m.ReqContext) Response {
 
 
 	sort.Sort(result)
 	sort.Sort(result)
 
 
-	return Json(200, &result)
+	return JSON(200, &result)
 }
 }
 
 
-func GetDataSourceById(c *m.ReqContext) Response {
+func GetDataSourceByID(c *m.ReqContext) Response {
 	query := m.GetDataSourceByIdQuery{
 	query := m.GetDataSourceByIdQuery{
 		Id:    c.ParamsInt64(":id"),
 		Id:    c.ParamsInt64(":id"),
 		OrgId: c.OrgId,
 		OrgId: c.OrgId,
@@ -57,66 +57,66 @@ func GetDataSourceById(c *m.ReqContext) Response {
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrDataSourceNotFound {
 		if err == m.ErrDataSourceNotFound {
-			return ApiError(404, "Data source not found", nil)
+			return Error(404, "Data source not found", nil)
 		}
 		}
-		return ApiError(500, "Failed to query datasources", err)
+		return Error(500, "Failed to query datasources", err)
 	}
 	}
 
 
 	ds := query.Result
 	ds := query.Result
 	dtos := convertModelToDtos(ds)
 	dtos := convertModelToDtos(ds)
 
 
-	return Json(200, &dtos)
+	return JSON(200, &dtos)
 }
 }
 
 
-func DeleteDataSourceById(c *m.ReqContext) Response {
+func DeleteDataSourceByID(c *m.ReqContext) Response {
 	id := c.ParamsInt64(":id")
 	id := c.ParamsInt64(":id")
 
 
 	if id <= 0 {
 	if id <= 0 {
-		return ApiError(400, "Missing valid datasource id", nil)
+		return Error(400, "Missing valid datasource id", nil)
 	}
 	}
 
 
-	ds, err := getRawDataSourceById(id, c.OrgId)
+	ds, err := getRawDataSourceByID(id, c.OrgId)
 	if err != nil {
 	if err != nil {
-		return ApiError(400, "Failed to delete datasource", nil)
+		return Error(400, "Failed to delete datasource", nil)
 	}
 	}
 
 
 	if ds.ReadOnly {
 	if ds.ReadOnly {
-		return ApiError(403, "Cannot delete read-only data source", nil)
+		return Error(403, "Cannot delete read-only data source", nil)
 	}
 	}
 
 
 	cmd := &m.DeleteDataSourceByIdCommand{Id: id, OrgId: c.OrgId}
 	cmd := &m.DeleteDataSourceByIdCommand{Id: id, OrgId: c.OrgId}
 
 
 	err = bus.Dispatch(cmd)
 	err = bus.Dispatch(cmd)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to delete datasource", err)
+		return Error(500, "Failed to delete datasource", err)
 	}
 	}
 
 
-	return ApiSuccess("Data source deleted")
+	return Success("Data source deleted")
 }
 }
 
 
 func DeleteDataSourceByName(c *m.ReqContext) Response {
 func DeleteDataSourceByName(c *m.ReqContext) Response {
 	name := c.Params(":name")
 	name := c.Params(":name")
 
 
 	if name == "" {
 	if name == "" {
-		return ApiError(400, "Missing valid datasource name", nil)
+		return Error(400, "Missing valid datasource name", nil)
 	}
 	}
 
 
 	getCmd := &m.GetDataSourceByNameQuery{Name: name, OrgId: c.OrgId}
 	getCmd := &m.GetDataSourceByNameQuery{Name: name, OrgId: c.OrgId}
 	if err := bus.Dispatch(getCmd); err != nil {
 	if err := bus.Dispatch(getCmd); err != nil {
-		return ApiError(500, "Failed to delete datasource", err)
+		return Error(500, "Failed to delete datasource", err)
 	}
 	}
 
 
 	if getCmd.Result.ReadOnly {
 	if getCmd.Result.ReadOnly {
-		return ApiError(403, "Cannot delete read-only data source", nil)
+		return Error(403, "Cannot delete read-only data source", nil)
 	}
 	}
 
 
 	cmd := &m.DeleteDataSourceByNameCommand{Name: name, OrgId: c.OrgId}
 	cmd := &m.DeleteDataSourceByNameCommand{Name: name, OrgId: c.OrgId}
 	err := bus.Dispatch(cmd)
 	err := bus.Dispatch(cmd)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to delete datasource", err)
+		return Error(500, "Failed to delete datasource", err)
 	}
 	}
 
 
-	return ApiSuccess("Data source deleted")
+	return Success("Data source deleted")
 }
 }
 
 
 func AddDataSource(c *m.ReqContext, cmd m.AddDataSourceCommand) Response {
 func AddDataSource(c *m.ReqContext, cmd m.AddDataSourceCommand) Response {
@@ -124,14 +124,14 @@ func AddDataSource(c *m.ReqContext, cmd m.AddDataSourceCommand) Response {
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		if err == m.ErrDataSourceNameExists {
 		if err == m.ErrDataSourceNameExists {
-			return ApiError(409, err.Error(), err)
+			return Error(409, err.Error(), err)
 		}
 		}
 
 
-		return ApiError(500, "Failed to add datasource", err)
+		return Error(500, "Failed to add datasource", err)
 	}
 	}
 
 
 	ds := convertModelToDtos(cmd.Result)
 	ds := convertModelToDtos(cmd.Result)
-	return Json(200, util.DynMap{
+	return JSON(200, util.DynMap{
 		"message":    "Datasource added",
 		"message":    "Datasource added",
 		"id":         cmd.Result.Id,
 		"id":         cmd.Result.Id,
 		"name":       cmd.Result.Name,
 		"name":       cmd.Result.Name,
@@ -143,21 +143,20 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
 	cmd.Id = c.ParamsInt64(":id")
 	cmd.Id = c.ParamsInt64(":id")
 
 
-	err := fillWithSecureJsonData(&cmd)
+	err := fillWithSecureJSONData(&cmd)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to update datasource", err)
+		return Error(500, "Failed to update datasource", err)
 	}
 	}
 
 
 	err = bus.Dispatch(&cmd)
 	err = bus.Dispatch(&cmd)
 	if err != nil {
 	if err != nil {
 		if err == m.ErrDataSourceUpdatingOldVersion {
 		if err == m.ErrDataSourceUpdatingOldVersion {
-			return ApiError(500, "Failed to update datasource. Reload new version and try again", err)
-		} else {
-			return ApiError(500, "Failed to update datasource", err)
+			return Error(500, "Failed to update datasource. Reload new version and try again", err)
 		}
 		}
+		return Error(500, "Failed to update datasource", err)
 	}
 	}
 	ds := convertModelToDtos(cmd.Result)
 	ds := convertModelToDtos(cmd.Result)
-	return Json(200, util.DynMap{
+	return JSON(200, util.DynMap{
 		"message":    "Datasource updated",
 		"message":    "Datasource updated",
 		"id":         cmd.Id,
 		"id":         cmd.Id,
 		"name":       cmd.Name,
 		"name":       cmd.Name,
@@ -165,12 +164,12 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response {
 	})
 	})
 }
 }
 
 
-func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
+func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
 	if len(cmd.SecureJsonData) == 0 {
 	if len(cmd.SecureJsonData) == 0 {
 		return nil
 		return nil
 	}
 	}
 
 
-	ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId)
+	ds, err := getRawDataSourceByID(cmd.Id, cmd.OrgId)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -179,8 +178,8 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
 		return m.ErrDatasourceIsReadOnly
 		return m.ErrDatasourceIsReadOnly
 	}
 	}
 
 
-	secureJsonData := ds.SecureJsonData.Decrypt()
-	for k, v := range secureJsonData {
+	secureJSONData := ds.SecureJsonData.Decrypt()
+	for k, v := range secureJSONData {
 
 
 		if _, ok := cmd.SecureJsonData[k]; !ok {
 		if _, ok := cmd.SecureJsonData[k]; !ok {
 			cmd.SecureJsonData[k] = v
 			cmd.SecureJsonData[k] = v
@@ -190,10 +189,10 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
 	return nil
 	return nil
 }
 }
 
 
-func getRawDataSourceById(id int64, orgId int64) (*m.DataSource, error) {
+func getRawDataSourceByID(id int64, orgID int64) (*m.DataSource, error) {
 	query := m.GetDataSourceByIdQuery{
 	query := m.GetDataSourceByIdQuery{
 		Id:    id,
 		Id:    id,
-		OrgId: orgId,
+		OrgId: orgID,
 	}
 	}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
@@ -209,25 +208,25 @@ func GetDataSourceByName(c *m.ReqContext) Response {
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrDataSourceNotFound {
 		if err == m.ErrDataSourceNotFound {
-			return ApiError(404, "Data source not found", nil)
+			return Error(404, "Data source not found", nil)
 		}
 		}
-		return ApiError(500, "Failed to query datasources", err)
+		return Error(500, "Failed to query datasources", err)
 	}
 	}
 
 
 	dtos := convertModelToDtos(query.Result)
 	dtos := convertModelToDtos(query.Result)
 	dtos.ReadOnly = true
 	dtos.ReadOnly = true
-	return Json(200, &dtos)
+	return JSON(200, &dtos)
 }
 }
 
 
 // Get /api/datasources/id/:name
 // Get /api/datasources/id/:name
-func GetDataSourceIdByName(c *m.ReqContext) Response {
+func GetDataSourceIDByName(c *m.ReqContext) Response {
 	query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
 	query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrDataSourceNotFound {
 		if err == m.ErrDataSourceNotFound {
-			return ApiError(404, "Data source not found", nil)
+			return Error(404, "Data source not found", nil)
 		}
 		}
-		return ApiError(500, "Failed to query datasources", err)
+		return Error(500, "Failed to query datasources", err)
 	}
 	}
 
 
 	ds := query.Result
 	ds := query.Result
@@ -235,7 +234,7 @@ func GetDataSourceIdByName(c *m.ReqContext) Response {
 		Id: ds.Id,
 		Id: ds.Id,
 	}
 	}
 
 
-	return Json(200, &dtos)
+	return JSON(200, &dtos)
 }
 }
 
 
 func convertModelToDtos(ds *m.DataSource) dtos.DataSource {
 func convertModelToDtos(ds *m.DataSource) dtos.DataSource {

+ 2 - 2
pkg/api/dtos/prefs.go

@@ -2,12 +2,12 @@ package dtos
 
 
 type Prefs struct {
 type Prefs struct {
 	Theme           string `json:"theme"`
 	Theme           string `json:"theme"`
-	HomeDashboardId int64  `json:"homeDashboardId"`
+	HomeDashboardID int64  `json:"homeDashboardId"`
 	Timezone        string `json:"timezone"`
 	Timezone        string `json:"timezone"`
 }
 }
 
 
 type UpdatePrefsCmd struct {
 type UpdatePrefsCmd struct {
 	Theme           string `json:"theme"`
 	Theme           string `json:"theme"`
-	HomeDashboardId int64  `json:"homeDashboardId"`
+	HomeDashboardID int64  `json:"homeDashboardId"`
 	Timezone        string `json:"timezone"`
 	Timezone        string `json:"timezone"`
 }
 }

+ 15 - 15
pkg/api/folder.go

@@ -28,30 +28,30 @@ func GetFolders(c *m.ReqContext) Response {
 		})
 		})
 	}
 	}
 
 
-	return Json(200, result)
+	return JSON(200, result)
 }
 }
 
 
-func GetFolderByUid(c *m.ReqContext) Response {
+func GetFolderByUID(c *m.ReqContext) Response {
 	s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
 	s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
-	folder, err := s.GetFolderByUid(c.Params(":uid"))
+	folder, err := s.GetFolderByUID(c.Params(":uid"))
 
 
 	if err != nil {
 	if err != nil {
 		return toFolderError(err)
 		return toFolderError(err)
 	}
 	}
 
 
 	g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
 	g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
-	return Json(200, toFolderDto(g, folder))
+	return JSON(200, toFolderDto(g, folder))
 }
 }
 
 
-func GetFolderById(c *m.ReqContext) Response {
+func GetFolderByID(c *m.ReqContext) Response {
 	s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
 	s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
-	folder, err := s.GetFolderById(c.ParamsInt64(":id"))
+	folder, err := s.GetFolderByID(c.ParamsInt64(":id"))
 	if err != nil {
 	if err != nil {
 		return toFolderError(err)
 		return toFolderError(err)
 	}
 	}
 
 
 	g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
 	g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
-	return Json(200, toFolderDto(g, folder))
+	return JSON(200, toFolderDto(g, folder))
 }
 }
 
 
 func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
 func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
@@ -62,7 +62,7 @@ func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
 	}
 	}
 
 
 	g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
 	g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
-	return Json(200, toFolderDto(g, cmd.Result))
+	return JSON(200, toFolderDto(g, cmd.Result))
 }
 }
 
 
 func UpdateFolder(c *m.ReqContext, cmd m.UpdateFolderCommand) Response {
 func UpdateFolder(c *m.ReqContext, cmd m.UpdateFolderCommand) Response {
@@ -73,7 +73,7 @@ func UpdateFolder(c *m.ReqContext, cmd m.UpdateFolderCommand) Response {
 	}
 	}
 
 
 	g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
 	g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
-	return Json(200, toFolderDto(g, cmd.Result))
+	return JSON(200, toFolderDto(g, cmd.Result))
 }
 }
 
 
 func DeleteFolder(c *m.ReqContext) Response {
 func DeleteFolder(c *m.ReqContext) Response {
@@ -83,7 +83,7 @@ func DeleteFolder(c *m.ReqContext) Response {
 		return toFolderError(err)
 		return toFolderError(err)
 	}
 	}
 
 
-	return Json(200, util.DynMap{
+	return JSON(200, util.DynMap{
 		"title":   f.Title,
 		"title":   f.Title,
 		"message": fmt.Sprintf("Folder %s deleted", f.Title),
 		"message": fmt.Sprintf("Folder %s deleted", f.Title),
 	})
 	})
@@ -127,20 +127,20 @@ func toFolderError(err error) Response {
 		err == m.ErrDashboardTypeMismatch ||
 		err == m.ErrDashboardTypeMismatch ||
 		err == m.ErrDashboardInvalidUid ||
 		err == m.ErrDashboardInvalidUid ||
 		err == m.ErrDashboardUidToLong {
 		err == m.ErrDashboardUidToLong {
-		return ApiError(400, err.Error(), nil)
+		return Error(400, err.Error(), nil)
 	}
 	}
 
 
 	if err == m.ErrFolderAccessDenied {
 	if err == m.ErrFolderAccessDenied {
-		return ApiError(403, "Access denied", err)
+		return Error(403, "Access denied", err)
 	}
 	}
 
 
 	if err == m.ErrFolderNotFound {
 	if err == m.ErrFolderNotFound {
-		return Json(404, util.DynMap{"status": "not-found", "message": m.ErrFolderNotFound.Error()})
+		return JSON(404, util.DynMap{"status": "not-found", "message": m.ErrFolderNotFound.Error()})
 	}
 	}
 
 
 	if err == m.ErrFolderVersionMismatch {
 	if err == m.ErrFolderVersionMismatch {
-		return Json(412, util.DynMap{"status": "version-mismatch", "message": m.ErrFolderVersionMismatch.Error()})
+		return JSON(412, util.DynMap{"status": "version-mismatch", "message": m.ErrFolderVersionMismatch.Error()})
 	}
 	}
 
 
-	return ApiError(500, "Folder API error", err)
+	return Error(500, "Folder API error", err)
 }
 }

+ 10 - 10
pkg/api/folder_permission.go

@@ -12,7 +12,7 @@ import (
 
 
 func GetFolderPermissionList(c *m.ReqContext) Response {
 func GetFolderPermissionList(c *m.ReqContext) Response {
 	s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
 	s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
-	folder, err := s.GetFolderByUid(c.Params(":uid"))
+	folder, err := s.GetFolderByUID(c.Params(":uid"))
 
 
 	if err != nil {
 	if err != nil {
 		return toFolderError(err)
 		return toFolderError(err)
@@ -26,7 +26,7 @@ func GetFolderPermissionList(c *m.ReqContext) Response {
 
 
 	acl, err := g.GetAcl()
 	acl, err := g.GetAcl()
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to get folder permissions", err)
+		return Error(500, "Failed to get folder permissions", err)
 	}
 	}
 
 
 	for _, perm := range acl {
 	for _, perm := range acl {
@@ -38,12 +38,12 @@ func GetFolderPermissionList(c *m.ReqContext) Response {
 		}
 		}
 	}
 	}
 
 
-	return Json(200, acl)
+	return JSON(200, acl)
 }
 }
 
 
 func UpdateFolderPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
 func UpdateFolderPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
 	s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
 	s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
-	folder, err := s.GetFolderByUid(c.Params(":uid"))
+	folder, err := s.GetFolderByUID(c.Params(":uid"))
 
 
 	if err != nil {
 	if err != nil {
 		return toFolderError(err)
 		return toFolderError(err)
@@ -79,13 +79,13 @@ func UpdateFolderPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclComm
 		if err != nil {
 		if err != nil {
 			if err == guardian.ErrGuardianPermissionExists ||
 			if err == guardian.ErrGuardianPermissionExists ||
 				err == guardian.ErrGuardianOverride {
 				err == guardian.ErrGuardianOverride {
-				return ApiError(400, err.Error(), err)
+				return Error(400, err.Error(), err)
 			}
 			}
 
 
-			return ApiError(500, "Error while checking folder permissions", err)
+			return Error(500, "Error while checking folder permissions", err)
 		}
 		}
 
 
-		return ApiError(403, "Cannot remove own admin permission for a folder", nil)
+		return Error(403, "Cannot remove own admin permission for a folder", nil)
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
@@ -97,11 +97,11 @@ func UpdateFolderPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclComm
 		}
 		}
 
 
 		if err == m.ErrFolderAclInfoMissing || err == m.ErrFolderPermissionFolderEmpty {
 		if err == m.ErrFolderAclInfoMissing || err == m.ErrFolderPermissionFolderEmpty {
-			return ApiError(409, err.Error(), err)
+			return Error(409, err.Error(), err)
 		}
 		}
 
 
-		return ApiError(500, "Failed to create permission", err)
+		return Error(500, "Failed to create permission", err)
 	}
 	}
 
 
-	return ApiSuccess("Folder permissions updated")
+	return Success("Folder permissions updated")
 }
 }

+ 5 - 5
pkg/api/folder_permission_test.go

@@ -17,7 +17,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
 	Convey("Folder permissions test", t, func() {
 	Convey("Folder permissions test", t, func() {
 		Convey("Given folder not exists", func() {
 		Convey("Given folder not exists", func() {
 			mock := &fakeFolderService{
 			mock := &fakeFolderService{
-				GetFolderByUidError: m.ErrFolderNotFound,
+				GetFolderByUIDError: m.ErrFolderNotFound,
 			}
 			}
 
 
 			origNewFolderService := dashboards.NewFolderService
 			origNewFolderService := dashboards.NewFolderService
@@ -49,7 +49,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
 			guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
 			guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
 
 
 			mock := &fakeFolderService{
 			mock := &fakeFolderService{
-				GetFolderByUidResult: &m.Folder{
+				GetFolderByUIDResult: &m.Folder{
 					Id:    1,
 					Id:    1,
 					Uid:   "uid",
 					Uid:   "uid",
 					Title: "Folder",
 					Title: "Folder",
@@ -96,7 +96,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
 			})
 			})
 
 
 			mock := &fakeFolderService{
 			mock := &fakeFolderService{
-				GetFolderByUidResult: &m.Folder{
+				GetFolderByUIDResult: &m.Folder{
 					Id:    1,
 					Id:    1,
 					Uid:   "uid",
 					Uid:   "uid",
 					Title: "Folder",
 					Title: "Folder",
@@ -142,7 +142,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
 			})
 			})
 
 
 			mock := &fakeFolderService{
 			mock := &fakeFolderService{
-				GetFolderByUidResult: &m.Folder{
+				GetFolderByUIDResult: &m.Folder{
 					Id:    1,
 					Id:    1,
 					Uid:   "uid",
 					Uid:   "uid",
 					Title: "Folder",
 					Title: "Folder",
@@ -178,7 +178,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
 			)
 			)
 
 
 			mock := &fakeFolderService{
 			mock := &fakeFolderService{
-				GetFolderByUidResult: &m.Folder{
+				GetFolderByUIDResult: &m.Folder{
 					Id:    1,
 					Id:    1,
 					Uid:   "uid",
 					Uid:   "uid",
 					Title: "Folder",
 					Title: "Folder",

+ 11 - 11
pkg/api/folder_test.go

@@ -133,8 +133,8 @@ func TestFoldersApiEndpoint(t *testing.T) {
 	})
 	})
 }
 }
 
 
-func callGetFolderByUid(sc *scenarioContext) {
-	sc.handlerFunc = GetFolderByUid
+func callGetFolderByUID(sc *scenarioContext) {
+	sc.handlerFunc = GetFolderByUID
 	sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
 	sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
 }
 }
 
 
@@ -204,10 +204,10 @@ func updateFolderScenario(desc string, url string, routePattern string, mock *fa
 type fakeFolderService struct {
 type fakeFolderService struct {
 	GetFoldersResult     []*m.Folder
 	GetFoldersResult     []*m.Folder
 	GetFoldersError      error
 	GetFoldersError      error
-	GetFolderByUidResult *m.Folder
-	GetFolderByUidError  error
-	GetFolderByIdResult  *m.Folder
-	GetFolderByIdError   error
+	GetFolderByUIDResult *m.Folder
+	GetFolderByUIDError  error
+	GetFolderByIDResult  *m.Folder
+	GetFolderByIDError   error
 	CreateFolderResult   *m.Folder
 	CreateFolderResult   *m.Folder
 	CreateFolderError    error
 	CreateFolderError    error
 	UpdateFolderResult   *m.Folder
 	UpdateFolderResult   *m.Folder
@@ -221,12 +221,12 @@ func (s *fakeFolderService) GetFolders(limit int) ([]*m.Folder, error) {
 	return s.GetFoldersResult, s.GetFoldersError
 	return s.GetFoldersResult, s.GetFoldersError
 }
 }
 
 
-func (s *fakeFolderService) GetFolderById(id int64) (*m.Folder, error) {
-	return s.GetFolderByIdResult, s.GetFolderByIdError
+func (s *fakeFolderService) GetFolderByID(id int64) (*m.Folder, error) {
+	return s.GetFolderByIDResult, s.GetFolderByIDError
 }
 }
 
 
-func (s *fakeFolderService) GetFolderByUid(uid string) (*m.Folder, error) {
-	return s.GetFolderByUidResult, s.GetFolderByUidError
+func (s *fakeFolderService) GetFolderByUID(uid string) (*m.Folder, error) {
+	return s.GetFolderByUIDResult, s.GetFolderByUIDError
 }
 }
 
 
 func (s *fakeFolderService) CreateFolder(cmd *m.CreateFolderCommand) error {
 func (s *fakeFolderService) CreateFolder(cmd *m.CreateFolderCommand) error {
@@ -234,7 +234,7 @@ func (s *fakeFolderService) CreateFolder(cmd *m.CreateFolderCommand) error {
 	return s.CreateFolderError
 	return s.CreateFolderError
 }
 }
 
 
-func (s *fakeFolderService) UpdateFolder(existingUid string, cmd *m.UpdateFolderCommand) error {
+func (s *fakeFolderService) UpdateFolder(existingUID string, cmd *m.UpdateFolderCommand) error {
 	cmd.Result = s.UpdateFolderResult
 	cmd.Result = s.UpdateFolderResult
 	return s.UpdateFolderError
 	return s.UpdateFolderError
 }
 }

+ 15 - 12
pkg/api/http_server.go

@@ -29,7 +29,7 @@ import (
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
-type HttpServer struct {
+type HTTPServer struct {
 	log           log.Logger
 	log           log.Logger
 	macaron       *macaron.Macaron
 	macaron       *macaron.Macaron
 	context       context.Context
 	context       context.Context
@@ -39,14 +39,14 @@ type HttpServer struct {
 	httpSrv *http.Server
 	httpSrv *http.Server
 }
 }
 
 
-func NewHttpServer() *HttpServer {
-	return &HttpServer{
+func NewHTTPServer() *HTTPServer {
+	return &HTTPServer{
 		log:   log.New("http.server"),
 		log:   log.New("http.server"),
 		cache: gocache.New(5*time.Minute, 10*time.Minute),
 		cache: gocache.New(5*time.Minute, 10*time.Minute),
 	}
 	}
 }
 }
 
 
-func (hs *HttpServer) Start(ctx context.Context) error {
+func (hs *HTTPServer) Start(ctx context.Context) error {
 	var err error
 	var err error
 
 
 	hs.context = ctx
 	hs.context = ctx
@@ -74,12 +74,15 @@ func (hs *HttpServer) Start(ctx context.Context) error {
 			return nil
 			return nil
 		}
 		}
 	case setting.SOCKET:
 	case setting.SOCKET:
-		ln, err := net.Listen("unix", setting.SocketPath)
+		ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: setting.SocketPath, Net: "unix"})
 		if err != nil {
 		if err != nil {
 			hs.log.Debug("server was shutdown gracefully")
 			hs.log.Debug("server was shutdown gracefully")
 			return nil
 			return nil
 		}
 		}
 
 
+		// Make socket writable by group
+		os.Chmod(setting.SocketPath, 0660)
+
 		err = hs.httpSrv.Serve(ln)
 		err = hs.httpSrv.Serve(ln)
 		if err != nil {
 		if err != nil {
 			hs.log.Debug("server was shutdown gracefully")
 			hs.log.Debug("server was shutdown gracefully")
@@ -93,13 +96,13 @@ func (hs *HttpServer) Start(ctx context.Context) error {
 	return err
 	return err
 }
 }
 
 
-func (hs *HttpServer) Shutdown(ctx context.Context) error {
+func (hs *HTTPServer) Shutdown(ctx context.Context) error {
 	err := hs.httpSrv.Shutdown(ctx)
 	err := hs.httpSrv.Shutdown(ctx)
 	hs.log.Info("Stopped HTTP server")
 	hs.log.Info("Stopped HTTP server")
 	return err
 	return err
 }
 }
 
 
-func (hs *HttpServer) listenAndServeTLS(certfile, keyfile string) error {
+func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
 	if certfile == "" {
 	if certfile == "" {
 		return fmt.Errorf("cert_file cannot be empty when using HTTPS")
 		return fmt.Errorf("cert_file cannot be empty when using HTTPS")
 	}
 	}
@@ -141,7 +144,7 @@ func (hs *HttpServer) listenAndServeTLS(certfile, keyfile string) error {
 	return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
 	return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
 }
 }
 
 
-func (hs *HttpServer) newMacaron() *macaron.Macaron {
+func (hs *HTTPServer) newMacaron() *macaron.Macaron {
 	macaron.Env = setting.Env
 	macaron.Env = setting.Env
 	m := macaron.New()
 	m := macaron.New()
 
 
@@ -175,7 +178,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
 	m.Use(hs.healthHandler)
 	m.Use(hs.healthHandler)
 	m.Use(hs.metricsEndpoint)
 	m.Use(hs.metricsEndpoint)
 	m.Use(middleware.GetContextHandler())
 	m.Use(middleware.GetContextHandler())
-	m.Use(middleware.Sessioner(&setting.SessionOptions))
+	m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime))
 	m.Use(middleware.OrgRedirect())
 	m.Use(middleware.OrgRedirect())
 
 
 	// needs to be after context handler
 	// needs to be after context handler
@@ -188,7 +191,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
 	return m
 	return m
 }
 }
 
 
-func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) {
+func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
 	if ctx.Req.Method != "GET" || ctx.Req.URL.Path != "/metrics" {
 	if ctx.Req.Method != "GET" || ctx.Req.URL.Path != "/metrics" {
 		return
 		return
 	}
 	}
@@ -197,7 +200,7 @@ func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) {
 		ServeHTTP(ctx.Resp, ctx.Req.Request)
 		ServeHTTP(ctx.Resp, ctx.Req.Request)
 }
 }
 
 
-func (hs *HttpServer) healthHandler(ctx *macaron.Context) {
+func (hs *HTTPServer) healthHandler(ctx *macaron.Context) {
 	notHeadOrGet := ctx.Req.Method != http.MethodGet && ctx.Req.Method != http.MethodHead
 	notHeadOrGet := ctx.Req.Method != http.MethodGet && ctx.Req.Method != http.MethodHead
 	if notHeadOrGet || ctx.Req.URL.Path != "/api/health" {
 	if notHeadOrGet || ctx.Req.URL.Path != "/api/health" {
 		return
 		return
@@ -221,7 +224,7 @@ func (hs *HttpServer) healthHandler(ctx *macaron.Context) {
 	ctx.Resp.Write(dataBytes)
 	ctx.Resp.Write(dataBytes)
 }
 }
 
 
-func (hs *HttpServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
+func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
 	headers := func(c *macaron.Context) {
 	headers := func(c *macaron.Context) {
 		c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
 		c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
 	}
 	}

+ 15 - 14
pkg/api/index.go

@@ -32,13 +32,13 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 		locale = parts[0]
 		locale = parts[0]
 	}
 	}
 
 
-	appUrl := setting.AppUrl
-	appSubUrl := setting.AppSubUrl
+	appURL := setting.AppUrl
+	appSubURL := setting.AppSubUrl
 
 
 	// special case when doing localhost call from phantomjs
 	// special case when doing localhost call from phantomjs
 	if c.IsRenderCall {
 	if c.IsRenderCall {
-		appUrl = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort)
-		appSubUrl = ""
+		appURL = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort)
+		appSubURL = ""
 		settings["appSubUrl"] = ""
 		settings["appSubUrl"] = ""
 	}
 	}
 
 
@@ -62,8 +62,8 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 		},
 		},
 		Settings:                settings,
 		Settings:                settings,
 		Theme:                   prefs.Theme,
 		Theme:                   prefs.Theme,
-		AppUrl:                  appUrl,
-		AppSubUrl:               appSubUrl,
+		AppUrl:                  appURL,
+		AppSubUrl:               appSubURL,
 		GoogleAnalyticsId:       setting.GoogleAnalyticsId,
 		GoogleAnalyticsId:       setting.GoogleAnalyticsId,
 		GoogleTagManagerId:      setting.GoogleTagManagerId,
 		GoogleTagManagerId:      setting.GoogleTagManagerId,
 		BuildVersion:            setting.BuildVersion,
 		BuildVersion:            setting.BuildVersion,
@@ -80,8 +80,8 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 		data.User.Name = data.User.Login
 		data.User.Name = data.User.Login
 	}
 	}
 
 
-	themeUrlParam := c.Query("theme")
-	if themeUrlParam == "light" {
+	themeURLParam := c.Query("theme")
+	if themeURLParam == "light" {
 		data.User.LightTheme = true
 		data.User.LightTheme = true
 		data.Theme = "light"
 		data.Theme = "light"
 	}
 	}
@@ -299,12 +299,12 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 }
 }
 
 
 func Index(c *m.ReqContext) {
 func Index(c *m.ReqContext) {
-	if data, err := setIndexViewData(c); err != nil {
+	data, err := setIndexViewData(c)
+	if err != nil {
 		c.Handle(500, "Failed to get settings", err)
 		c.Handle(500, "Failed to get settings", err)
 		return
 		return
-	} else {
-		c.HTML(200, "index", data)
 	}
 	}
+	c.HTML(200, "index", data)
 }
 }
 
 
 func NotFoundHandler(c *m.ReqContext) {
 func NotFoundHandler(c *m.ReqContext) {
@@ -313,10 +313,11 @@ func NotFoundHandler(c *m.ReqContext) {
 		return
 		return
 	}
 	}
 
 
-	if data, err := setIndexViewData(c); err != nil {
+	data, err := setIndexViewData(c)
+	if err != nil {
 		c.Handle(500, "Failed to get settings", err)
 		c.Handle(500, "Failed to get settings", err)
 		return
 		return
-	} else {
-		c.HTML(404, "index", data)
 	}
 	}
+
+	c.HTML(404, "index", data)
 }
 }

+ 7 - 7
pkg/api/login.go

@@ -14,7 +14,7 @@ import (
 )
 )
 
 
 const (
 const (
-	VIEW_INDEX = "index"
+	ViewIndex = "index"
 )
 )
 
 
 func LoginView(c *m.ReqContext) {
 func LoginView(c *m.ReqContext) {
@@ -40,7 +40,7 @@ func LoginView(c *m.ReqContext) {
 	}
 	}
 
 
 	if !tryLoginUsingRememberCookie(c) {
 	if !tryLoginUsingRememberCookie(c) {
-		c.HTML(200, VIEW_INDEX, viewData)
+		c.HTML(200, ViewIndex, viewData)
 		return
 		return
 	}
 	}
 
 
@@ -87,7 +87,7 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
 	return true
 	return true
 }
 }
 
 
-func LoginApiPing(c *m.ReqContext) {
+func LoginAPIPing(c *m.ReqContext) {
 	if !tryLoginUsingRememberCookie(c) {
 	if !tryLoginUsingRememberCookie(c) {
 		c.JsonApiErr(401, "Unauthorized", nil)
 		c.JsonApiErr(401, "Unauthorized", nil)
 		return
 		return
@@ -98,7 +98,7 @@ func LoginApiPing(c *m.ReqContext) {
 
 
 func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 	if setting.DisableLoginForm {
 	if setting.DisableLoginForm {
-		return ApiError(401, "Login is disabled", nil)
+		return Error(401, "Login is disabled", nil)
 	}
 	}
 
 
 	authQuery := login.LoginUserQuery{
 	authQuery := login.LoginUserQuery{
@@ -109,10 +109,10 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 
 
 	if err := bus.Dispatch(&authQuery); err != nil {
 	if err := bus.Dispatch(&authQuery); err != nil {
 		if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
 		if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
-			return ApiError(401, "Invalid username or password", err)
+			return Error(401, "Invalid username or password", err)
 		}
 		}
 
 
-		return ApiError(500, "Error while trying to authenticate user", err)
+		return Error(500, "Error while trying to authenticate user", err)
 	}
 	}
 
 
 	user := authQuery.User
 	user := authQuery.User
@@ -130,7 +130,7 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 
 
 	metrics.M_Api_Login_Post.Inc()
 	metrics.M_Api_Login_Post.Inc()
 
 
-	return Json(200, result)
+	return JSON(200, result)
 }
 }
 
 
 func loginUserWithUser(user *m.User, c *m.ReqContext) {
 func loginUserWithUser(user *m.User, c *m.ReqContext) {

+ 14 - 14
pkg/api/metrics.go

@@ -17,17 +17,17 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
 	timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
 	timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
 
 
 	if len(reqDto.Queries) == 0 {
 	if len(reqDto.Queries) == 0 {
-		return ApiError(400, "No queries found in query", nil)
+		return Error(400, "No queries found in query", nil)
 	}
 	}
 
 
-	dsId, err := reqDto.Queries[0].Get("datasourceId").Int64()
+	dsID, err := reqDto.Queries[0].Get("datasourceId").Int64()
 	if err != nil {
 	if err != nil {
-		return ApiError(400, "Query missing datasourceId", nil)
+		return Error(400, "Query missing datasourceId", nil)
 	}
 	}
 
 
-	dsQuery := m.GetDataSourceByIdQuery{Id: dsId, OrgId: c.OrgId}
+	dsQuery := m.GetDataSourceByIdQuery{Id: dsID, OrgId: c.OrgId}
 	if err := bus.Dispatch(&dsQuery); err != nil {
 	if err := bus.Dispatch(&dsQuery); err != nil {
-		return ApiError(500, "failed to fetch data source", err)
+		return Error(500, "failed to fetch data source", err)
 	}
 	}
 
 
 	request := &tsdb.TsdbQuery{TimeRange: timeRange}
 	request := &tsdb.TsdbQuery{TimeRange: timeRange}
@@ -44,7 +44,7 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
 
 
 	resp, err := tsdb.HandleRequest(context.Background(), dsQuery.Result, request)
 	resp, err := tsdb.HandleRequest(context.Background(), dsQuery.Result, request)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Metric request error", err)
+		return Error(500, "Metric request error", err)
 	}
 	}
 
 
 	statusCode := 200
 	statusCode := 200
@@ -56,7 +56,7 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
 		}
 		}
 	}
 	}
 
 
-	return Json(statusCode, &resp)
+	return JSON(statusCode, &resp)
 }
 }
 
 
 // GET /api/tsdb/testdata/scenarios
 // GET /api/tsdb/testdata/scenarios
@@ -72,22 +72,22 @@ func GetTestDataScenarios(c *m.ReqContext) Response {
 		})
 		})
 	}
 	}
 
 
-	return Json(200, &result)
+	return JSON(200, &result)
 }
 }
 
 
 // Genereates a index out of range error
 // Genereates a index out of range error
 func GenerateError(c *m.ReqContext) Response {
 func GenerateError(c *m.ReqContext) Response {
 	var array []string
 	var array []string
-	return Json(200, array[20])
+	return JSON(200, array[20])
 }
 }
 
 
 // GET /api/tsdb/testdata/gensql
 // GET /api/tsdb/testdata/gensql
-func GenerateSqlTestData(c *m.ReqContext) Response {
+func GenerateSQLTestData(c *m.ReqContext) Response {
 	if err := bus.Dispatch(&m.InsertSqlTestDataCommand{}); err != nil {
 	if err := bus.Dispatch(&m.InsertSqlTestDataCommand{}); err != nil {
-		return ApiError(500, "Failed to insert test data", err)
+		return Error(500, "Failed to insert test data", err)
 	}
 	}
 
 
-	return Json(200, &util.DynMap{"message": "OK"})
+	return JSON(200, &util.DynMap{"message": "OK"})
 }
 }
 
 
 // GET /api/tsdb/testdata/random-walk
 // GET /api/tsdb/testdata/random-walk
@@ -111,8 +111,8 @@ func GetTestDataRandomWalk(c *m.ReqContext) Response {
 
 
 	resp, err := tsdb.HandleRequest(context.Background(), dsInfo, request)
 	resp, err := tsdb.HandleRequest(context.Background(), dsInfo, request)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Metric request error", err)
+		return Error(500, "Metric request error", err)
 	}
 	}
 
 
-	return Json(200, &resp)
+	return JSON(200, &resp)
 }
 }

+ 28 - 28
pkg/api/org.go

@@ -15,7 +15,7 @@ func GetOrgCurrent(c *m.ReqContext) Response {
 }
 }
 
 
 // GET /api/orgs/:orgId
 // GET /api/orgs/:orgId
-func GetOrgById(c *m.ReqContext) Response {
+func GetOrgByID(c *m.ReqContext) Response {
 	return getOrgHelper(c.ParamsInt64(":orgId"))
 	return getOrgHelper(c.ParamsInt64(":orgId"))
 }
 }
 
 
@@ -24,10 +24,10 @@ func GetOrgByName(c *m.ReqContext) Response {
 	query := m.GetOrgByNameQuery{Name: c.Params(":name")}
 	query := m.GetOrgByNameQuery{Name: c.Params(":name")}
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrOrgNotFound {
 		if err == m.ErrOrgNotFound {
-			return ApiError(404, "Organization not found", err)
+			return Error(404, "Organization not found", err)
 		}
 		}
 
 
-		return ApiError(500, "Failed to get organization", err)
+		return Error(500, "Failed to get organization", err)
 	}
 	}
 	org := query.Result
 	org := query.Result
 	result := m.OrgDetailsDTO{
 	result := m.OrgDetailsDTO{
@@ -43,18 +43,18 @@ func GetOrgByName(c *m.ReqContext) Response {
 		},
 		},
 	}
 	}
 
 
-	return Json(200, &result)
+	return JSON(200, &result)
 }
 }
 
 
-func getOrgHelper(orgId int64) Response {
-	query := m.GetOrgByIdQuery{Id: orgId}
+func getOrgHelper(orgID int64) Response {
+	query := m.GetOrgByIdQuery{Id: orgID}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrOrgNotFound {
 		if err == m.ErrOrgNotFound {
-			return ApiError(404, "Organization not found", err)
+			return Error(404, "Organization not found", err)
 		}
 		}
 
 
-		return ApiError(500, "Failed to get organization", err)
+		return Error(500, "Failed to get organization", err)
 	}
 	}
 
 
 	org := query.Result
 	org := query.Result
@@ -71,26 +71,26 @@ func getOrgHelper(orgId int64) Response {
 		},
 		},
 	}
 	}
 
 
-	return Json(200, &result)
+	return JSON(200, &result)
 }
 }
 
 
 // POST /api/orgs
 // POST /api/orgs
 func CreateOrg(c *m.ReqContext, cmd m.CreateOrgCommand) Response {
 func CreateOrg(c *m.ReqContext, cmd m.CreateOrgCommand) Response {
 	if !c.IsSignedIn || (!setting.AllowUserOrgCreate && !c.IsGrafanaAdmin) {
 	if !c.IsSignedIn || (!setting.AllowUserOrgCreate && !c.IsGrafanaAdmin) {
-		return ApiError(403, "Access denied", nil)
+		return Error(403, "Access denied", nil)
 	}
 	}
 
 
 	cmd.UserId = c.UserId
 	cmd.UserId = c.UserId
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		if err == m.ErrOrgNameTaken {
 		if err == m.ErrOrgNameTaken {
-			return ApiError(409, "Organization name taken", err)
+			return Error(409, "Organization name taken", err)
 		}
 		}
-		return ApiError(500, "Failed to create organization", err)
+		return Error(500, "Failed to create organization", err)
 	}
 	}
 
 
 	metrics.M_Api_Org_Create.Inc()
 	metrics.M_Api_Org_Create.Inc()
 
 
-	return Json(200, &util.DynMap{
+	return JSON(200, &util.DynMap{
 		"orgId":   cmd.Result.Id,
 		"orgId":   cmd.Result.Id,
 		"message": "Organization created",
 		"message": "Organization created",
 	})
 	})
@@ -106,16 +106,16 @@ func UpdateOrg(c *m.ReqContext, form dtos.UpdateOrgForm) Response {
 	return updateOrgHelper(form, c.ParamsInt64(":orgId"))
 	return updateOrgHelper(form, c.ParamsInt64(":orgId"))
 }
 }
 
 
-func updateOrgHelper(form dtos.UpdateOrgForm, orgId int64) Response {
-	cmd := m.UpdateOrgCommand{Name: form.Name, OrgId: orgId}
+func updateOrgHelper(form dtos.UpdateOrgForm, orgID int64) Response {
+	cmd := m.UpdateOrgCommand{Name: form.Name, OrgId: orgID}
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		if err == m.ErrOrgNameTaken {
 		if err == m.ErrOrgNameTaken {
-			return ApiError(400, "Organization name taken", err)
+			return Error(400, "Organization name taken", err)
 		}
 		}
-		return ApiError(500, "Failed to update organization", err)
+		return Error(500, "Failed to update organization", err)
 	}
 	}
 
 
-	return ApiSuccess("Organization updated")
+	return Success("Organization updated")
 }
 }
 
 
 // PUT /api/org/address
 // PUT /api/org/address
@@ -128,9 +128,9 @@ func UpdateOrgAddress(c *m.ReqContext, form dtos.UpdateOrgAddressForm) Response
 	return updateOrgAddressHelper(form, c.ParamsInt64(":orgId"))
 	return updateOrgAddressHelper(form, c.ParamsInt64(":orgId"))
 }
 }
 
 
-func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Response {
+func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgID int64) Response {
 	cmd := m.UpdateOrgAddressCommand{
 	cmd := m.UpdateOrgAddressCommand{
-		OrgId: orgId,
+		OrgId: orgID,
 		Address: m.Address{
 		Address: m.Address{
 			Address1: form.Address1,
 			Address1: form.Address1,
 			Address2: form.Address2,
 			Address2: form.Address2,
@@ -142,21 +142,21 @@ func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Respons
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to update org address", err)
+		return Error(500, "Failed to update org address", err)
 	}
 	}
 
 
-	return ApiSuccess("Address updated")
+	return Success("Address updated")
 }
 }
 
 
 // GET /api/orgs/:orgId
 // GET /api/orgs/:orgId
-func DeleteOrgById(c *m.ReqContext) Response {
+func DeleteOrgByID(c *m.ReqContext) Response {
 	if err := bus.Dispatch(&m.DeleteOrgCommand{Id: c.ParamsInt64(":orgId")}); err != nil {
 	if err := bus.Dispatch(&m.DeleteOrgCommand{Id: c.ParamsInt64(":orgId")}); err != nil {
 		if err == m.ErrOrgNotFound {
 		if err == m.ErrOrgNotFound {
-			return ApiError(404, "Failed to delete organization. ID not found", nil)
+			return Error(404, "Failed to delete organization. ID not found", nil)
 		}
 		}
-		return ApiError(500, "Failed to update organization", err)
+		return Error(500, "Failed to update organization", err)
 	}
 	}
-	return ApiSuccess("Organization deleted")
+	return Success("Organization deleted")
 }
 }
 
 
 func SearchOrgs(c *m.ReqContext) Response {
 func SearchOrgs(c *m.ReqContext) Response {
@@ -168,8 +168,8 @@ func SearchOrgs(c *m.ReqContext) Response {
 	}
 	}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to search orgs", err)
+		return Error(500, "Failed to search orgs", err)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }

+ 39 - 40
pkg/api/org_invite.go

@@ -16,30 +16,30 @@ func GetPendingOrgInvites(c *m.ReqContext) Response {
 	query := m.GetTempUsersQuery{OrgId: c.OrgId, Status: m.TmpUserInvitePending}
 	query := m.GetTempUsersQuery{OrgId: c.OrgId, Status: m.TmpUserInvitePending}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to get invites from db", err)
+		return Error(500, "Failed to get invites from db", err)
 	}
 	}
 
 
 	for _, invite := range query.Result {
 	for _, invite := range query.Result {
 		invite.Url = setting.ToAbsUrl("invite/" + invite.Code)
 		invite.Url = setting.ToAbsUrl("invite/" + invite.Code)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
 func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
 func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
 	if !inviteDto.Role.IsValid() {
 	if !inviteDto.Role.IsValid() {
-		return ApiError(400, "Invalid role specified", nil)
+		return Error(400, "Invalid role specified", nil)
 	}
 	}
 
 
 	// first try get existing user
 	// first try get existing user
 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: inviteDto.LoginOrEmail}
 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: inviteDto.LoginOrEmail}
 	if err := bus.Dispatch(&userQuery); err != nil {
 	if err := bus.Dispatch(&userQuery); err != nil {
 		if err != m.ErrUserNotFound {
 		if err != m.ErrUserNotFound {
-			return ApiError(500, "Failed to query db for existing user check", err)
+			return Error(500, "Failed to query db for existing user check", err)
 		}
 		}
 
 
 		if setting.DisableLoginForm {
 		if setting.DisableLoginForm {
-			return ApiError(401, "User could not be found", nil)
+			return Error(401, "User could not be found", nil)
 		}
 		}
 	} else {
 	} else {
 		return inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
 		return inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
@@ -56,7 +56,7 @@ func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
 	cmd.RemoteAddr = c.Req.RemoteAddr
 	cmd.RemoteAddr = c.Req.RemoteAddr
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to save invite to database", err)
+		return Error(500, "Failed to save invite to database", err)
 	}
 	}
 
 
 	// send invite email
 	// send invite email
@@ -74,18 +74,18 @@ func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
 		}
 		}
 
 
 		if err := bus.Dispatch(&emailCmd); err != nil {
 		if err := bus.Dispatch(&emailCmd); err != nil {
-			return ApiError(500, "Failed to send email invite", err)
+			return Error(500, "Failed to send email invite", err)
 		}
 		}
 
 
 		emailSentCmd := m.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code}
 		emailSentCmd := m.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code}
 		if err := bus.Dispatch(&emailSentCmd); err != nil {
 		if err := bus.Dispatch(&emailSentCmd); err != nil {
-			return ApiError(500, "Failed to update invite with email sent info", err)
+			return Error(500, "Failed to update invite with email sent info", err)
 		}
 		}
 
 
-		return ApiSuccess(fmt.Sprintf("Sent invite to %s", inviteDto.LoginOrEmail))
+		return Success(fmt.Sprintf("Sent invite to %s", inviteDto.LoginOrEmail))
 	}
 	}
 
 
-	return ApiSuccess(fmt.Sprintf("Created invite for %s", inviteDto.LoginOrEmail))
+	return Success(fmt.Sprintf("Created invite for %s", inviteDto.LoginOrEmail))
 }
 }
 
 
 func inviteExistingUserToOrg(c *m.ReqContext, user *m.User, inviteDto *dtos.AddInviteForm) Response {
 func inviteExistingUserToOrg(c *m.ReqContext, user *m.User, inviteDto *dtos.AddInviteForm) Response {
@@ -93,29 +93,28 @@ func inviteExistingUserToOrg(c *m.ReqContext, user *m.User, inviteDto *dtos.AddI
 	createOrgUserCmd := m.AddOrgUserCommand{OrgId: c.OrgId, UserId: user.Id, Role: inviteDto.Role}
 	createOrgUserCmd := m.AddOrgUserCommand{OrgId: c.OrgId, UserId: user.Id, Role: inviteDto.Role}
 	if err := bus.Dispatch(&createOrgUserCmd); err != nil {
 	if err := bus.Dispatch(&createOrgUserCmd); err != nil {
 		if err == m.ErrOrgUserAlreadyAdded {
 		if err == m.ErrOrgUserAlreadyAdded {
-			return ApiError(412, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err)
+			return Error(412, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err)
 		}
 		}
-		return ApiError(500, "Error while trying to create org user", err)
-	} else {
+		return Error(500, "Error while trying to create org user", err)
+	}
 
 
-		if inviteDto.SendEmail && util.IsEmail(user.Email) {
-			emailCmd := m.SendEmailCommand{
-				To:       []string{user.Email},
-				Template: "invited_to_org.html",
-				Data: map[string]interface{}{
-					"Name":      user.NameOrFallback(),
-					"OrgName":   c.OrgName,
-					"InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login),
-				},
-			}
-
-			if err := bus.Dispatch(&emailCmd); err != nil {
-				return ApiError(500, "Failed to send email invited_to_org", err)
-			}
+	if inviteDto.SendEmail && util.IsEmail(user.Email) {
+		emailCmd := m.SendEmailCommand{
+			To:       []string{user.Email},
+			Template: "invited_to_org.html",
+			Data: map[string]interface{}{
+				"Name":      user.NameOrFallback(),
+				"OrgName":   c.OrgName,
+				"InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login),
+			},
 		}
 		}
 
 
-		return ApiSuccess(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName))
+		if err := bus.Dispatch(&emailCmd); err != nil {
+			return Error(500, "Failed to send email invited_to_org", err)
+		}
 	}
 	}
+
+	return Success(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName))
 }
 }
 
 
 func RevokeInvite(c *m.ReqContext) Response {
 func RevokeInvite(c *m.ReqContext) Response {
@@ -123,7 +122,7 @@ func RevokeInvite(c *m.ReqContext) Response {
 		return rsp
 		return rsp
 	}
 	}
 
 
-	return ApiSuccess("Invite revoked")
+	return Success("Invite revoked")
 }
 }
 
 
 func GetInviteInfoByCode(c *m.ReqContext) Response {
 func GetInviteInfoByCode(c *m.ReqContext) Response {
@@ -131,14 +130,14 @@ func GetInviteInfoByCode(c *m.ReqContext) Response {
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrTempUserNotFound {
 		if err == m.ErrTempUserNotFound {
-			return ApiError(404, "Invite not found", nil)
+			return Error(404, "Invite not found", nil)
 		}
 		}
-		return ApiError(500, "Failed to get invite", err)
+		return Error(500, "Failed to get invite", err)
 	}
 	}
 
 
 	invite := query.Result
 	invite := query.Result
 
 
-	return Json(200, dtos.InviteInfo{
+	return JSON(200, dtos.InviteInfo{
 		Email:     invite.Email,
 		Email:     invite.Email,
 		Name:      invite.Name,
 		Name:      invite.Name,
 		Username:  invite.Email,
 		Username:  invite.Email,
@@ -151,14 +150,14 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrTempUserNotFound {
 		if err == m.ErrTempUserNotFound {
-			return ApiError(404, "Invite not found", nil)
+			return Error(404, "Invite not found", nil)
 		}
 		}
-		return ApiError(500, "Failed to get invite", err)
+		return Error(500, "Failed to get invite", err)
 	}
 	}
 
 
 	invite := query.Result
 	invite := query.Result
 	if invite.Status != m.TmpUserInvitePending {
 	if invite.Status != m.TmpUserInvitePending {
-		return ApiError(412, fmt.Sprintf("Invite cannot be used in status %s", invite.Status), nil)
+		return Error(412, fmt.Sprintf("Invite cannot be used in status %s", invite.Status), nil)
 	}
 	}
 
 
 	cmd := m.CreateUserCommand{
 	cmd := m.CreateUserCommand{
@@ -170,7 +169,7 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "failed to create user", err)
+		return Error(500, "failed to create user", err)
 	}
 	}
 
 
 	user := &cmd.Result
 	user := &cmd.Result
@@ -189,14 +188,14 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
 	metrics.M_Api_User_SignUpCompleted.Inc()
 	metrics.M_Api_User_SignUpCompleted.Inc()
 	metrics.M_Api_User_SignUpInvite.Inc()
 	metrics.M_Api_User_SignUpInvite.Inc()
 
 
-	return ApiSuccess("User created and logged in")
+	return Success("User created and logged in")
 }
 }
 
 
 func updateTempUserStatus(code string, status m.TempUserStatus) (bool, Response) {
 func updateTempUserStatus(code string, status m.TempUserStatus) (bool, Response) {
 	// update temp user status
 	// update temp user status
 	updateTmpUserCmd := m.UpdateTempUserStatusCommand{Code: code, Status: status}
 	updateTmpUserCmd := m.UpdateTempUserStatusCommand{Code: code, Status: status}
 	if err := bus.Dispatch(&updateTmpUserCmd); err != nil {
 	if err := bus.Dispatch(&updateTmpUserCmd); err != nil {
-		return false, ApiError(500, "Failed to update invite status", err)
+		return false, Error(500, "Failed to update invite status", err)
 	}
 	}
 
 
 	return true, nil
 	return true, nil
@@ -207,7 +206,7 @@ func applyUserInvite(user *m.User, invite *m.TempUserDTO, setActive bool) (bool,
 	addOrgUserCmd := m.AddOrgUserCommand{OrgId: invite.OrgId, UserId: user.Id, Role: invite.Role}
 	addOrgUserCmd := m.AddOrgUserCommand{OrgId: invite.OrgId, UserId: user.Id, Role: invite.Role}
 	if err := bus.Dispatch(&addOrgUserCmd); err != nil {
 	if err := bus.Dispatch(&addOrgUserCmd); err != nil {
 		if err != m.ErrOrgUserAlreadyAdded {
 		if err != m.ErrOrgUserAlreadyAdded {
-			return false, ApiError(500, "Error while trying to create org user", err)
+			return false, Error(500, "Error while trying to create org user", err)
 		}
 		}
 	}
 	}
 
 
@@ -219,7 +218,7 @@ func applyUserInvite(user *m.User, invite *m.TempUserDTO, setActive bool) (bool,
 	if setActive {
 	if setActive {
 		// set org to active
 		// set org to active
 		if err := bus.Dispatch(&m.SetUsingOrgCommand{OrgId: invite.OrgId, UserId: user.Id}); err != nil {
 		if err := bus.Dispatch(&m.SetUsingOrgCommand{OrgId: invite.OrgId, UserId: user.Id}); err != nil {
-			return false, ApiError(500, "Failed to set org as active", err)
+			return false, Error(500, "Failed to set org as active", err)
 		}
 		}
 	}
 	}
 
 

+ 23 - 23
pkg/api/org_users.go

@@ -20,13 +20,13 @@ func AddOrgUser(c *m.ReqContext, cmd m.AddOrgUserCommand) Response {
 
 
 func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
 func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
 	if !cmd.Role.IsValid() {
 	if !cmd.Role.IsValid() {
-		return ApiError(400, "Invalid role specified", nil)
+		return Error(400, "Invalid role specified", nil)
 	}
 	}
 
 
 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
 	err := bus.Dispatch(&userQuery)
 	err := bus.Dispatch(&userQuery)
 	if err != nil {
 	if err != nil {
-		return ApiError(404, "User not found", nil)
+		return Error(404, "User not found", nil)
 	}
 	}
 
 
 	userToAdd := userQuery.Result
 	userToAdd := userQuery.Result
@@ -35,12 +35,12 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		if err == m.ErrOrgUserAlreadyAdded {
 		if err == m.ErrOrgUserAlreadyAdded {
-			return ApiError(409, "User is already member of this organization", nil)
+			return Error(409, "User is already member of this organization", nil)
 		}
 		}
-		return ApiError(500, "Could not add user to organization", err)
+		return Error(500, "Could not add user to organization", err)
 	}
 	}
 
 
-	return ApiSuccess("User added to organization")
+	return Success("User added to organization")
 }
 }
 
 
 // GET /api/org/users
 // GET /api/org/users
@@ -53,22 +53,22 @@ func GetOrgUsers(c *m.ReqContext) Response {
 	return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0)
 	return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0)
 }
 }
 
 
-func getOrgUsersHelper(orgId int64, query string, limit int) Response {
+func getOrgUsersHelper(orgID int64, query string, limit int) Response {
 	q := m.GetOrgUsersQuery{
 	q := m.GetOrgUsersQuery{
-		OrgId: orgId,
+		OrgId: orgID,
 		Query: query,
 		Query: query,
 		Limit: limit,
 		Limit: limit,
 	}
 	}
 
 
 	if err := bus.Dispatch(&q); err != nil {
 	if err := bus.Dispatch(&q); err != nil {
-		return ApiError(500, "Failed to get account user", err)
+		return Error(500, "Failed to get account user", err)
 	}
 	}
 
 
 	for _, user := range q.Result {
 	for _, user := range q.Result {
 		user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
 		user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
 	}
 	}
 
 
-	return Json(200, q.Result)
+	return JSON(200, q.Result)
 }
 }
 
 
 // PATCH /api/org/users/:userId
 // PATCH /api/org/users/:userId
@@ -87,41 +87,41 @@ func UpdateOrgUser(c *m.ReqContext, cmd m.UpdateOrgUserCommand) Response {
 
 
 func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
 func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
 	if !cmd.Role.IsValid() {
 	if !cmd.Role.IsValid() {
-		return ApiError(400, "Invalid role specified", nil)
+		return Error(400, "Invalid role specified", nil)
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		if err == m.ErrLastOrgAdmin {
 		if err == m.ErrLastOrgAdmin {
-			return ApiError(400, "Cannot change role so that there is no organization admin left", nil)
+			return Error(400, "Cannot change role so that there is no organization admin left", nil)
 		}
 		}
-		return ApiError(500, "Failed update org user", err)
+		return Error(500, "Failed update org user", err)
 	}
 	}
 
 
-	return ApiSuccess("Organization user updated")
+	return Success("Organization user updated")
 }
 }
 
 
 // DELETE /api/org/users/:userId
 // DELETE /api/org/users/:userId
 func RemoveOrgUserForCurrentOrg(c *m.ReqContext) Response {
 func RemoveOrgUserForCurrentOrg(c *m.ReqContext) Response {
-	userId := c.ParamsInt64(":userId")
-	return removeOrgUserHelper(c.OrgId, userId)
+	userID := c.ParamsInt64(":userId")
+	return removeOrgUserHelper(c.OrgId, userID)
 }
 }
 
 
 // DELETE /api/orgs/:orgId/users/:userId
 // DELETE /api/orgs/:orgId/users/:userId
 func RemoveOrgUser(c *m.ReqContext) Response {
 func RemoveOrgUser(c *m.ReqContext) Response {
-	userId := c.ParamsInt64(":userId")
-	orgId := c.ParamsInt64(":orgId")
-	return removeOrgUserHelper(orgId, userId)
+	userID := c.ParamsInt64(":userId")
+	orgID := c.ParamsInt64(":orgId")
+	return removeOrgUserHelper(orgID, userID)
 }
 }
 
 
-func removeOrgUserHelper(orgId int64, userId int64) Response {
-	cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: userId}
+func removeOrgUserHelper(orgID int64, userID int64) Response {
+	cmd := m.RemoveOrgUserCommand{OrgId: orgID, UserId: userID}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		if err == m.ErrLastOrgAdmin {
 		if err == m.ErrLastOrgAdmin {
-			return ApiError(400, "Cannot remove last organization admin", nil)
+			return Error(400, "Cannot remove last organization admin", nil)
 		}
 		}
-		return ApiError(500, "Failed to remove user from organization", err)
+		return Error(500, "Failed to remove user from organization", err)
 	}
 	}
 
 
-	return ApiSuccess("User removed from organization")
+	return Success("User removed from organization")
 }
 }

+ 8 - 8
pkg/api/password.go

@@ -12,15 +12,15 @@ func SendResetPasswordEmail(c *m.ReqContext, form dtos.SendResetPasswordEmailFor
 
 
 	if err := bus.Dispatch(&userQuery); err != nil {
 	if err := bus.Dispatch(&userQuery); err != nil {
 		c.Logger.Info("Requested password reset for user that was not found", "user", userQuery.LoginOrEmail)
 		c.Logger.Info("Requested password reset for user that was not found", "user", userQuery.LoginOrEmail)
-		return ApiError(200, "Email sent", err)
+		return Error(200, "Email sent", err)
 	}
 	}
 
 
 	emailCmd := m.SendResetPasswordEmailCommand{User: userQuery.Result}
 	emailCmd := m.SendResetPasswordEmailCommand{User: userQuery.Result}
 	if err := bus.Dispatch(&emailCmd); err != nil {
 	if err := bus.Dispatch(&emailCmd); err != nil {
-		return ApiError(500, "Failed to send email", err)
+		return Error(500, "Failed to send email", err)
 	}
 	}
 
 
-	return ApiSuccess("Email sent")
+	return Success("Email sent")
 }
 }
 
 
 func ResetPassword(c *m.ReqContext, form dtos.ResetUserPasswordForm) Response {
 func ResetPassword(c *m.ReqContext, form dtos.ResetUserPasswordForm) Response {
@@ -28,13 +28,13 @@ func ResetPassword(c *m.ReqContext, form dtos.ResetUserPasswordForm) Response {
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrInvalidEmailCode {
 		if err == m.ErrInvalidEmailCode {
-			return ApiError(400, "Invalid or expired reset password code", nil)
+			return Error(400, "Invalid or expired reset password code", nil)
 		}
 		}
-		return ApiError(500, "Unknown error validating email code", err)
+		return Error(500, "Unknown error validating email code", err)
 	}
 	}
 
 
 	if form.NewPassword != form.ConfirmPassword {
 	if form.NewPassword != form.ConfirmPassword {
-		return ApiError(400, "Passwords do not match", nil)
+		return Error(400, "Passwords do not match", nil)
 	}
 	}
 
 
 	cmd := m.ChangeUserPasswordCommand{}
 	cmd := m.ChangeUserPasswordCommand{}
@@ -42,8 +42,8 @@ func ResetPassword(c *m.ReqContext, form dtos.ResetUserPasswordForm) Response {
 	cmd.NewPassword = util.EncodePassword(form.NewPassword, query.Result.Salt)
 	cmd.NewPassword = util.EncodePassword(form.NewPassword, query.Result.Salt)
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to change user password", err)
+		return Error(500, "Failed to change user password", err)
 	}
 	}
 
 
-	return ApiSuccess("User password changed")
+	return Success("User password changed")
 }
 }

+ 17 - 17
pkg/api/playlist.go

@@ -55,10 +55,10 @@ func SearchPlaylists(c *m.ReqContext) Response {
 
 
 	err := bus.Dispatch(&searchQuery)
 	err := bus.Dispatch(&searchQuery)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Search failed", err)
+		return Error(500, "Search failed", err)
 	}
 	}
 
 
-	return Json(200, searchQuery.Result)
+	return JSON(200, searchQuery.Result)
 }
 }
 
 
 func GetPlaylist(c *m.ReqContext) Response {
 func GetPlaylist(c *m.ReqContext) Response {
@@ -66,7 +66,7 @@ func GetPlaylist(c *m.ReqContext) Response {
 	cmd := m.GetPlaylistByIdQuery{Id: id}
 	cmd := m.GetPlaylistByIdQuery{Id: id}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Playlist not found", err)
+		return Error(500, "Playlist not found", err)
 	}
 	}
 
 
 	playlistDTOs, _ := LoadPlaylistItemDTOs(id)
 	playlistDTOs, _ := LoadPlaylistItemDTOs(id)
@@ -79,7 +79,7 @@ func GetPlaylist(c *m.ReqContext) Response {
 		Items:    playlistDTOs,
 		Items:    playlistDTOs,
 	}
 	}
 
 
-	return Json(200, dto)
+	return JSON(200, dto)
 }
 }
 
 
 func LoadPlaylistItemDTOs(id int64) ([]m.PlaylistItemDTO, error) {
 func LoadPlaylistItemDTOs(id int64) ([]m.PlaylistItemDTO, error) {
@@ -120,21 +120,21 @@ func GetPlaylistItems(c *m.ReqContext) Response {
 	playlistDTOs, err := LoadPlaylistItemDTOs(id)
 	playlistDTOs, err := LoadPlaylistItemDTOs(id)
 
 
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Could not load playlist items", err)
+		return Error(500, "Could not load playlist items", err)
 	}
 	}
 
 
-	return Json(200, playlistDTOs)
+	return JSON(200, playlistDTOs)
 }
 }
 
 
 func GetPlaylistDashboards(c *m.ReqContext) Response {
 func GetPlaylistDashboards(c *m.ReqContext) Response {
-	playlistId := c.ParamsInt64(":id")
+	playlistID := c.ParamsInt64(":id")
 
 
-	playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistId)
+	playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistID)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Could not load dashboards", err)
+		return Error(500, "Could not load dashboards", err)
 	}
 	}
 
 
-	return Json(200, playlists)
+	return JSON(200, playlists)
 }
 }
 
 
 func DeletePlaylist(c *m.ReqContext) Response {
 func DeletePlaylist(c *m.ReqContext) Response {
@@ -142,34 +142,34 @@ func DeletePlaylist(c *m.ReqContext) Response {
 
 
 	cmd := m.DeletePlaylistCommand{Id: id, OrgId: c.OrgId}
 	cmd := m.DeletePlaylistCommand{Id: id, OrgId: c.OrgId}
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to delete playlist", err)
+		return Error(500, "Failed to delete playlist", err)
 	}
 	}
 
 
-	return Json(200, "")
+	return JSON(200, "")
 }
 }
 
 
 func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response {
 func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to create playlist", err)
+		return Error(500, "Failed to create playlist", err)
 	}
 	}
 
 
-	return Json(200, cmd.Result)
+	return JSON(200, cmd.Result)
 }
 }
 
 
 func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
 func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to save playlist", err)
+		return Error(500, "Failed to save playlist", err)
 	}
 	}
 
 
 	playlistDTOs, err := LoadPlaylistItemDTOs(cmd.Id)
 	playlistDTOs, err := LoadPlaylistItemDTOs(cmd.Id)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to save playlist", err)
+		return Error(500, "Failed to save playlist", err)
 	}
 	}
 
 
 	cmd.Result.Items = playlistDTOs
 	cmd.Result.Items = playlistDTOs
-	return Json(200, cmd.Result)
+	return JSON(200, cmd.Result)
 }
 }

+ 31 - 33
pkg/api/playlist_play.go

@@ -11,11 +11,11 @@ import (
 	"github.com/grafana/grafana/pkg/services/search"
 	"github.com/grafana/grafana/pkg/services/search"
 )
 )
 
 
-func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]int) (dtos.PlaylistDashboardsSlice, error) {
+func populateDashboardsByID(dashboardByIDs []int64, dashboardIDOrder map[int64]int) (dtos.PlaylistDashboardsSlice, error) {
 	result := make(dtos.PlaylistDashboardsSlice, 0)
 	result := make(dtos.PlaylistDashboardsSlice, 0)
 
 
-	if len(dashboardByIds) > 0 {
-		dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIds}
+	if len(dashboardByIDs) > 0 {
+		dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIDs}
 		if err := bus.Dispatch(&dashboardQuery); err != nil {
 		if err := bus.Dispatch(&dashboardQuery); err != nil {
 			return result, err
 			return result, err
 		}
 		}
@@ -26,7 +26,7 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i
 				Slug:  item.Slug,
 				Slug:  item.Slug,
 				Title: item.Title,
 				Title: item.Title,
 				Uri:   "db/" + item.Slug,
 				Uri:   "db/" + item.Slug,
-				Order: dashboardIdOrder[item.Id],
+				Order: dashboardIDOrder[item.Id],
 			})
 			})
 		}
 		}
 	}
 	}
@@ -34,29 +34,27 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i
 	return result, nil
 	return result, nil
 }
 }
 
 
-func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
+func populateDashboardsByTag(orgID int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
 	result := make(dtos.PlaylistDashboardsSlice, 0)
 	result := make(dtos.PlaylistDashboardsSlice, 0)
 
 
-	if len(dashboardByTag) > 0 {
-		for _, tag := range dashboardByTag {
-			searchQuery := search.Query{
-				Title:        "",
-				Tags:         []string{tag},
-				SignedInUser: signedInUser,
-				Limit:        100,
-				IsStarred:    false,
-				OrgId:        orgId,
-			}
+	for _, tag := range dashboardByTag {
+		searchQuery := search.Query{
+			Title:        "",
+			Tags:         []string{tag},
+			SignedInUser: signedInUser,
+			Limit:        100,
+			IsStarred:    false,
+			OrgId:        orgID,
+		}
 
 
-			if err := bus.Dispatch(&searchQuery); err == nil {
-				for _, item := range searchQuery.Result {
-					result = append(result, dtos.PlaylistDashboard{
-						Id:    item.Id,
-						Title: item.Title,
-						Uri:   item.Uri,
-						Order: dashboardTagOrder[tag],
-					})
-				}
+		if err := bus.Dispatch(&searchQuery); err == nil {
+			for _, item := range searchQuery.Result {
+				result = append(result, dtos.PlaylistDashboard{
+					Id:    item.Id,
+					Title: item.Title,
+					Uri:   item.Uri,
+					Order: dashboardTagOrder[tag],
+				})
 			}
 			}
 		}
 		}
 	}
 	}
@@ -64,19 +62,19 @@ func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboar
 	return result
 	return result
 }
 }
 
 
-func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
-	playlistItems, _ := LoadPlaylistItems(playlistId)
+func LoadPlaylistDashboards(orgID int64, signedInUser *m.SignedInUser, playlistID int64) (dtos.PlaylistDashboardsSlice, error) {
+	playlistItems, _ := LoadPlaylistItems(playlistID)
 
 
-	dashboardByIds := make([]int64, 0)
+	dashboardByIDs := make([]int64, 0)
 	dashboardByTag := make([]string, 0)
 	dashboardByTag := make([]string, 0)
-	dashboardIdOrder := make(map[int64]int)
+	dashboardIDOrder := make(map[int64]int)
 	dashboardTagOrder := make(map[string]int)
 	dashboardTagOrder := make(map[string]int)
 
 
 	for _, i := range playlistItems {
 	for _, i := range playlistItems {
 		if i.Type == "dashboard_by_id" {
 		if i.Type == "dashboard_by_id" {
-			dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
-			dashboardByIds = append(dashboardByIds, dashboardId)
-			dashboardIdOrder[dashboardId] = i.Order
+			dashboardID, _ := strconv.ParseInt(i.Value, 10, 64)
+			dashboardByIDs = append(dashboardByIDs, dashboardID)
+			dashboardIDOrder[dashboardID] = i.Order
 		}
 		}
 
 
 		if i.Type == "dashboard_by_tag" {
 		if i.Type == "dashboard_by_tag" {
@@ -87,9 +85,9 @@ func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistI
 
 
 	result := make(dtos.PlaylistDashboardsSlice, 0)
 	result := make(dtos.PlaylistDashboardsSlice, 0)
 
 
-	var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
+	var k, _ = populateDashboardsByID(dashboardByIDs, dashboardIDOrder)
 	result = append(result, k...)
 	result = append(result, k...)
-	result = append(result, populateDashboardsByTag(orgId, signedInUser, dashboardByTag, dashboardTagOrder)...)
+	result = append(result, populateDashboardsByTag(orgID, signedInUser, dashboardByTag, dashboardTagOrder)...)
 
 
 	sort.Sort(result)
 	sort.Sort(result)
 	return result, nil
 	return result, nil

+ 14 - 14
pkg/api/pluginproxy/ds_proxy.go

@@ -25,8 +25,8 @@ import (
 )
 )
 
 
 var (
 var (
-	logger log.Logger   = log.New("data-proxy-log")
-	client *http.Client = &http.Client{
+	logger = log.New("data-proxy-log")
+	client = &http.Client{
 		Timeout:   time.Second * 30,
 		Timeout:   time.Second * 30,
 		Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
 		Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
 	}
 	}
@@ -49,14 +49,14 @@ type DataSourceProxy struct {
 }
 }
 
 
 func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy {
 func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy {
-	targetUrl, _ := url.Parse(ds.Url)
+	targetURL, _ := url.Parse(ds.Url)
 
 
 	return &DataSourceProxy{
 	return &DataSourceProxy{
 		ds:        ds,
 		ds:        ds,
 		plugin:    plugin,
 		plugin:    plugin,
 		ctx:       ctx,
 		ctx:       ctx,
 		proxyPath: proxyPath,
 		proxyPath: proxyPath,
-		targetUrl: targetUrl,
+		targetUrl: targetURL,
 	}
 	}
 }
 }
 
 
@@ -279,16 +279,16 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
 		SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
 		SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
 	}
 	}
 
 
-	routeUrl, err := url.Parse(proxy.route.Url)
+	routeURL, err := url.Parse(proxy.route.Url)
 	if err != nil {
 	if err != nil {
 		logger.Error("Error parsing plugin route url")
 		logger.Error("Error parsing plugin route url")
 		return
 		return
 	}
 	}
 
 
-	req.URL.Scheme = routeUrl.Scheme
-	req.URL.Host = routeUrl.Host
-	req.Host = routeUrl.Host
-	req.URL.Path = util.JoinUrlFragments(routeUrl.Path, proxy.proxyPath)
+	req.URL.Scheme = routeURL.Scheme
+	req.URL.Host = routeURL.Host
+	req.Host = routeURL.Host
+	req.URL.Path = util.JoinUrlFragments(routeURL.Path, proxy.proxyPath)
 
 
 	if err := addHeaders(&req.Header, proxy.route, data); err != nil {
 	if err := addHeaders(&req.Header, proxy.route, data); err != nil {
 		logger.Error("Failed to render plugin headers", "error", err)
 		logger.Error("Failed to render plugin headers", "error", err)
@@ -320,11 +320,11 @@ func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error)
 
 
 	params := make(url.Values)
 	params := make(url.Values)
 	for key, value := range proxy.route.TokenAuth.Params {
 	for key, value := range proxy.route.TokenAuth.Params {
-		if interpolatedParam, err := interpolateString(value, data); err != nil {
+		interpolatedParam, err := interpolateString(value, data)
+		if err != nil {
 			return "", err
 			return "", err
-		} else {
-			params.Add(key, interpolatedParam)
 		}
 		}
+		params.Add(key, interpolatedParam)
 	}
 	}
 
 
 	getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode()))
 	getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode()))
@@ -354,13 +354,13 @@ func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error)
 func interpolateString(text string, data templateData) (string, error) {
 func interpolateString(text string, data templateData) (string, error) {
 	t, err := template.New("content").Parse(text)
 	t, err := template.New("content").Parse(text)
 	if err != nil {
 	if err != nil {
-		return "", errors.New(fmt.Sprintf("Could not parse template %s.", text))
+		return "", fmt.Errorf("could not parse template %s", text)
 	}
 	}
 
 
 	var contentBuf bytes.Buffer
 	var contentBuf bytes.Buffer
 	err = t.Execute(&contentBuf, data)
 	err = t.Execute(&contentBuf, data)
 	if err != nil {
 	if err != nil {
-		return "", errors.New(fmt.Sprintf("Failed to execute template %s.", text))
+		return "", fmt.Errorf("failed to execute template %s", text)
 	}
 	}
 
 
 	return contentBuf.String(), nil
 	return contentBuf.String(), nil

+ 8 - 8
pkg/api/pluginproxy/ds_proxy_test.go

@@ -107,8 +107,8 @@ func TestDSRouteRule(t *testing.T) {
 
 
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
 
 
-			requestUrl, _ := url.Parse("http://grafana.com/sub")
-			req := http.Request{URL: requestUrl}
+			requestURL, _ := url.Parse("http://grafana.com/sub")
+			req := http.Request{URL: requestURL}
 
 
 			proxy.getDirector()(&req)
 			proxy.getDirector()(&req)
 
 
@@ -132,8 +132,8 @@ func TestDSRouteRule(t *testing.T) {
 			ctx := &m.ReqContext{}
 			ctx := &m.ReqContext{}
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "")
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "")
 
 
-			requestUrl, _ := url.Parse("http://grafana.com/sub")
-			req := http.Request{URL: requestUrl}
+			requestURL, _ := url.Parse("http://grafana.com/sub")
+			req := http.Request{URL: requestURL}
 
 
 			proxy.getDirector()(&req)
 			proxy.getDirector()(&req)
 
 
@@ -162,8 +162,8 @@ func TestDSRouteRule(t *testing.T) {
 			ctx := &m.ReqContext{}
 			ctx := &m.ReqContext{}
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "")
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "")
 
 
-			requestUrl, _ := url.Parse("http://grafana.com/sub")
-			req := http.Request{URL: requestUrl, Header: make(http.Header)}
+			requestURL, _ := url.Parse("http://grafana.com/sub")
+			req := http.Request{URL: requestURL, Header: make(http.Header)}
 			cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
 			cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
 			req.Header.Set("Cookie", cookies)
 			req.Header.Set("Cookie", cookies)
 
 
@@ -188,8 +188,8 @@ func TestDSRouteRule(t *testing.T) {
 			ctx := &m.ReqContext{}
 			ctx := &m.ReqContext{}
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "")
 			proxy := NewDataSourceProxy(ds, plugin, ctx, "")
 
 
-			requestUrl, _ := url.Parse("http://grafana.com/sub")
-			req := http.Request{URL: requestUrl, Header: make(http.Header)}
+			requestURL, _ := url.Parse("http://grafana.com/sub")
+			req := http.Request{URL: requestURL, Header: make(http.Header)}
 			cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
 			cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
 			req.Header.Set("Cookie", cookies)
 			req.Header.Set("Cookie", cookies)
 
 

+ 9 - 9
pkg/api/pluginproxy/pluginproxy.go

@@ -19,10 +19,10 @@ type templateData struct {
 	SecureJsonData map[string]string
 	SecureJsonData map[string]string
 }
 }
 
 
-func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.Header, error) {
+func getHeaders(route *plugins.AppPluginRoute, orgId int64, appID string) (http.Header, error) {
 	result := http.Header{}
 	result := http.Header{}
 
 
-	query := m.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appId}
+	query := m.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appID}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		return nil, err
 		return nil, err
@@ -37,16 +37,16 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.
 	return result, err
 	return result, err
 }
 }
 
 
-func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy {
-	targetUrl, _ := url.Parse(route.Url)
+func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string) *httputil.ReverseProxy {
+	targetURL, _ := url.Parse(route.Url)
 
 
 	director := func(req *http.Request) {
 	director := func(req *http.Request) {
 
 
-		req.URL.Scheme = targetUrl.Scheme
-		req.URL.Host = targetUrl.Host
-		req.Host = targetUrl.Host
+		req.URL.Scheme = targetURL.Scheme
+		req.URL.Host = targetURL.Host
+		req.Host = targetURL.Host
 
 
-		req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
+		req.URL.Path = util.JoinUrlFragments(targetURL.Path, proxyPath)
 
 
 		// clear cookie headers
 		// clear cookie headers
 		req.Header.Del("Cookie")
 		req.Header.Del("Cookie")
@@ -80,7 +80,7 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
 		req.Header.Add("X-Grafana-Context", string(ctxJson))
 		req.Header.Add("X-Grafana-Context", string(ctxJson))
 
 
 		if len(route.Headers) > 0 {
 		if len(route.Headers) > 0 {
-			headers, err := getHeaders(route, ctx.OrgId, appId)
+			headers, err := getHeaders(route, ctx.OrgId, appID)
 			if err != nil {
 			if err != nil {
 				ctx.JsonApiErr(500, "Could not generate plugin route header", err)
 				ctx.JsonApiErr(500, "Could not generate plugin route header", err)
 				return
 				return

+ 55 - 53
pkg/api/plugins.go

@@ -19,7 +19,7 @@ func GetPluginList(c *m.ReqContext) Response {
 	pluginSettingsMap, err := plugins.GetPluginSettings(c.OrgId)
 	pluginSettingsMap, err := plugins.GetPluginSettings(c.OrgId)
 
 
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to get list of plugins", err)
+		return Error(500, "Failed to get list of plugins", err)
 	}
 	}
 
 
 	result := make(dtos.PluginList, 0)
 	result := make(dtos.PluginList, 0)
@@ -75,92 +75,94 @@ func GetPluginList(c *m.ReqContext) Response {
 	}
 	}
 
 
 	sort.Sort(result)
 	sort.Sort(result)
-	return Json(200, result)
+	return JSON(200, result)
 }
 }
 
 
-func GetPluginSettingById(c *m.ReqContext) Response {
-	pluginId := c.Params(":pluginId")
+func GetPluginSettingByID(c *m.ReqContext) Response {
+	pluginID := c.Params(":pluginId")
 
 
-	if def, exists := plugins.Plugins[pluginId]; !exists {
-		return ApiError(404, "Plugin not found, no installed plugin with that id", nil)
-	} else {
+	def, exists := plugins.Plugins[pluginID]
+	if !exists {
+		return Error(404, "Plugin not found, no installed plugin with that id", nil)
+	}
 
 
-		dto := &dtos.PluginSetting{
-			Type:          def.Type,
-			Id:            def.Id,
-			Name:          def.Name,
-			Info:          &def.Info,
-			Dependencies:  &def.Dependencies,
-			Includes:      def.Includes,
-			BaseUrl:       def.BaseUrl,
-			Module:        def.Module,
-			DefaultNavUrl: def.DefaultNavUrl,
-			LatestVersion: def.GrafanaNetVersion,
-			HasUpdate:     def.GrafanaNetHasUpdate,
-			State:         def.State,
-		}
+	dto := &dtos.PluginSetting{
+		Type:          def.Type,
+		Id:            def.Id,
+		Name:          def.Name,
+		Info:          &def.Info,
+		Dependencies:  &def.Dependencies,
+		Includes:      def.Includes,
+		BaseUrl:       def.BaseUrl,
+		Module:        def.Module,
+		DefaultNavUrl: def.DefaultNavUrl,
+		LatestVersion: def.GrafanaNetVersion,
+		HasUpdate:     def.GrafanaNetHasUpdate,
+		State:         def.State,
+	}
 
 
-		query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId}
-		if err := bus.Dispatch(&query); err != nil {
-			if err != m.ErrPluginSettingNotFound {
-				return ApiError(500, "Failed to get login settings", nil)
-			}
-		} else {
-			dto.Enabled = query.Result.Enabled
-			dto.Pinned = query.Result.Pinned
-			dto.JsonData = query.Result.JsonData
+	query := m.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
+	if err := bus.Dispatch(&query); err != nil {
+		if err != m.ErrPluginSettingNotFound {
+			return Error(500, "Failed to get login settings", nil)
 		}
 		}
-
-		return Json(200, dto)
+	} else {
+		dto.Enabled = query.Result.Enabled
+		dto.Pinned = query.Result.Pinned
+		dto.JsonData = query.Result.JsonData
 	}
 	}
+
+	return JSON(200, dto)
 }
 }
 
 
 func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response {
 func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response {
-	pluginId := c.Params(":pluginId")
+	pluginID := c.Params(":pluginId")
 
 
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
-	cmd.PluginId = pluginId
+	cmd.PluginId = pluginID
 
 
 	if _, ok := plugins.Apps[cmd.PluginId]; !ok {
 	if _, ok := plugins.Apps[cmd.PluginId]; !ok {
-		return ApiError(404, "Plugin not installed.", nil)
+		return Error(404, "Plugin not installed.", nil)
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to update plugin setting", err)
+		return Error(500, "Failed to update plugin setting", err)
 	}
 	}
 
 
-	return ApiSuccess("Plugin settings updated")
+	return Success("Plugin settings updated")
 }
 }
 
 
 func GetPluginDashboards(c *m.ReqContext) Response {
 func GetPluginDashboards(c *m.ReqContext) Response {
-	pluginId := c.Params(":pluginId")
+	pluginID := c.Params(":pluginId")
 
 
-	if list, err := plugins.GetPluginDashboards(c.OrgId, pluginId); err != nil {
+	list, err := plugins.GetPluginDashboards(c.OrgId, pluginID)
+	if err != nil {
 		if notfound, ok := err.(plugins.PluginNotFoundError); ok {
 		if notfound, ok := err.(plugins.PluginNotFoundError); ok {
-			return ApiError(404, notfound.Error(), nil)
+			return Error(404, notfound.Error(), nil)
 		}
 		}
 
 
-		return ApiError(500, "Failed to get plugin dashboards", err)
-	} else {
-		return Json(200, list)
+		return Error(500, "Failed to get plugin dashboards", err)
 	}
 	}
+
+	return JSON(200, list)
 }
 }
 
 
 func GetPluginMarkdown(c *m.ReqContext) Response {
 func GetPluginMarkdown(c *m.ReqContext) Response {
-	pluginId := c.Params(":pluginId")
+	pluginID := c.Params(":pluginId")
 	name := c.Params(":name")
 	name := c.Params(":name")
 
 
-	if content, err := plugins.GetPluginMarkdown(pluginId, name); err != nil {
+	content, err := plugins.GetPluginMarkdown(pluginID, name)
+	if err != nil {
 		if notfound, ok := err.(plugins.PluginNotFoundError); ok {
 		if notfound, ok := err.(plugins.PluginNotFoundError); ok {
-			return ApiError(404, notfound.Error(), nil)
+			return Error(404, notfound.Error(), nil)
 		}
 		}
 
 
-		return ApiError(500, "Could not get markdown file", err)
-	} else {
-		resp := Respond(200, content)
-		resp.Header("Content-Type", "text/plain; charset=utf-8")
-		return resp
+		return Error(500, "Could not get markdown file", err)
 	}
 	}
+
+	resp := Respond(200, content)
+	resp.Header("Content-Type", "text/plain; charset=utf-8")
+	return resp
 }
 }
 
 
 func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Response {
 func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Response {
@@ -176,8 +178,8 @@ func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Respon
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to import dashboard", err)
+		return Error(500, "Failed to import dashboard", err)
 	}
 	}
 
 
-	return Json(200, cmd.Result)
+	return JSON(200, cmd.Result)
 }
 }

+ 13 - 13
pkg/api/preferences.go

@@ -13,10 +13,10 @@ func SetHomeDashboard(c *m.ReqContext, cmd m.SavePreferencesCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to set home dashboard", err)
+		return Error(500, "Failed to set home dashboard", err)
 	}
 	}
 
 
-	return ApiSuccess("Home dashboard set")
+	return Success("Home dashboard set")
 }
 }
 
 
 // GET /api/user/preferences
 // GET /api/user/preferences
@@ -24,20 +24,20 @@ func GetUserPreferences(c *m.ReqContext) Response {
 	return getPreferencesFor(c.OrgId, c.UserId)
 	return getPreferencesFor(c.OrgId, c.UserId)
 }
 }
 
 
-func getPreferencesFor(orgId int64, userId int64) Response {
-	prefsQuery := m.GetPreferencesQuery{UserId: userId, OrgId: orgId}
+func getPreferencesFor(orgID int64, userID int64) Response {
+	prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID}
 
 
 	if err := bus.Dispatch(&prefsQuery); err != nil {
 	if err := bus.Dispatch(&prefsQuery); err != nil {
-		return ApiError(500, "Failed to get preferences", err)
+		return Error(500, "Failed to get preferences", err)
 	}
 	}
 
 
 	dto := dtos.Prefs{
 	dto := dtos.Prefs{
 		Theme:           prefsQuery.Result.Theme,
 		Theme:           prefsQuery.Result.Theme,
-		HomeDashboardId: prefsQuery.Result.HomeDashboardId,
+		HomeDashboardID: prefsQuery.Result.HomeDashboardId,
 		Timezone:        prefsQuery.Result.Timezone,
 		Timezone:        prefsQuery.Result.Timezone,
 	}
 	}
 
 
-	return Json(200, &dto)
+	return JSON(200, &dto)
 }
 }
 
 
 // PUT /api/user/preferences
 // PUT /api/user/preferences
@@ -45,20 +45,20 @@ func UpdateUserPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response
 	return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd)
 	return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd)
 }
 }
 
 
-func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
+func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
 	saveCmd := m.SavePreferencesCommand{
 	saveCmd := m.SavePreferencesCommand{
-		UserId:          userId,
-		OrgId:           orgId,
+		UserId:          userID,
+		OrgId:           orgID,
 		Theme:           dtoCmd.Theme,
 		Theme:           dtoCmd.Theme,
 		Timezone:        dtoCmd.Timezone,
 		Timezone:        dtoCmd.Timezone,
-		HomeDashboardId: dtoCmd.HomeDashboardId,
+		HomeDashboardId: dtoCmd.HomeDashboardID,
 	}
 	}
 
 
 	if err := bus.Dispatch(&saveCmd); err != nil {
 	if err := bus.Dispatch(&saveCmd); err != nil {
-		return ApiError(500, "Failed to save preferences", err)
+		return Error(500, "Failed to save preferences", err)
 	}
 	}
 
 
-	return ApiSuccess("Preferences updated")
+	return Success("Preferences updated")
 }
 }
 
 
 // GET /api/org/preferences
 // GET /api/org/preferences

+ 14 - 14
pkg/api/quota.go

@@ -8,60 +8,60 @@ import (
 
 
 func GetOrgQuotas(c *m.ReqContext) Response {
 func GetOrgQuotas(c *m.ReqContext) Response {
 	if !setting.Quota.Enabled {
 	if !setting.Quota.Enabled {
-		return ApiError(404, "Quotas not enabled", nil)
+		return Error(404, "Quotas not enabled", nil)
 	}
 	}
 	query := m.GetOrgQuotasQuery{OrgId: c.ParamsInt64(":orgId")}
 	query := m.GetOrgQuotasQuery{OrgId: c.ParamsInt64(":orgId")}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to get org quotas", err)
+		return Error(500, "Failed to get org quotas", err)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
 func UpdateOrgQuota(c *m.ReqContext, cmd m.UpdateOrgQuotaCmd) Response {
 func UpdateOrgQuota(c *m.ReqContext, cmd m.UpdateOrgQuotaCmd) Response {
 	if !setting.Quota.Enabled {
 	if !setting.Quota.Enabled {
-		return ApiError(404, "Quotas not enabled", nil)
+		return Error(404, "Quotas not enabled", nil)
 	}
 	}
 	cmd.OrgId = c.ParamsInt64(":orgId")
 	cmd.OrgId = c.ParamsInt64(":orgId")
 	cmd.Target = c.Params(":target")
 	cmd.Target = c.Params(":target")
 
 
 	if _, ok := setting.Quota.Org.ToMap()[cmd.Target]; !ok {
 	if _, ok := setting.Quota.Org.ToMap()[cmd.Target]; !ok {
-		return ApiError(404, "Invalid quota target", nil)
+		return Error(404, "Invalid quota target", nil)
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to update org quotas", err)
+		return Error(500, "Failed to update org quotas", err)
 	}
 	}
-	return ApiSuccess("Organization quota updated")
+	return Success("Organization quota updated")
 }
 }
 
 
 func GetUserQuotas(c *m.ReqContext) Response {
 func GetUserQuotas(c *m.ReqContext) Response {
 	if !setting.Quota.Enabled {
 	if !setting.Quota.Enabled {
-		return ApiError(404, "Quotas not enabled", nil)
+		return Error(404, "Quotas not enabled", nil)
 	}
 	}
 	query := m.GetUserQuotasQuery{UserId: c.ParamsInt64(":id")}
 	query := m.GetUserQuotasQuery{UserId: c.ParamsInt64(":id")}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to get org quotas", err)
+		return Error(500, "Failed to get org quotas", err)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
 func UpdateUserQuota(c *m.ReqContext, cmd m.UpdateUserQuotaCmd) Response {
 func UpdateUserQuota(c *m.ReqContext, cmd m.UpdateUserQuotaCmd) Response {
 	if !setting.Quota.Enabled {
 	if !setting.Quota.Enabled {
-		return ApiError(404, "Quotas not enabled", nil)
+		return Error(404, "Quotas not enabled", nil)
 	}
 	}
 	cmd.UserId = c.ParamsInt64(":id")
 	cmd.UserId = c.ParamsInt64(":id")
 	cmd.Target = c.Params(":target")
 	cmd.Target = c.Params(":target")
 
 
 	if _, ok := setting.Quota.User.ToMap()[cmd.Target]; !ok {
 	if _, ok := setting.Quota.User.ToMap()[cmd.Target]; !ok {
-		return ApiError(404, "Invalid quota target", nil)
+		return Error(404, "Invalid quota target", nil)
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to update org quotas", err)
+		return Error(500, "Failed to update org quotas", err)
 	}
 	}
-	return ApiSuccess("Organization quota updated")
+	return Success("Organization quota updated")
 }
 }

+ 8 - 8
pkg/api/search.go

@@ -25,19 +25,19 @@ func Search(c *m.ReqContext) {
 		permission = m.PERMISSION_EDIT
 		permission = m.PERMISSION_EDIT
 	}
 	}
 
 
-	dbids := make([]int64, 0)
+	dbIDs := make([]int64, 0)
 	for _, id := range c.QueryStrings("dashboardIds") {
 	for _, id := range c.QueryStrings("dashboardIds") {
-		dashboardId, err := strconv.ParseInt(id, 10, 64)
+		dashboardID, err := strconv.ParseInt(id, 10, 64)
 		if err == nil {
 		if err == nil {
-			dbids = append(dbids, dashboardId)
+			dbIDs = append(dbIDs, dashboardID)
 		}
 		}
 	}
 	}
 
 
-	folderIds := make([]int64, 0)
+	folderIDs := make([]int64, 0)
 	for _, id := range c.QueryStrings("folderIds") {
 	for _, id := range c.QueryStrings("folderIds") {
-		folderId, err := strconv.ParseInt(id, 10, 64)
+		folderID, err := strconv.ParseInt(id, 10, 64)
 		if err == nil {
 		if err == nil {
-			folderIds = append(folderIds, folderId)
+			folderIDs = append(folderIDs, folderID)
 		}
 		}
 	}
 	}
 
 
@@ -48,9 +48,9 @@ func Search(c *m.ReqContext) {
 		Limit:        limit,
 		Limit:        limit,
 		IsStarred:    starred == "true",
 		IsStarred:    starred == "true",
 		OrgId:        c.OrgId,
 		OrgId:        c.OrgId,
-		DashboardIds: dbids,
+		DashboardIds: dbIDs,
 		Type:         dashboardType,
 		Type:         dashboardType,
-		FolderIds:    folderIds,
+		FolderIds:    folderIDs,
 		Permission:   permission,
 		Permission:   permission,
 	}
 	}
 
 

+ 13 - 13
pkg/api/signup.go

@@ -12,7 +12,7 @@ import (
 
 
 // GET /api/user/signup/options
 // GET /api/user/signup/options
 func GetSignUpOptions(c *m.ReqContext) Response {
 func GetSignUpOptions(c *m.ReqContext) Response {
-	return Json(200, util.DynMap{
+	return JSON(200, util.DynMap{
 		"verifyEmailEnabled": setting.VerifyEmailEnabled,
 		"verifyEmailEnabled": setting.VerifyEmailEnabled,
 		"autoAssignOrg":      setting.AutoAssignOrg,
 		"autoAssignOrg":      setting.AutoAssignOrg,
 	})
 	})
@@ -21,12 +21,12 @@ func GetSignUpOptions(c *m.ReqContext) Response {
 // POST /api/user/signup
 // POST /api/user/signup
 func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
 func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
 	if !setting.AllowUserSignUp {
 	if !setting.AllowUserSignUp {
-		return ApiError(401, "User signup is disabled", nil)
+		return Error(401, "User signup is disabled", nil)
 	}
 	}
 
 
 	existing := m.GetUserByLoginQuery{LoginOrEmail: form.Email}
 	existing := m.GetUserByLoginQuery{LoginOrEmail: form.Email}
 	if err := bus.Dispatch(&existing); err == nil {
 	if err := bus.Dispatch(&existing); err == nil {
-		return ApiError(422, "User with same email address already exists", nil)
+		return Error(422, "User with same email address already exists", nil)
 	}
 	}
 
 
 	cmd := m.CreateTempUserCommand{}
 	cmd := m.CreateTempUserCommand{}
@@ -38,7 +38,7 @@ func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
 	cmd.RemoteAddr = c.Req.RemoteAddr
 	cmd.RemoteAddr = c.Req.RemoteAddr
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to create signup", err)
+		return Error(500, "Failed to create signup", err)
 	}
 	}
 
 
 	bus.Publish(&events.SignUpStarted{
 	bus.Publish(&events.SignUpStarted{
@@ -48,12 +48,12 @@ func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
 
 
 	metrics.M_Api_User_SignUpStarted.Inc()
 	metrics.M_Api_User_SignUpStarted.Inc()
 
 
-	return Json(200, util.DynMap{"status": "SignUpCreated"})
+	return JSON(200, util.DynMap{"status": "SignUpCreated"})
 }
 }
 
 
 func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
 func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
 	if !setting.AllowUserSignUp {
 	if !setting.AllowUserSignUp {
-		return ApiError(401, "User signup is disabled", nil)
+		return Error(401, "User signup is disabled", nil)
 	}
 	}
 
 
 	createUserCmd := m.CreateUserCommand{
 	createUserCmd := m.CreateUserCommand{
@@ -75,12 +75,12 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
 	// check if user exists
 	// check if user exists
 	existing := m.GetUserByLoginQuery{LoginOrEmail: form.Email}
 	existing := m.GetUserByLoginQuery{LoginOrEmail: form.Email}
 	if err := bus.Dispatch(&existing); err == nil {
 	if err := bus.Dispatch(&existing); err == nil {
-		return ApiError(401, "User with same email address already exists", nil)
+		return Error(401, "User with same email address already exists", nil)
 	}
 	}
 
 
 	// dispatch create command
 	// dispatch create command
 	if err := bus.Dispatch(&createUserCmd); err != nil {
 	if err := bus.Dispatch(&createUserCmd); err != nil {
-		return ApiError(500, "Failed to create user", err)
+		return Error(500, "Failed to create user", err)
 	}
 	}
 
 
 	// publish signup event
 	// publish signup event
@@ -98,7 +98,7 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
 	// check for pending invites
 	// check for pending invites
 	invitesQuery := m.GetTempUsersQuery{Email: form.Email, Status: m.TmpUserInvitePending}
 	invitesQuery := m.GetTempUsersQuery{Email: form.Email, Status: m.TmpUserInvitePending}
 	if err := bus.Dispatch(&invitesQuery); err != nil {
 	if err := bus.Dispatch(&invitesQuery); err != nil {
-		return ApiError(500, "Failed to query database for invites", err)
+		return Error(500, "Failed to query database for invites", err)
 	}
 	}
 
 
 	apiResponse := util.DynMap{"message": "User sign up completed successfully", "code": "redirect-to-landing-page"}
 	apiResponse := util.DynMap{"message": "User sign up completed successfully", "code": "redirect-to-landing-page"}
@@ -112,7 +112,7 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
 	loginUserWithUser(user, c)
 	loginUserWithUser(user, c)
 	metrics.M_Api_User_SignUpCompleted.Inc()
 	metrics.M_Api_User_SignUpCompleted.Inc()
 
 
-	return Json(200, apiResponse)
+	return JSON(200, apiResponse)
 }
 }
 
 
 func verifyUserSignUpEmail(email string, code string) (bool, Response) {
 func verifyUserSignUpEmail(email string, code string) (bool, Response) {
@@ -120,14 +120,14 @@ func verifyUserSignUpEmail(email string, code string) (bool, Response) {
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrTempUserNotFound {
 		if err == m.ErrTempUserNotFound {
-			return false, ApiError(404, "Invalid email verification code", nil)
+			return false, Error(404, "Invalid email verification code", nil)
 		}
 		}
-		return false, ApiError(500, "Failed to read temp user", err)
+		return false, Error(500, "Failed to read temp user", err)
 	}
 	}
 
 
 	tempUser := query.Result
 	tempUser := query.Result
 	if tempUser.Email != email {
 	if tempUser.Email != email {
-		return false, ApiError(404, "Email verification code does not match email", nil)
+		return false, Error(404, "Email verification code does not match email", nil)
 	}
 	}
 
 
 	return true, nil
 	return true, nil

+ 7 - 7
pkg/api/stars.go

@@ -7,20 +7,20 @@ import (
 
 
 func StarDashboard(c *m.ReqContext) Response {
 func StarDashboard(c *m.ReqContext) Response {
 	if !c.IsSignedIn {
 	if !c.IsSignedIn {
-		return ApiError(412, "You need to sign in to star dashboards", nil)
+		return Error(412, "You need to sign in to star dashboards", nil)
 	}
 	}
 
 
 	cmd := m.StarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
 	cmd := m.StarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
 
 
 	if cmd.DashboardId <= 0 {
 	if cmd.DashboardId <= 0 {
-		return ApiError(400, "Missing dashboard id", nil)
+		return Error(400, "Missing dashboard id", nil)
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to star dashboard", err)
+		return Error(500, "Failed to star dashboard", err)
 	}
 	}
 
 
-	return ApiSuccess("Dashboard starred!")
+	return Success("Dashboard starred!")
 }
 }
 
 
 func UnstarDashboard(c *m.ReqContext) Response {
 func UnstarDashboard(c *m.ReqContext) Response {
@@ -28,12 +28,12 @@ func UnstarDashboard(c *m.ReqContext) Response {
 	cmd := m.UnstarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
 	cmd := m.UnstarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
 
 
 	if cmd.DashboardId <= 0 {
 	if cmd.DashboardId <= 0 {
-		return ApiError(400, "Missing dashboard id", nil)
+		return Error(400, "Missing dashboard id", nil)
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to unstar dashboard", err)
+		return Error(500, "Failed to unstar dashboard", err)
 	}
 	}
 
 
-	return ApiSuccess("Dashboard unstarred")
+	return Success("Dashboard unstarred")
 }
 }

+ 16 - 16
pkg/api/team.go

@@ -12,12 +12,12 @@ func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		if err == m.ErrTeamNameTaken {
 		if err == m.ErrTeamNameTaken {
-			return ApiError(409, "Team name taken", err)
+			return Error(409, "Team name taken", err)
 		}
 		}
-		return ApiError(500, "Failed to create Team", err)
+		return Error(500, "Failed to create Team", err)
 	}
 	}
 
 
-	return Json(200, &util.DynMap{
+	return JSON(200, &util.DynMap{
 		"teamId":  cmd.Result.Id,
 		"teamId":  cmd.Result.Id,
 		"message": "Team created",
 		"message": "Team created",
 	})
 	})
@@ -29,23 +29,23 @@ func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response {
 	cmd.Id = c.ParamsInt64(":teamId")
 	cmd.Id = c.ParamsInt64(":teamId")
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		if err == m.ErrTeamNameTaken {
 		if err == m.ErrTeamNameTaken {
-			return ApiError(400, "Team name taken", err)
+			return Error(400, "Team name taken", err)
 		}
 		}
-		return ApiError(500, "Failed to update Team", err)
+		return Error(500, "Failed to update Team", err)
 	}
 	}
 
 
-	return ApiSuccess("Team updated")
+	return Success("Team updated")
 }
 }
 
 
 // DELETE /api/teams/:teamId
 // DELETE /api/teams/:teamId
-func DeleteTeamById(c *m.ReqContext) Response {
+func DeleteTeamByID(c *m.ReqContext) Response {
 	if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil {
 	if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil {
 		if err == m.ErrTeamNotFound {
 		if err == m.ErrTeamNotFound {
-			return ApiError(404, "Failed to delete Team. ID not found", nil)
+			return Error(404, "Failed to delete Team. ID not found", nil)
 		}
 		}
-		return ApiError(500, "Failed to update Team", err)
+		return Error(500, "Failed to update Team", err)
 	}
 	}
-	return ApiSuccess("Team deleted")
+	return Success("Team deleted")
 }
 }
 
 
 // GET /api/teams/search
 // GET /api/teams/search
@@ -68,7 +68,7 @@ func SearchTeams(c *m.ReqContext) Response {
 	}
 	}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to search Teams", err)
+		return Error(500, "Failed to search Teams", err)
 	}
 	}
 
 
 	for _, team := range query.Result.Teams {
 	for _, team := range query.Result.Teams {
@@ -78,20 +78,20 @@ func SearchTeams(c *m.ReqContext) Response {
 	query.Result.Page = page
 	query.Result.Page = page
 	query.Result.PerPage = perPage
 	query.Result.PerPage = perPage
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
 // GET /api/teams/:teamId
 // GET /api/teams/:teamId
-func GetTeamById(c *m.ReqContext) Response {
+func GetTeamByID(c *m.ReqContext) Response {
 	query := m.GetTeamByIdQuery{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}
 	query := m.GetTeamByIdQuery{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrTeamNotFound {
 		if err == m.ErrTeamNotFound {
-			return ApiError(404, "Team not found", err)
+			return Error(404, "Team not found", err)
 		}
 		}
 
 
-		return ApiError(500, "Failed to get Team", err)
+		return Error(500, "Failed to get Team", err)
 	}
 	}
 
 
-	return Json(200, &query.Result)
+	return JSON(200, &query.Result)
 }
 }

+ 10 - 10
pkg/api/team_members.go

@@ -12,14 +12,14 @@ func GetTeamMembers(c *m.ReqContext) Response {
 	query := m.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")}
 	query := m.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to get Team Members", err)
+		return Error(500, "Failed to get Team Members", err)
 	}
 	}
 
 
 	for _, member := range query.Result {
 	for _, member := range query.Result {
 		member.AvatarUrl = dtos.GetGravatarUrl(member.Email)
 		member.AvatarUrl = dtos.GetGravatarUrl(member.Email)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
 // POST /api/teams/:teamId/members
 // POST /api/teams/:teamId/members
@@ -29,17 +29,17 @@ func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		if err == m.ErrTeamNotFound {
 		if err == m.ErrTeamNotFound {
-			return ApiError(404, "Team not found", nil)
+			return Error(404, "Team not found", nil)
 		}
 		}
 
 
 		if err == m.ErrTeamMemberAlreadyAdded {
 		if err == m.ErrTeamMemberAlreadyAdded {
-			return ApiError(400, "User is already added to this team", nil)
+			return Error(400, "User is already added to this team", nil)
 		}
 		}
 
 
-		return ApiError(500, "Failed to add Member to Team", err)
+		return Error(500, "Failed to add Member to Team", err)
 	}
 	}
 
 
-	return Json(200, &util.DynMap{
+	return JSON(200, &util.DynMap{
 		"message": "Member added to Team",
 		"message": "Member added to Team",
 	})
 	})
 }
 }
@@ -48,14 +48,14 @@ func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
 func RemoveTeamMember(c *m.ReqContext) Response {
 func RemoveTeamMember(c *m.ReqContext) Response {
 	if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
 	if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
 		if err == m.ErrTeamNotFound {
 		if err == m.ErrTeamNotFound {
-			return ApiError(404, "Team not found", nil)
+			return Error(404, "Team not found", nil)
 		}
 		}
 
 
 		if err == m.ErrTeamMemberNotFound {
 		if err == m.ErrTeamMemberNotFound {
-			return ApiError(404, "Team member not found", nil)
+			return Error(404, "Team member not found", nil)
 		}
 		}
 
 
-		return ApiError(500, "Failed to remove Member from Team", err)
+		return Error(500, "Failed to remove Member from Team", err)
 	}
 	}
-	return ApiSuccess("Team Member removed")
+	return Success("Team Member removed")
 }
 }

+ 51 - 51
pkg/api/user.go

@@ -14,21 +14,21 @@ func GetSignedInUser(c *m.ReqContext) Response {
 }
 }
 
 
 // GET /api/users/:id
 // GET /api/users/:id
-func GetUserById(c *m.ReqContext) Response {
+func GetUserByID(c *m.ReqContext) Response {
 	return getUserUserProfile(c.ParamsInt64(":id"))
 	return getUserUserProfile(c.ParamsInt64(":id"))
 }
 }
 
 
-func getUserUserProfile(userId int64) Response {
-	query := m.GetUserProfileQuery{UserId: userId}
+func getUserUserProfile(userID int64) Response {
+	query := m.GetUserProfileQuery{UserId: userID}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrUserNotFound {
 		if err == m.ErrUserNotFound {
-			return ApiError(404, m.ErrUserNotFound.Error(), nil)
+			return Error(404, m.ErrUserNotFound.Error(), nil)
 		}
 		}
-		return ApiError(500, "Failed to get user", err)
+		return Error(500, "Failed to get user", err)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
 // GET /api/users/lookup
 // GET /api/users/lookup
@@ -36,9 +36,9 @@ func GetUserByLoginOrEmail(c *m.ReqContext) Response {
 	query := m.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")}
 	query := m.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")}
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		if err == m.ErrUserNotFound {
 		if err == m.ErrUserNotFound {
-			return ApiError(404, m.ErrUserNotFound.Error(), nil)
+			return Error(404, m.ErrUserNotFound.Error(), nil)
 		}
 		}
-		return ApiError(500, "Failed to get user", err)
+		return Error(500, "Failed to get user", err)
 	}
 	}
 	user := query.Result
 	user := query.Result
 	result := m.UserProfileDTO{
 	result := m.UserProfileDTO{
@@ -50,17 +50,17 @@ func GetUserByLoginOrEmail(c *m.ReqContext) Response {
 		IsGrafanaAdmin: user.IsAdmin,
 		IsGrafanaAdmin: user.IsAdmin,
 		OrgId:          user.OrgId,
 		OrgId:          user.OrgId,
 	}
 	}
-	return Json(200, &result)
+	return JSON(200, &result)
 }
 }
 
 
 // POST /api/user
 // POST /api/user
 func UpdateSignedInUser(c *m.ReqContext, cmd m.UpdateUserCommand) Response {
 func UpdateSignedInUser(c *m.ReqContext, cmd m.UpdateUserCommand) Response {
 	if setting.AuthProxyEnabled {
 	if setting.AuthProxyEnabled {
 		if setting.AuthProxyHeaderProperty == "email" && cmd.Email != c.Email {
 		if setting.AuthProxyHeaderProperty == "email" && cmd.Email != c.Email {
-			return ApiError(400, "Not allowed to change email when auth proxy is using email property", nil)
+			return Error(400, "Not allowed to change email when auth proxy is using email property", nil)
 		}
 		}
 		if setting.AuthProxyHeaderProperty == "username" && cmd.Login != c.Login {
 		if setting.AuthProxyHeaderProperty == "username" && cmd.Login != c.Login {
-			return ApiError(400, "Not allowed to change username when auth proxy is using username property", nil)
+			return Error(400, "Not allowed to change username when auth proxy is using username property", nil)
 		}
 		}
 	}
 	}
 	cmd.UserId = c.UserId
 	cmd.UserId = c.UserId
@@ -75,35 +75,35 @@ func UpdateUser(c *m.ReqContext, cmd m.UpdateUserCommand) Response {
 
 
 //POST /api/users/:id/using/:orgId
 //POST /api/users/:id/using/:orgId
 func UpdateUserActiveOrg(c *m.ReqContext) Response {
 func UpdateUserActiveOrg(c *m.ReqContext) Response {
-	userId := c.ParamsInt64(":id")
-	orgId := c.ParamsInt64(":orgId")
+	userID := c.ParamsInt64(":id")
+	orgID := c.ParamsInt64(":orgId")
 
 
-	if !validateUsingOrg(userId, orgId) {
-		return ApiError(401, "Not a valid organization", nil)
+	if !validateUsingOrg(userID, orgID) {
+		return Error(401, "Not a valid organization", nil)
 	}
 	}
 
 
-	cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId}
+	cmd := m.SetUsingOrgCommand{UserId: userID, OrgId: orgID}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to change active organization", err)
+		return Error(500, "Failed to change active organization", err)
 	}
 	}
 
 
-	return ApiSuccess("Active organization changed")
+	return Success("Active organization changed")
 }
 }
 
 
 func handleUpdateUser(cmd m.UpdateUserCommand) Response {
 func handleUpdateUser(cmd m.UpdateUserCommand) Response {
 	if len(cmd.Login) == 0 {
 	if len(cmd.Login) == 0 {
 		cmd.Login = cmd.Email
 		cmd.Login = cmd.Email
 		if len(cmd.Login) == 0 {
 		if len(cmd.Login) == 0 {
-			return ApiError(400, "Validation error, need to specify either username or email", nil)
+			return Error(400, "Validation error, need to specify either username or email", nil)
 		}
 		}
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to update user", err)
+		return Error(500, "Failed to update user", err)
 	}
 	}
 
 
-	return ApiSuccess("User updated")
+	return Success("User updated")
 }
 }
 
 
 // GET /api/user/orgs
 // GET /api/user/orgs
@@ -116,18 +116,18 @@ func GetUserOrgList(c *m.ReqContext) Response {
 	return getUserOrgList(c.ParamsInt64(":id"))
 	return getUserOrgList(c.ParamsInt64(":id"))
 }
 }
 
 
-func getUserOrgList(userId int64) Response {
-	query := m.GetUserOrgListQuery{UserId: userId}
+func getUserOrgList(userID int64) Response {
+	query := m.GetUserOrgListQuery{UserId: userID}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
-		return ApiError(500, "Failed to get user organizations", err)
+		return Error(500, "Failed to get user organizations", err)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
-func validateUsingOrg(userId int64, orgId int64) bool {
-	query := m.GetUserOrgListQuery{UserId: userId}
+func validateUsingOrg(userID int64, orgID int64) bool {
+	query := m.GetUserOrgListQuery{UserId: userID}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		return false
 		return false
@@ -136,7 +136,7 @@ func validateUsingOrg(userId int64, orgId int64) bool {
 	// validate that the org id in the list
 	// validate that the org id in the list
 	valid := false
 	valid := false
 	for _, other := range query.Result {
 	for _, other := range query.Result {
-		if other.OrgId == orgId {
+		if other.OrgId == orgID {
 			valid = true
 			valid = true
 		}
 		}
 	}
 	}
@@ -146,30 +146,30 @@ func validateUsingOrg(userId int64, orgId int64) bool {
 
 
 // POST /api/user/using/:id
 // POST /api/user/using/:id
 func UserSetUsingOrg(c *m.ReqContext) Response {
 func UserSetUsingOrg(c *m.ReqContext) Response {
-	orgId := c.ParamsInt64(":id")
+	orgID := c.ParamsInt64(":id")
 
 
-	if !validateUsingOrg(c.UserId, orgId) {
-		return ApiError(401, "Not a valid organization", nil)
+	if !validateUsingOrg(c.UserId, orgID) {
+		return Error(401, "Not a valid organization", nil)
 	}
 	}
 
 
-	cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
+	cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to change active organization", err)
+		return Error(500, "Failed to change active organization", err)
 	}
 	}
 
 
-	return ApiSuccess("Active organization changed")
+	return Success("Active organization changed")
 }
 }
 
 
 // GET /profile/switch-org/:id
 // GET /profile/switch-org/:id
 func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
 func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
-	orgId := c.ParamsInt64(":id")
+	orgID := c.ParamsInt64(":id")
 
 
-	if !validateUsingOrg(c.UserId, orgId) {
+	if !validateUsingOrg(c.UserId, orgID) {
 		NotFoundHandler(c)
 		NotFoundHandler(c)
 	}
 	}
 
 
-	cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
+	cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		NotFoundHandler(c)
 		NotFoundHandler(c)
@@ -180,53 +180,53 @@ func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
 
 
 func ChangeUserPassword(c *m.ReqContext, cmd m.ChangeUserPasswordCommand) Response {
 func ChangeUserPassword(c *m.ReqContext, cmd m.ChangeUserPasswordCommand) Response {
 	if setting.LdapEnabled || setting.AuthProxyEnabled {
 	if setting.LdapEnabled || setting.AuthProxyEnabled {
-		return ApiError(400, "Not allowed to change password when LDAP or Auth Proxy is enabled", nil)
+		return Error(400, "Not allowed to change password when LDAP or Auth Proxy is enabled", nil)
 	}
 	}
 
 
 	userQuery := m.GetUserByIdQuery{Id: c.UserId}
 	userQuery := m.GetUserByIdQuery{Id: c.UserId}
 
 
 	if err := bus.Dispatch(&userQuery); err != nil {
 	if err := bus.Dispatch(&userQuery); err != nil {
-		return ApiError(500, "Could not read user from database", err)
+		return Error(500, "Could not read user from database", err)
 	}
 	}
 
 
 	passwordHashed := util.EncodePassword(cmd.OldPassword, userQuery.Result.Salt)
 	passwordHashed := util.EncodePassword(cmd.OldPassword, userQuery.Result.Salt)
 	if passwordHashed != userQuery.Result.Password {
 	if passwordHashed != userQuery.Result.Password {
-		return ApiError(401, "Invalid old password", nil)
+		return Error(401, "Invalid old password", nil)
 	}
 	}
 
 
 	password := m.Password(cmd.NewPassword)
 	password := m.Password(cmd.NewPassword)
 	if password.IsWeak() {
 	if password.IsWeak() {
-		return ApiError(400, "New password is too short", nil)
+		return Error(400, "New password is too short", nil)
 	}
 	}
 
 
 	cmd.UserId = c.UserId
 	cmd.UserId = c.UserId
 	cmd.NewPassword = util.EncodePassword(cmd.NewPassword, userQuery.Result.Salt)
 	cmd.NewPassword = util.EncodePassword(cmd.NewPassword, userQuery.Result.Salt)
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to change user password", err)
+		return Error(500, "Failed to change user password", err)
 	}
 	}
 
 
-	return ApiSuccess("User password changed")
+	return Success("User password changed")
 }
 }
 
 
 // GET /api/users
 // GET /api/users
 func SearchUsers(c *m.ReqContext) Response {
 func SearchUsers(c *m.ReqContext) Response {
 	query, err := searchUser(c)
 	query, err := searchUser(c)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to fetch users", err)
+		return Error(500, "Failed to fetch users", err)
 	}
 	}
 
 
-	return Json(200, query.Result.Users)
+	return JSON(200, query.Result.Users)
 }
 }
 
 
 // GET /api/users/search
 // GET /api/users/search
 func SearchUsersWithPaging(c *m.ReqContext) Response {
 func SearchUsersWithPaging(c *m.ReqContext) Response {
 	query, err := searchUser(c)
 	query, err := searchUser(c)
 	if err != nil {
 	if err != nil {
-		return ApiError(500, "Failed to fetch users", err)
+		return Error(500, "Failed to fetch users", err)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return JSON(200, query.Result)
 }
 }
 
 
 func searchUser(c *m.ReqContext) (*m.SearchUsersQuery, error) {
 func searchUser(c *m.ReqContext) (*m.SearchUsersQuery, error) {
@@ -269,10 +269,10 @@ func SetHelpFlag(c *m.ReqContext) Response {
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to update help flag", err)
+		return Error(500, "Failed to update help flag", err)
 	}
 	}
 
 
-	return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
+	return JSON(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
 }
 }
 
 
 func ClearHelpFlags(c *m.ReqContext) Response {
 func ClearHelpFlags(c *m.ReqContext) Response {
@@ -282,8 +282,8 @@ func ClearHelpFlags(c *m.ReqContext) Response {
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		return ApiError(500, "Failed to update help flag", err)
+		return Error(500, "Failed to update help flag", err)
 	}
 	}
 
 
-	return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
+	return JSON(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
 }
 }

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

@@ -49,7 +49,7 @@ type GrafanaServerImpl struct {
 	childRoutines *errgroup.Group
 	childRoutines *errgroup.Group
 	log           log.Logger
 	log           log.Logger
 
 
-	httpServer *api.HttpServer
+	httpServer *api.HTTPServer
 }
 }
 
 
 func (g *GrafanaServerImpl) Start() error {
 func (g *GrafanaServerImpl) Start() error {
@@ -120,7 +120,7 @@ func (g *GrafanaServerImpl) initLogging() {
 }
 }
 
 
 func (g *GrafanaServerImpl) startHttpServer() error {
 func (g *GrafanaServerImpl) startHttpServer() error {
-	g.httpServer = api.NewHttpServer()
+	g.httpServer = api.NewHTTPServer()
 
 
 	err := g.httpServer.Start(g.context)
 	err := g.httpServer.Start(g.context)
 
 

+ 3 - 3
pkg/middleware/auth.go

@@ -17,10 +17,10 @@ type AuthOptions struct {
 }
 }
 
 
 func getRequestUserId(c *m.ReqContext) int64 {
 func getRequestUserId(c *m.ReqContext) int64 {
-	userId := c.Session.Get(session.SESS_KEY_USERID)
+	userID := c.Session.Get(session.SESS_KEY_USERID)
 
 
-	if userId != nil {
-		return userId.(int64)
+	if userID != nil {
+		return userID.(int64)
 	}
 	}
 
 
 	return 0
 	return 0

+ 52 - 49
pkg/middleware/auth_proxy.go

@@ -14,7 +14,7 @@ import (
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
-func initContextWithAuthProxy(ctx *m.ReqContext, orgId int64) bool {
+func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 	if !setting.AuthProxyEnabled {
 	if !setting.AuthProxyEnabled {
 		return false
 		return false
 	}
 	}
@@ -31,31 +31,31 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgId int64) bool {
 	}
 	}
 
 
 	query := getSignedInUserQueryForProxyAuth(proxyHeaderValue)
 	query := getSignedInUserQueryForProxyAuth(proxyHeaderValue)
-	query.OrgId = orgId
+	query.OrgId = orgID
 	if err := bus.Dispatch(query); err != nil {
 	if err := bus.Dispatch(query); err != nil {
 		if err != m.ErrUserNotFound {
 		if err != m.ErrUserNotFound {
 			ctx.Handle(500, "Failed to find user specified in auth proxy header", err)
 			ctx.Handle(500, "Failed to find user specified in auth proxy header", err)
 			return true
 			return true
 		}
 		}
 
 
-		if setting.AuthProxyAutoSignUp {
-			cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
-			if setting.LdapEnabled {
-				cmd.SkipOrgSetup = true
-			}
-
-			if err := bus.Dispatch(cmd); err != nil {
-				ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
-				return true
-			}
-			query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id, OrgId: orgId}
-			if err := bus.Dispatch(query); err != nil {
-				ctx.Handle(500, "Failed find user after creation", err)
-				return true
-			}
-		} else {
+		if !setting.AuthProxyAutoSignUp {
 			return false
 			return false
 		}
 		}
+
+		cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
+		if setting.LdapEnabled {
+			cmd.SkipOrgSetup = true
+		}
+
+		if err := bus.Dispatch(cmd); err != nil {
+			ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
+			return true
+		}
+		query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id, OrgId: orgID}
+		if err := bus.Dispatch(query); err != nil {
+			ctx.Handle(500, "Failed find user after creation", err)
+			return true
+		}
 	}
 	}
 
 
 	// initialize session
 	// initialize session
@@ -96,50 +96,53 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgId int64) bool {
 }
 }
 
 
 var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
 var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
-	if setting.LdapEnabled {
-		expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
+	if !setting.LdapEnabled {
+		return nil
+	}
 
 
-		var lastLdapSync int64
-		if lastLdapSyncInSession := ctx.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
-			lastLdapSync = lastLdapSyncInSession.(int64)
-		}
+	expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
 
 
-		if lastLdapSync < expireEpoch {
-			ldapCfg := login.LdapCfg
+	var lastLdapSync int64
+	if lastLdapSyncInSession := ctx.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
+		lastLdapSync = lastLdapSyncInSession.(int64)
+	}
 
 
-			for _, server := range ldapCfg.Servers {
-				author := login.NewLdapAuthenticator(server)
-				if err := author.SyncSignedInUser(query.Result); err != nil {
-					return err
-				}
-			}
+	if lastLdapSync < expireEpoch {
+		ldapCfg := login.LdapCfg
 
 
-			ctx.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
+		for _, server := range ldapCfg.Servers {
+			author := login.NewLdapAuthenticator(server)
+			if err := author.SyncSignedInUser(query.Result); err != nil {
+				return err
+			}
 		}
 		}
+
+		ctx.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
 	}
 	}
 
 
 	return nil
 	return nil
 }
 }
 
 
 func checkAuthenticationProxy(ctx *m.ReqContext, proxyHeaderValue string) error {
 func checkAuthenticationProxy(ctx *m.ReqContext, proxyHeaderValue string) error {
-	if len(strings.TrimSpace(setting.AuthProxyWhitelist)) > 0 {
-		proxies := strings.Split(setting.AuthProxyWhitelist, ",")
-		remoteAddrSplit := strings.Split(ctx.Req.RemoteAddr, ":")
-		sourceIP := remoteAddrSplit[0]
-
-		found := false
-		for _, proxyIP := range proxies {
-			if sourceIP == strings.TrimSpace(proxyIP) {
-				found = true
-				break
-			}
+	if len(strings.TrimSpace(setting.AuthProxyWhitelist)) == 0 {
+		return nil
+	}
+	proxies := strings.Split(setting.AuthProxyWhitelist, ",")
+	remoteAddrSplit := strings.Split(ctx.Req.RemoteAddr, ":")
+	sourceIP := remoteAddrSplit[0]
+
+	found := false
+	for _, proxyIP := range proxies {
+		if sourceIP == strings.TrimSpace(proxyIP) {
+			found = true
+			break
 		}
 		}
+	}
 
 
-		if !found {
-			msg := fmt.Sprintf("Request for user (%s) is not from the authentication proxy", proxyHeaderValue)
-			err := errors.New(msg)
-			return err
-		}
+	if !found {
+		msg := fmt.Sprintf("Request for user (%s) is not from the authentication proxy", proxyHeaderValue)
+		err := errors.New(msg)
+		return err
 	}
 	}
 
 
 	return nil
 	return nil

+ 6 - 6
pkg/middleware/dashboard_redirect.go

@@ -10,8 +10,8 @@ import (
 	"gopkg.in/macaron.v1"
 	"gopkg.in/macaron.v1"
 )
 )
 
 
-func getDashboardUrlBySlug(orgId int64, slug string) (string, error) {
-	query := m.GetDashboardQuery{Slug: slug, OrgId: orgId}
+func getDashboardURLBySlug(orgID int64, slug string) (string, error) {
+	query := m.GetDashboardQuery{Slug: slug, OrgId: orgID}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		return "", m.ErrDashboardNotFound
 		return "", m.ErrDashboardNotFound
@@ -20,12 +20,12 @@ func getDashboardUrlBySlug(orgId int64, slug string) (string, error) {
 	return m.GetDashboardUrl(query.Result.Uid, query.Result.Slug), nil
 	return m.GetDashboardUrl(query.Result.Uid, query.Result.Slug), nil
 }
 }
 
 
-func RedirectFromLegacyDashboardUrl() macaron.Handler {
+func RedirectFromLegacyDashboardURL() macaron.Handler {
 	return func(c *m.ReqContext) {
 	return func(c *m.ReqContext) {
 		slug := c.Params("slug")
 		slug := c.Params("slug")
 
 
 		if slug != "" {
 		if slug != "" {
-			if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
+			if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil {
 				url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
 				url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
 				c.Redirect(url, 301)
 				c.Redirect(url, 301)
 				return
 				return
@@ -34,13 +34,13 @@ func RedirectFromLegacyDashboardUrl() macaron.Handler {
 	}
 	}
 }
 }
 
 
-func RedirectFromLegacyDashboardSoloUrl() macaron.Handler {
+func RedirectFromLegacyDashboardSoloURL() macaron.Handler {
 	return func(c *m.ReqContext) {
 	return func(c *m.ReqContext) {
 		slug := c.Params("slug")
 		slug := c.Params("slug")
 		renderRequest := c.QueryBool("render")
 		renderRequest := c.QueryBool("render")
 
 
 		if slug != "" {
 		if slug != "" {
-			if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
+			if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil {
 				if renderRequest && strings.Contains(url, setting.AppSubUrl) {
 				if renderRequest && strings.Contains(url, setting.AppSubUrl) {
 					url = strings.Replace(url, setting.AppSubUrl, "", 1)
 					url = strings.Replace(url, setting.AppSubUrl, "", 1)
 				}
 				}

+ 10 - 10
pkg/middleware/dashboard_redirect_test.go

@@ -13,8 +13,8 @@ import (
 func TestMiddlewareDashboardRedirect(t *testing.T) {
 func TestMiddlewareDashboardRedirect(t *testing.T) {
 	Convey("Given the dashboard redirect middleware", t, func() {
 	Convey("Given the dashboard redirect middleware", t, func() {
 		bus.ClearBusHandlers()
 		bus.ClearBusHandlers()
-		redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardUrl()
-		redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloUrl()
+		redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardURL()
+		redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloURL()
 
 
 		fakeDash := m.NewDashboard("Child dash")
 		fakeDash := m.NewDashboard("Child dash")
 		fakeDash.Id = 1
 		fakeDash.Id = 1
@@ -34,9 +34,9 @@ func TestMiddlewareDashboardRedirect(t *testing.T) {
 
 
 			Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
 			Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
 				So(sc.resp.Code, ShouldEqual, 301)
 				So(sc.resp.Code, ShouldEqual, 301)
-				redirectUrl, _ := sc.resp.Result().Location()
-				So(redirectUrl.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug))
-				So(len(redirectUrl.Query()), ShouldEqual, 2)
+				redirectURL, _ := sc.resp.Result().Location()
+				So(redirectURL.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug))
+				So(len(redirectURL.Query()), ShouldEqual, 2)
 			})
 			})
 		})
 		})
 
 
@@ -47,11 +47,11 @@ func TestMiddlewareDashboardRedirect(t *testing.T) {
 
 
 			Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
 			Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
 				So(sc.resp.Code, ShouldEqual, 301)
 				So(sc.resp.Code, ShouldEqual, 301)
-				redirectUrl, _ := sc.resp.Result().Location()
-				expectedUrl := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)
-				expectedUrl = strings.Replace(expectedUrl, "/d/", "/d-solo/", 1)
-				So(redirectUrl.Path, ShouldEqual, expectedUrl)
-				So(len(redirectUrl.Query()), ShouldEqual, 2)
+				redirectURL, _ := sc.resp.Result().Location()
+				expectedURL := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)
+				expectedURL = strings.Replace(expectedURL, "/d/", "/d-solo/", 1)
+				So(redirectURL.Path, ShouldEqual, expectedURL)
+				So(len(redirectURL.Query()), ShouldEqual, 2)
 			})
 			})
 		})
 		})
 	})
 	})

+ 1 - 1
pkg/middleware/middleware_test.go

@@ -338,7 +338,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
 		sc.m.Use(GetContextHandler())
 		sc.m.Use(GetContextHandler())
 		// mock out gc goroutine
 		// mock out gc goroutine
 		session.StartSessionGC = func() {}
 		session.StartSessionGC = func() {}
-		sc.m.Use(Sessioner(&ms.Options{}))
+		sc.m.Use(Sessioner(&ms.Options{}, 0))
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(AddDefaultResponseHeaders())
 		sc.m.Use(AddDefaultResponseHeaders())
 
 

+ 7 - 7
pkg/middleware/recovery_test.go

@@ -14,10 +14,10 @@ import (
 
 
 func TestRecoveryMiddleware(t *testing.T) {
 func TestRecoveryMiddleware(t *testing.T) {
 	Convey("Given an api route that panics", t, func() {
 	Convey("Given an api route that panics", t, func() {
-		apiUrl := "/api/whatever"
-		recoveryScenario("recovery middleware should return json", apiUrl, func(sc *scenarioContext) {
+		apiURL := "/api/whatever"
+		recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) {
 			sc.handlerFunc = PanicHandler
 			sc.handlerFunc = PanicHandler
-			sc.fakeReq("GET", apiUrl).exec()
+			sc.fakeReq("GET", apiURL).exec()
 			sc.req.Header.Add("content-type", "application/json")
 			sc.req.Header.Add("content-type", "application/json")
 
 
 			So(sc.resp.Code, ShouldEqual, 500)
 			So(sc.resp.Code, ShouldEqual, 500)
@@ -27,10 +27,10 @@ func TestRecoveryMiddleware(t *testing.T) {
 	})
 	})
 
 
 	Convey("Given a non-api route that panics", t, func() {
 	Convey("Given a non-api route that panics", t, func() {
-		apiUrl := "/whatever"
-		recoveryScenario("recovery middleware should return html", apiUrl, func(sc *scenarioContext) {
+		apiURL := "/whatever"
+		recoveryScenario("recovery middleware should return html", apiURL, func(sc *scenarioContext) {
 			sc.handlerFunc = PanicHandler
 			sc.handlerFunc = PanicHandler
-			sc.fakeReq("GET", apiUrl).exec()
+			sc.fakeReq("GET", apiURL).exec()
 
 
 			So(sc.resp.Code, ShouldEqual, 500)
 			So(sc.resp.Code, ShouldEqual, 500)
 			So(sc.resp.Header().Get("content-type"), ShouldEqual, "text/html; charset=UTF-8")
 			So(sc.resp.Header().Get("content-type"), ShouldEqual, "text/html; charset=UTF-8")
@@ -63,7 +63,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
 		sc.m.Use(GetContextHandler())
 		sc.m.Use(GetContextHandler())
 		// mock out gc goroutine
 		// mock out gc goroutine
 		session.StartSessionGC = func() {}
 		session.StartSessionGC = func() {}
-		sc.m.Use(Sessioner(&ms.Options{}))
+		sc.m.Use(Sessioner(&ms.Options{}, 0))
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(AddDefaultResponseHeaders())
 		sc.m.Use(AddDefaultResponseHeaders())
 
 

+ 7 - 7
pkg/middleware/render_auth.go

@@ -19,16 +19,16 @@ func initContextWithRenderAuth(ctx *m.ReqContext) bool {
 	renderKeysLock.Lock()
 	renderKeysLock.Lock()
 	defer renderKeysLock.Unlock()
 	defer renderKeysLock.Unlock()
 
 
-	if renderUser, exists := renderKeys[key]; !exists {
+	renderUser, exists := renderKeys[key]
+	if !exists {
 		ctx.JsonApiErr(401, "Invalid Render Key", nil)
 		ctx.JsonApiErr(401, "Invalid Render Key", nil)
 		return true
 		return true
-	} else {
-
-		ctx.IsSignedIn = true
-		ctx.SignedInUser = renderUser
-		ctx.IsRenderCall = true
-		return true
 	}
 	}
+
+	ctx.IsSignedIn = true
+	ctx.SignedInUser = renderUser
+	ctx.IsRenderCall = true
+	return true
 }
 }
 
 
 type renderContextFunc func(key string) (string, error)
 type renderContextFunc func(key string) (string, error)

+ 2 - 2
pkg/middleware/session.go

@@ -8,8 +8,8 @@ import (
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/services/session"
 )
 )
 
 
-func Sessioner(options *ms.Options) macaron.Handler {
-	session.Init(options)
+func Sessioner(options *ms.Options, sessionConnMaxLifetime int64) macaron.Handler {
+	session.Init(options, sessionConnMaxLifetime)
 
 
 	return func(ctx *m.ReqContext) {
 	return func(ctx *m.ReqContext) {
 		ctx.Next()
 		ctx.Next()

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

@@ -86,17 +86,63 @@ func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
 		case <-grafanaCtx.Done():
 		case <-grafanaCtx.Done():
 			return dispatcherGroup.Wait()
 			return dispatcherGroup.Wait()
 		case job := <-e.execQueue:
 		case job := <-e.execQueue:
-			dispatcherGroup.Go(func() error { return e.processJob(alertCtx, job) })
+			dispatcherGroup.Go(func() error { return e.processJobWithRetry(alertCtx, job) })
 		}
 		}
 	}
 	}
 }
 }
 
 
 var (
 var (
 	unfinishedWorkTimeout time.Duration = time.Second * 5
 	unfinishedWorkTimeout time.Duration = time.Second * 5
-	alertTimeout          time.Duration = time.Second * 30
+	// TODO: Make alertTimeout and alertMaxAttempts configurable in the config file.
+	alertTimeout     time.Duration = time.Second * 30
+	alertMaxAttempts int           = 3
 )
 )
 
 
-func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
+func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
+	defer func() {
+		if err := recover(); err != nil {
+			e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
+		}
+	}()
+
+	cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
+	attemptChan := make(chan int, 1)
+
+	// Initialize with first attemptID=1
+	attemptChan <- 1
+	job.Running = true
+
+	for {
+		select {
+		case <-grafanaCtx.Done():
+			// In case grafana server context is cancel, let a chance to job processing
+			// to finish gracefully - by waiting a timeout duration - before forcing its end.
+			unfinishedWorkTimer := time.NewTimer(unfinishedWorkTimeout)
+			select {
+			case <-unfinishedWorkTimer.C:
+				return e.endJob(grafanaCtx.Err(), cancelChan, job)
+			case <-attemptChan:
+				return e.endJob(nil, cancelChan, job)
+			}
+		case attemptID, more := <-attemptChan:
+			if !more {
+				return e.endJob(nil, cancelChan, job)
+			}
+			go e.processJob(attemptID, attemptChan, cancelChan, job)
+		}
+	}
+}
+
+func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
+	job.Running = false
+	close(cancelChan)
+	for cancelFn := range cancelChan {
+		cancelFn()
+	}
+	return err
+}
+
+func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
 	defer func() {
 	defer func() {
 		if err := recover(); err != nil {
 		if err := recover(); err != nil {
 			e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
 			e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
@@ -104,14 +150,13 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
 	}()
 	}()
 
 
 	alertCtx, cancelFn := context.WithTimeout(context.Background(), alertTimeout)
 	alertCtx, cancelFn := context.WithTimeout(context.Background(), alertTimeout)
+	cancelChan <- cancelFn
 	span := opentracing.StartSpan("alert execution")
 	span := opentracing.StartSpan("alert execution")
 	alertCtx = opentracing.ContextWithSpan(alertCtx, span)
 	alertCtx = opentracing.ContextWithSpan(alertCtx, span)
 
 
-	job.Running = true
 	evalContext := NewEvalContext(alertCtx, job.Rule)
 	evalContext := NewEvalContext(alertCtx, job.Rule)
 	evalContext.Ctx = alertCtx
 	evalContext.Ctx = alertCtx
 
 
-	done := make(chan struct{})
 	go func() {
 	go func() {
 		defer func() {
 		defer func() {
 			if err := recover(); err != nil {
 			if err := recover(); err != nil {
@@ -122,43 +167,36 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
 					tlog.String("message", "failed to execute alert rule. panic was recovered."),
 					tlog.String("message", "failed to execute alert rule. panic was recovered."),
 				)
 				)
 				span.Finish()
 				span.Finish()
-				close(done)
+				close(attemptChan)
 			}
 			}
 		}()
 		}()
 
 
 		e.evalHandler.Eval(evalContext)
 		e.evalHandler.Eval(evalContext)
-		e.resultHandler.Handle(evalContext)
 
 
 		span.SetTag("alertId", evalContext.Rule.Id)
 		span.SetTag("alertId", evalContext.Rule.Id)
 		span.SetTag("dashboardId", evalContext.Rule.DashboardId)
 		span.SetTag("dashboardId", evalContext.Rule.DashboardId)
 		span.SetTag("firing", evalContext.Firing)
 		span.SetTag("firing", evalContext.Firing)
 		span.SetTag("nodatapoints", evalContext.NoDataFound)
 		span.SetTag("nodatapoints", evalContext.NoDataFound)
+		span.SetTag("attemptID", attemptID)
+
 		if evalContext.Error != nil {
 		if evalContext.Error != nil {
 			ext.Error.Set(span, true)
 			ext.Error.Set(span, true)
 			span.LogFields(
 			span.LogFields(
 				tlog.Error(evalContext.Error),
 				tlog.Error(evalContext.Error),
-				tlog.String("message", "alerting execution failed"),
+				tlog.String("message", "alerting execution attempt failed"),
 			)
 			)
+			if attemptID < alertMaxAttempts {
+				span.Finish()
+				e.log.Debug("Job Execution attempt triggered retry", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID)
+				attemptChan <- (attemptID + 1)
+				return
+			}
 		}
 		}
 
 
+		evalContext.Rule.State = evalContext.GetNewState()
+		e.resultHandler.Handle(evalContext)
 		span.Finish()
 		span.Finish()
-		close(done)
+		e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID)
+		close(attemptChan)
 	}()
 	}()
-
-	var err error = nil
-	select {
-	case <-grafanaCtx.Done():
-		select {
-		case <-time.After(unfinishedWorkTimeout):
-			cancelFn()
-			err = grafanaCtx.Err()
-		case <-done:
-		}
-	case <-done:
-	}
-
-	e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing)
-	job.Running = false
-	cancelFn()
-	return err
 }
 }

+ 118 - 0
pkg/services/alerting/engine_test.go

@@ -0,0 +1,118 @@
+package alerting
+
+import (
+	"context"
+	"errors"
+	"math"
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+type FakeEvalHandler struct {
+	SuccessCallID int // 0 means never sucess
+	CallNb        int
+}
+
+func NewFakeEvalHandler(successCallID int) *FakeEvalHandler {
+	return &FakeEvalHandler{
+		SuccessCallID: successCallID,
+		CallNb:        0,
+	}
+}
+
+func (handler *FakeEvalHandler) Eval(evalContext *EvalContext) {
+	handler.CallNb++
+	if handler.CallNb != handler.SuccessCallID {
+		evalContext.Error = errors.New("Fake evaluation failure")
+	}
+}
+
+type FakeResultHandler struct{}
+
+func (handler *FakeResultHandler) Handle(evalContext *EvalContext) error {
+	return nil
+}
+
+func TestEngineProcessJob(t *testing.T) {
+	Convey("Alerting engine job processing", t, func() {
+		engine := NewEngine()
+		engine.resultHandler = &FakeResultHandler{}
+		job := &Job{Running: true, Rule: &Rule{}}
+
+		Convey("Should trigger retry if needed", func() {
+
+			Convey("error + not last attempt -> retry", func() {
+				engine.evalHandler = NewFakeEvalHandler(0)
+
+				for i := 1; i < alertMaxAttempts; i++ {
+					attemptChan := make(chan int, 1)
+					cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
+
+					engine.processJob(i, attemptChan, cancelChan, job)
+					nextAttemptID, more := <-attemptChan
+
+					So(nextAttemptID, ShouldEqual, i+1)
+					So(more, ShouldEqual, true)
+					So(<-cancelChan, ShouldNotBeNil)
+				}
+			})
+
+			Convey("error + last attempt -> no retry", func() {
+				engine.evalHandler = NewFakeEvalHandler(0)
+				attemptChan := make(chan int, 1)
+				cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
+
+				engine.processJob(alertMaxAttempts, attemptChan, cancelChan, job)
+				nextAttemptID, more := <-attemptChan
+
+				So(nextAttemptID, ShouldEqual, 0)
+				So(more, ShouldEqual, false)
+				So(<-cancelChan, ShouldNotBeNil)
+			})
+
+			Convey("no error -> no retry", func() {
+				engine.evalHandler = NewFakeEvalHandler(1)
+				attemptChan := make(chan int, 1)
+				cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
+
+				engine.processJob(1, attemptChan, cancelChan, job)
+				nextAttemptID, more := <-attemptChan
+
+				So(nextAttemptID, ShouldEqual, 0)
+				So(more, ShouldEqual, false)
+				So(<-cancelChan, ShouldNotBeNil)
+			})
+		})
+
+		Convey("Should trigger as many retries as needed", func() {
+
+			Convey("never sucess -> max retries number", func() {
+				expectedAttempts := alertMaxAttempts
+				evalHandler := NewFakeEvalHandler(0)
+				engine.evalHandler = evalHandler
+
+				engine.processJobWithRetry(context.TODO(), job)
+				So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
+			})
+
+			Convey("always sucess -> never retry", func() {
+				expectedAttempts := 1
+				evalHandler := NewFakeEvalHandler(1)
+				engine.evalHandler = evalHandler
+
+				engine.processJobWithRetry(context.TODO(), job)
+				So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
+			})
+
+			Convey("some errors before sucess -> some retries", func() {
+				expectedAttempts := int(math.Ceil(float64(alertMaxAttempts) / 2))
+				evalHandler := NewFakeEvalHandler(expectedAttempts)
+				engine.evalHandler = evalHandler
+
+				engine.processJobWithRetry(context.TODO(), job)
+				So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
+			})
+		})
+	})
+}

+ 31 - 0
pkg/services/alerting/eval_context.go

@@ -112,3 +112,34 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
 		return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
 		return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
 	}
 	}
 }
 }
+
+func (c *EvalContext) GetNewState() m.AlertStateType {
+	if c.Error != nil {
+		c.log.Error("Alert Rule Result Error",
+			"ruleId", c.Rule.Id,
+			"name", c.Rule.Name,
+			"error", c.Error,
+			"changing state to", c.Rule.ExecutionErrorState.ToAlertState())
+
+		if c.Rule.ExecutionErrorState == m.ExecutionErrorKeepState {
+			return c.PrevAlertState
+		}
+		return c.Rule.ExecutionErrorState.ToAlertState()
+
+	} else if c.Firing {
+		return m.AlertStateAlerting
+
+	} else if c.NoDataFound {
+		c.log.Info("Alert Rule returned no data",
+			"ruleId", c.Rule.Id,
+			"name", c.Rule.Name,
+			"changing state to", c.Rule.NoDataState.ToAlertState())
+
+		if c.Rule.NoDataState == m.NoDataKeepState {
+			return c.PrevAlertState
+		}
+		return c.Rule.NoDataState.ToAlertState()
+	}
+
+	return m.AlertStateOK
+}

+ 68 - 1
pkg/services/alerting/eval_context_test.go

@@ -2,6 +2,7 @@ package alerting
 
 
 import (
 import (
 	"context"
 	"context"
+	"fmt"
 	"testing"
 	"testing"
 
 
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
@@ -12,7 +13,7 @@ func TestAlertingEvalContext(t *testing.T) {
 	Convey("Eval context", t, func() {
 	Convey("Eval context", t, func() {
 		ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
 		ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
 
 
-		Convey("Should update alert state", func() {
+		Convey("Should update alert state when needed", func() {
 
 
 			Convey("ok -> alerting", func() {
 			Convey("ok -> alerting", func() {
 				ctx.PrevAlertState = models.AlertStateOK
 				ctx.PrevAlertState = models.AlertStateOK
@@ -28,5 +29,71 @@ func TestAlertingEvalContext(t *testing.T) {
 				So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
 				So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
 			})
 			})
 		})
 		})
+
+		Convey("Should compute and replace properly new rule state", func() {
+			dummieError := fmt.Errorf("dummie error")
+
+			Convey("ok -> alerting", func() {
+				ctx.PrevAlertState = models.AlertStateOK
+				ctx.Firing = true
+
+				ctx.Rule.State = ctx.GetNewState()
+				So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
+			})
+
+			Convey("ok -> error(alerting)", func() {
+				ctx.PrevAlertState = models.AlertStateOK
+				ctx.Error = dummieError
+				ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
+
+				ctx.Rule.State = ctx.GetNewState()
+				So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
+			})
+
+			Convey("ok -> error(keep_last)", func() {
+				ctx.PrevAlertState = models.AlertStateOK
+				ctx.Error = dummieError
+				ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
+
+				ctx.Rule.State = ctx.GetNewState()
+				So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
+			})
+
+			Convey("pending -> error(keep_last)", func() {
+				ctx.PrevAlertState = models.AlertStatePending
+				ctx.Error = dummieError
+				ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
+
+				ctx.Rule.State = ctx.GetNewState()
+				So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
+			})
+
+			Convey("ok -> no_data(alerting)", func() {
+				ctx.PrevAlertState = models.AlertStateOK
+				ctx.Rule.NoDataState = models.NoDataSetAlerting
+				ctx.NoDataFound = true
+
+				ctx.Rule.State = ctx.GetNewState()
+				So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
+			})
+
+			Convey("ok -> no_data(keep_last)", func() {
+				ctx.PrevAlertState = models.AlertStateOK
+				ctx.Rule.NoDataState = models.NoDataKeepState
+				ctx.NoDataFound = true
+
+				ctx.Rule.State = ctx.GetNewState()
+				So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
+			})
+
+			Convey("pending -> no_data(keep_last)", func() {
+				ctx.PrevAlertState = models.AlertStatePending
+				ctx.Rule.NoDataState = models.NoDataKeepState
+				ctx.NoDataFound = true
+
+				ctx.Rule.State = ctx.GetNewState()
+				So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
+			})
+		})
 	})
 	})
 }
 }

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

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

+ 0 - 70
pkg/services/alerting/eval_handler_test.go

@@ -2,10 +2,8 @@ package alerting
 
 
 import (
 import (
 	"context"
 	"context"
-	"fmt"
 	"testing"
 	"testing"
 
 
-	"github.com/grafana/grafana/pkg/models"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
@@ -203,73 +201,5 @@ func TestAlertingEvaluationHandler(t *testing.T) {
 			handler.Eval(context)
 			handler.Eval(context)
 			So(context.NoDataFound, ShouldBeTrue)
 			So(context.NoDataFound, ShouldBeTrue)
 		})
 		})
-
-		Convey("EvalHandler can replace alert state based for errors and no_data", func() {
-			ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
-			dummieError := fmt.Errorf("dummie error")
-			Convey("Should update alert state", func() {
-
-				Convey("ok -> alerting", func() {
-					ctx.PrevAlertState = models.AlertStateOK
-					ctx.Firing = true
-
-					So(handler.getNewState(ctx), ShouldEqual, models.AlertStateAlerting)
-				})
-
-				Convey("ok -> error(alerting)", func() {
-					ctx.PrevAlertState = models.AlertStateOK
-					ctx.Error = dummieError
-					ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
-
-					ctx.Rule.State = handler.getNewState(ctx)
-					So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
-				})
-
-				Convey("ok -> error(keep_last)", func() {
-					ctx.PrevAlertState = models.AlertStateOK
-					ctx.Error = dummieError
-					ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
-
-					ctx.Rule.State = handler.getNewState(ctx)
-					So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
-				})
-
-				Convey("pending -> error(keep_last)", func() {
-					ctx.PrevAlertState = models.AlertStatePending
-					ctx.Error = dummieError
-					ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
-
-					ctx.Rule.State = handler.getNewState(ctx)
-					So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
-				})
-
-				Convey("ok -> no_data(alerting)", func() {
-					ctx.PrevAlertState = models.AlertStateOK
-					ctx.Rule.NoDataState = models.NoDataSetAlerting
-					ctx.NoDataFound = true
-
-					ctx.Rule.State = handler.getNewState(ctx)
-					So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
-				})
-
-				Convey("ok -> no_data(keep_last)", func() {
-					ctx.PrevAlertState = models.AlertStateOK
-					ctx.Rule.NoDataState = models.NoDataKeepState
-					ctx.NoDataFound = true
-
-					ctx.Rule.State = handler.getNewState(ctx)
-					So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
-				})
-
-				Convey("pending -> no_data(keep_last)", func() {
-					ctx.PrevAlertState = models.AlertStatePending
-					ctx.Rule.NoDataState = models.NoDataKeepState
-					ctx.NoDataFound = true
-
-					ctx.Rule.State = handler.getNewState(ctx)
-					So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
-				})
-			})
-		})
 	})
 	})
 }
 }

+ 1 - 0
pkg/services/alerting/test_rule.go

@@ -53,6 +53,7 @@ func testAlertRule(rule *Rule) *EvalContext {
 	context.IsTestRun = true
 	context.IsTestRun = true
 
 
 	handler.Eval(context)
 	handler.Eval(context)
+	context.Rule.State = context.GetNewState()
 
 
 	return context
 	return context
 }
 }

+ 4 - 4
pkg/services/dashboards/folder_service.go

@@ -10,8 +10,8 @@ import (
 // FolderService service for operating on folders
 // FolderService service for operating on folders
 type FolderService interface {
 type FolderService interface {
 	GetFolders(limit int) ([]*models.Folder, error)
 	GetFolders(limit int) ([]*models.Folder, error)
-	GetFolderById(id int64) (*models.Folder, error)
-	GetFolderByUid(uid string) (*models.Folder, error)
+	GetFolderByID(id int64) (*models.Folder, error)
+	GetFolderByUID(uid string) (*models.Folder, error)
 	CreateFolder(cmd *models.CreateFolderCommand) error
 	CreateFolder(cmd *models.CreateFolderCommand) error
 	UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error
 	UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error
 	DeleteFolder(uid string) (*models.Folder, error)
 	DeleteFolder(uid string) (*models.Folder, error)
@@ -57,7 +57,7 @@ func (dr *dashboardServiceImpl) GetFolders(limit int) ([]*models.Folder, error)
 	return folders, nil
 	return folders, nil
 }
 }
 
 
-func (dr *dashboardServiceImpl) GetFolderById(id int64) (*models.Folder, error) {
+func (dr *dashboardServiceImpl) GetFolderByID(id int64) (*models.Folder, error) {
 	query := models.GetDashboardQuery{OrgId: dr.orgId, Id: id}
 	query := models.GetDashboardQuery{OrgId: dr.orgId, Id: id}
 	dashFolder, err := getFolder(query)
 	dashFolder, err := getFolder(query)
 
 
@@ -76,7 +76,7 @@ func (dr *dashboardServiceImpl) GetFolderById(id int64) (*models.Folder, error)
 	return dashToFolder(dashFolder), nil
 	return dashToFolder(dashFolder), nil
 }
 }
 
 
-func (dr *dashboardServiceImpl) GetFolderByUid(uid string) (*models.Folder, error) {
+func (dr *dashboardServiceImpl) GetFolderByUID(uid string) (*models.Folder, error) {
 	query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid}
 	query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid}
 	dashFolder, err := getFolder(query)
 	dashFolder, err := getFolder(query)
 
 

+ 4 - 4
pkg/services/dashboards/folder_service_test.go

@@ -36,13 +36,13 @@ func TestFolderService(t *testing.T) {
 			})
 			})
 
 
 			Convey("When get folder by id should return access denied error", func() {
 			Convey("When get folder by id should return access denied error", func() {
-				_, err := service.GetFolderById(1)
+				_, err := service.GetFolderByID(1)
 				So(err, ShouldNotBeNil)
 				So(err, ShouldNotBeNil)
 				So(err, ShouldEqual, models.ErrFolderAccessDenied)
 				So(err, ShouldEqual, models.ErrFolderAccessDenied)
 			})
 			})
 
 
 			Convey("When get folder by uid should return access denied error", func() {
 			Convey("When get folder by uid should return access denied error", func() {
-				_, err := service.GetFolderByUid("uid")
+				_, err := service.GetFolderByUID("uid")
 				So(err, ShouldNotBeNil)
 				So(err, ShouldNotBeNil)
 				So(err, ShouldEqual, models.ErrFolderAccessDenied)
 				So(err, ShouldEqual, models.ErrFolderAccessDenied)
 			})
 			})
@@ -147,14 +147,14 @@ func TestFolderService(t *testing.T) {
 			})
 			})
 
 
 			Convey("When get folder by id should return folder", func() {
 			Convey("When get folder by id should return folder", func() {
-				f, _ := service.GetFolderById(1)
+				f, _ := service.GetFolderByID(1)
 				So(f.Id, ShouldEqual, dashFolder.Id)
 				So(f.Id, ShouldEqual, dashFolder.Id)
 				So(f.Uid, ShouldEqual, dashFolder.Uid)
 				So(f.Uid, ShouldEqual, dashFolder.Uid)
 				So(f.Title, ShouldEqual, dashFolder.Title)
 				So(f.Title, ShouldEqual, dashFolder.Title)
 			})
 			})
 
 
 			Convey("When get folder by uid should return folder", func() {
 			Convey("When get folder by uid should return folder", func() {
-				f, _ := service.GetFolderByUid("uid")
+				f, _ := service.GetFolderByUID("uid")
 				So(f.Id, ShouldEqual, dashFolder.Id)
 				So(f.Id, ShouldEqual, dashFolder.Id)
 				So(f.Uid, ShouldEqual, dashFolder.Uid)
 				So(f.Uid, ShouldEqual, dashFolder.Uid)
 				So(f.Title, ShouldEqual, dashFolder.Title)
 				So(f.Title, ShouldEqual, dashFolder.Title)

+ 26 - 3
vendor/github.com/go-macaron/session/mysql/mysql.go → pkg/services/session/mysql.go

@@ -108,6 +108,7 @@ func (p *MysqlProvider) Init(expire int64, connStr string) (err error) {
 	p.expire = expire
 	p.expire = expire
 
 
 	p.c, err = sql.Open("mysql", connStr)
 	p.c, err = sql.Open("mysql", connStr)
+	p.c.SetConnMaxLifetime(time.Second * time.Duration(sessionConnMaxLifetime))
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -141,12 +142,29 @@ func (p *MysqlProvider) Read(sid string) (session.RawStore, error) {
 
 
 // Exist returns true if session with given ID exists.
 // Exist returns true if session with given ID exists.
 func (p *MysqlProvider) Exist(sid string) bool {
 func (p *MysqlProvider) Exist(sid string) bool {
+	exists, err := p.queryExists(sid)
+
+	if err != nil {
+		exists, err = p.queryExists(sid)
+	}
+
+	if err != nil {
+		log.Printf("session/mysql: error checking if session exists: %v", err)
+		return false
+	}
+
+	return exists
+}
+
+func (p *MysqlProvider) queryExists(sid string) (bool, error) {
 	var data []byte
 	var data []byte
 	err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data)
 	err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data)
+
 	if err != nil && err != sql.ErrNoRows {
 	if err != nil && err != sql.ErrNoRows {
-		panic("session/mysql: error checking existence: " + err.Error())
+		return false, err
 	}
 	}
-	return err != sql.ErrNoRows
+
+	return err != sql.ErrNoRows, nil
 }
 }
 
 
 // Destory deletes a session by session ID.
 // Destory deletes a session by session ID.
@@ -185,7 +203,12 @@ func (p *MysqlProvider) Count() (total int) {
 
 
 // GC calls GC to clean expired sessions.
 // GC calls GC to clean expired sessions.
 func (p *MysqlProvider) GC() {
 func (p *MysqlProvider) GC() {
-	if _, err := p.c.Exec("DELETE FROM session WHERE  expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire); err != nil {
+	var err error
+	if _, err = p.c.Exec("DELETE FROM session WHERE  expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire); err != nil {
+		_, err = p.c.Exec("DELETE FROM session WHERE  expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire)
+	}
+
+	if err != nil {
 		log.Printf("session/mysql: error garbage collecting: %v", err)
 		log.Printf("session/mysql: error garbage collecting: %v", err)
 	}
 	}
 }
 }

+ 3 - 2
pkg/services/session/session.go

@@ -6,7 +6,6 @@ import (
 
 
 	ms "github.com/go-macaron/session"
 	ms "github.com/go-macaron/session"
 	_ "github.com/go-macaron/session/memcache"
 	_ "github.com/go-macaron/session/memcache"
-	_ "github.com/go-macaron/session/mysql"
 	_ "github.com/go-macaron/session/postgres"
 	_ "github.com/go-macaron/session/postgres"
 	_ "github.com/go-macaron/session/redis"
 	_ "github.com/go-macaron/session/redis"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
@@ -25,6 +24,7 @@ var sessionOptions *ms.Options
 var StartSessionGC func()
 var StartSessionGC func()
 var GetSessionCount func() int
 var GetSessionCount func() int
 var sessionLogger = log.New("session")
 var sessionLogger = log.New("session")
+var sessionConnMaxLifetime int64
 
 
 func init() {
 func init() {
 	StartSessionGC = func() {
 	StartSessionGC = func() {
@@ -37,9 +37,10 @@ func init() {
 	}
 	}
 }
 }
 
 
-func Init(options *ms.Options) {
+func Init(options *ms.Options, connMaxLifetime int64) {
 	var err error
 	var err error
 	sessionOptions = prepareOptions(options)
 	sessionOptions = prepareOptions(options)
+	sessionConnMaxLifetime = connMaxLifetime
 	sessionManager, err = ms.NewManager(options.Provider, *options)
 	sessionManager, err = ms.NewManager(options.Provider, *options)
 	if err != nil {
 	if err != nil {
 		panic(err)
 		panic(err)

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

@@ -255,7 +255,7 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
 		}
 		}
 
 
 		alert.State = cmd.State
 		alert.State = cmd.State
-		alert.StateChanges += 1
+		alert.StateChanges++
 		alert.NewStateDate = timeNow()
 		alert.NewStateDate = timeNow()
 		alert.EvalData = cmd.EvalData
 		alert.EvalData = cmd.EvalData
 
 

+ 1 - 0
pkg/services/sqlstore/dashboard_folder_test.go

@@ -46,6 +46,7 @@ func TestDashboardFolderDataAccess(t *testing.T) {
 						OrgId:        1, DashboardIds: []int64{folder.Id, dashInRoot.Id},
 						OrgId:        1, DashboardIds: []int64{folder.Id, dashInRoot.Id},
 					}
 					}
 					err := SearchDashboards(query)
 					err := SearchDashboards(query)
+
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 					So(len(query.Result), ShouldEqual, 1)
 					So(len(query.Result), ShouldEqual, 1)
 					So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)
 					So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)

+ 22 - 44
pkg/services/sqlstore/dashboard_version.go

@@ -67,30 +67,39 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error {
 	return nil
 	return nil
 }
 }
 
 
+const MAX_VERSIONS_TO_DELETE = 100
+
 func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error {
 func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
-		versions := []DashboardVersionExp{}
 		versionsToKeep := setting.DashboardVersionsToKeep
 		versionsToKeep := setting.DashboardVersionsToKeep
-
 		if versionsToKeep < 1 {
 		if versionsToKeep < 1 {
 			versionsToKeep = 1
 			versionsToKeep = 1
 		}
 		}
 
 
-		err := sess.Table("dashboard_version").
-			Select("dashboard_version.id, dashboard_version.version, dashboard_version.dashboard_id").
-			Where(`dashboard_id IN (
-			SELECT dashboard_id FROM dashboard_version
-			GROUP BY dashboard_id HAVING COUNT(dashboard_version.id) > ?
-		)`, versionsToKeep).
-			Desc("dashboard_version.dashboard_id", "dashboard_version.version").
-			Find(&versions)
-
+		// Idea of this query is finding version IDs to delete based on formula:
+		// min_version_to_keep = min_version + (versions_count - versions_to_keep)
+		// where version stats is processed for each dashboard. This guarantees that we keep at least versions_to_keep
+		// versions, but in some cases (when versions are sparse) this number may be more.
+		versionIdsToDeleteQuery := `SELECT id
+			FROM dashboard_version, (
+				SELECT dashboard_id, count(version) as count, min(version) as min
+				FROM dashboard_version
+				GROUP BY dashboard_id
+			) AS vtd
+			WHERE dashboard_version.dashboard_id=vtd.dashboard_id
+			AND version < vtd.min + vtd.count - ?`
+
+		var versionIdsToDelete []interface{}
+		err := sess.SQL(versionIdsToDeleteQuery, versionsToKeep).Find(&versionIdsToDelete)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		// Keep last versionsToKeep versions and delete other
-		versionIdsToDelete := getVersionIDsToDelete(versions, versionsToKeep)
+		// Don't delete more than MAX_VERSIONS_TO_DELETE version per time
+		if len(versionIdsToDelete) > MAX_VERSIONS_TO_DELETE {
+			versionIdsToDelete = versionIdsToDelete[:MAX_VERSIONS_TO_DELETE]
+		}
+
 		if len(versionIdsToDelete) > 0 {
 		if len(versionIdsToDelete) > 0 {
 			deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)`
 			deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)`
 			expiredResponse, err := sess.Exec(deleteExpiredSql, versionIdsToDelete...)
 			expiredResponse, err := sess.Exec(deleteExpiredSql, versionIdsToDelete...)
@@ -103,34 +112,3 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error {
 		return nil
 		return nil
 	})
 	})
 }
 }
-
-// Short version of DashboardVersion for getting expired versions
-type DashboardVersionExp struct {
-	Id          int64 `json:"id"`
-	DashboardId int64 `json:"dashboardId"`
-	Version     int   `json:"version"`
-}
-
-func getVersionIDsToDelete(versions []DashboardVersionExp, versionsToKeep int) []interface{} {
-	versionIds := make([]interface{}, 0)
-
-	if len(versions) == 0 {
-		return versionIds
-	}
-
-	currentDashboard := versions[0].DashboardId
-	count := 0
-	for _, v := range versions {
-		if v.DashboardId == currentDashboard {
-			count++
-		} else {
-			count = 1
-			currentDashboard = v.DashboardId
-		}
-		if count > versionsToKeep {
-			versionIds = append(versionIds, v.Id)
-		}
-	}
-
-	return versionIds
-}

+ 21 - 1
pkg/services/sqlstore/dashboard_version_test.go

@@ -136,10 +136,30 @@ func TestDeleteExpiredVersions(t *testing.T) {
 			err := DeleteExpiredVersions(&m.DeleteExpiredVersionsCommand{})
 			err := DeleteExpiredVersions(&m.DeleteExpiredVersionsCommand{})
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}
+			query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWrite}
 			GetDashboardVersions(&query)
 			GetDashboardVersions(&query)
 
 
 			So(len(query.Result), ShouldEqual, versionsToWrite)
 			So(len(query.Result), ShouldEqual, versionsToWrite)
 		})
 		})
+
+		Convey("Don't delete more than MAX_VERSIONS_TO_DELETE per iteration", func() {
+			versionsToWriteBigNumber := MAX_VERSIONS_TO_DELETE + versionsToWrite
+			for i := 0; i < versionsToWriteBigNumber-versionsToWrite; i++ {
+				updateTestDashboard(savedDash, map[string]interface{}{
+					"tags": "different-tag",
+				})
+			}
+
+			err := DeleteExpiredVersions(&m.DeleteExpiredVersionsCommand{})
+			So(err, ShouldBeNil)
+
+			query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWriteBigNumber}
+			GetDashboardVersions(&query)
+
+			// Ensure we have at least versionsToKeep versions
+			So(len(query.Result), ShouldBeGreaterThanOrEqualTo, versionsToKeep)
+			// Ensure we haven't deleted more than MAX_VERSIONS_TO_DELETE rows
+			So(versionsToWriteBigNumber-len(query.Result), ShouldBeLessThanOrEqualTo, MAX_VERSIONS_TO_DELETE)
+		})
 	})
 	})
 }
 }

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

@@ -125,7 +125,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
 	condition := m.GetCondition()
 	condition := m.GetCondition()
 	if condition != nil {
 	if condition != nil {
 		sql, args := condition.Sql(mg.dialect)
 		sql, args := condition.Sql(mg.dialect)
-		results, err := sess.Query(sql, args...)
+		results, err := sess.SQL(sql).Query(args...)
 		if err != nil || len(results) == 0 {
 		if err != nil || len(results) == 0 {
 			mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
 			mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
 			return sess.Rollback()
 			return sess.Rollback()

+ 3 - 0
pkg/services/sqlstore/org_test.go

@@ -2,6 +2,7 @@ package sqlstore
 
 
 import (
 import (
 	"testing"
 	"testing"
+	"time"
 
 
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 
 
@@ -241,6 +242,8 @@ func TestAccountDataAccess(t *testing.T) {
 func testHelperUpdateDashboardAcl(dashboardId int64, items ...m.DashboardAcl) error {
 func testHelperUpdateDashboardAcl(dashboardId int64, items ...m.DashboardAcl) error {
 	cmd := m.UpdateDashboardAclCommand{DashboardId: dashboardId}
 	cmd := m.UpdateDashboardAclCommand{DashboardId: dashboardId}
 	for _, item := range items {
 	for _, item := range items {
+		item.Created = time.Now()
+		item.Updated = time.Now()
 		cmd.Items = append(cmd.Items, &item)
 		cmd.Items = append(cmd.Items, &item)
 	}
 	}
 	return UpdateDashboardAcl(&cmd)
 	return UpdateDashboardAcl(&cmd)

+ 9 - 4
pkg/services/sqlstore/quota.go

@@ -2,6 +2,7 @@ package sqlstore
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"time"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
@@ -98,8 +99,9 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
 		//Check if quota is already defined in the DB
 		//Check if quota is already defined in the DB
 		quota := m.Quota{
 		quota := m.Quota{
-			Target: cmd.Target,
-			OrgId:  cmd.OrgId,
+			Target:  cmd.Target,
+			OrgId:   cmd.OrgId,
+			Updated: time.Now(),
 		}
 		}
 		has, err := sess.Get(&quota)
 		has, err := sess.Get(&quota)
 		if err != nil {
 		if err != nil {
@@ -107,6 +109,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
 		}
 		}
 		quota.Limit = cmd.Limit
 		quota.Limit = cmd.Limit
 		if has == false {
 		if has == false {
+			quota.Created = time.Now()
 			//No quota in the DB for this target, so create a new one.
 			//No quota in the DB for this target, so create a new one.
 			if _, err := sess.Insert(&quota); err != nil {
 			if _, err := sess.Insert(&quota); err != nil {
 				return err
 				return err
@@ -198,8 +201,9 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
 		//Check if quota is already defined in the DB
 		//Check if quota is already defined in the DB
 		quota := m.Quota{
 		quota := m.Quota{
-			Target: cmd.Target,
-			UserId: cmd.UserId,
+			Target:  cmd.Target,
+			UserId:  cmd.UserId,
+			Updated: time.Now(),
 		}
 		}
 		has, err := sess.Get(&quota)
 		has, err := sess.Get(&quota)
 		if err != nil {
 		if err != nil {
@@ -207,6 +211,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
 		}
 		}
 		quota.Limit = cmd.Limit
 		quota.Limit = cmd.Limit
 		if has == false {
 		if has == false {
+			quota.Created = time.Now()
 			//No quota in the DB for this target, so create a new one.
 			//No quota in the DB for this target, so create a new one.
 			if _, err := sess.Insert(&quota); err != nil {
 			if _, err := sess.Insert(&quota); err != nil {
 				return err
 				return err

+ 2 - 2
pkg/services/sqlstore/quota_test.go

@@ -104,12 +104,12 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
 			})
 			})
 		})
 		})
 		Convey("Given saved user quota for org", func() {
 		Convey("Given saved user quota for org", func() {
-			userQoutaCmd := m.UpdateUserQuotaCmd{
+			userQuotaCmd := m.UpdateUserQuotaCmd{
 				UserId: userId,
 				UserId: userId,
 				Target: "org_user",
 				Target: "org_user",
 				Limit:  10,
 				Limit:  10,
 			}
 			}
-			err := UpdateUserQuota(&userQoutaCmd)
+			err := UpdateUserQuota(&userQuotaCmd)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
 			Convey("Should be able to get saved quota by user id and target", func() {
 			Convey("Should be able to get saved quota by user id and target", func() {

+ 20 - 12
pkg/services/sqlstore/sqlstore.go

@@ -8,6 +8,7 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
 	"testing"
 	"testing"
+	"time"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
@@ -35,6 +36,7 @@ type DatabaseConfig struct {
 	ServerCertName                             string
 	ServerCertName                             string
 	MaxOpenConn                                int
 	MaxOpenConn                                int
 	MaxIdleConn                                int
 	MaxIdleConn                                int
+	ConnMaxLifetime                            int
 }
 }
 
 
 var (
 var (
@@ -159,18 +161,20 @@ func getEngine() (*xorm.Engine, error) {
 	engine, err := xorm.NewEngine(DbCfg.Type, cnnstr)
 	engine, err := xorm.NewEngine(DbCfg.Type, cnnstr)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
+	}
+
+	engine.SetMaxOpenConns(DbCfg.MaxOpenConn)
+	engine.SetMaxIdleConns(DbCfg.MaxIdleConn)
+	engine.SetConnMaxLifetime(time.Second * time.Duration(DbCfg.ConnMaxLifetime))
+	debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false)
+	if !debugSql {
+		engine.SetLogger(&xorm.DiscardLogger{})
 	} else {
 	} else {
-		engine.SetMaxOpenConns(DbCfg.MaxOpenConn)
-		engine.SetMaxIdleConns(DbCfg.MaxIdleConn)
-		debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false)
-		if !debugSql {
-			engine.SetLogger(&xorm.DiscardLogger{})
-		} else {
-			engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm")))
-			engine.ShowSQL(true)
-			engine.ShowExecTime(true)
-		}
+		engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm")))
+		engine.ShowSQL(true)
+		engine.ShowExecTime(true)
 	}
 	}
+
 	return engine, nil
 	return engine, nil
 }
 }
 
 
@@ -204,6 +208,7 @@ func LoadConfig() {
 	}
 	}
 	DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0)
 	DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0)
 	DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0)
 	DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0)
+	DbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400)
 
 
 	if DbCfg.Type == "sqlite3" {
 	if DbCfg.Type == "sqlite3" {
 		UseSQLite3 = true
 		UseSQLite3 = true
@@ -227,8 +232,8 @@ var (
 
 
 func InitTestDB(t *testing.T) *xorm.Engine {
 func InitTestDB(t *testing.T) *xorm.Engine {
 	selectedDb := dbSqlite
 	selectedDb := dbSqlite
-	//selectedDb := dbMySql
-	//selectedDb := dbPostgres
+	// selectedDb := dbMySql
+	// selectedDb := dbPostgres
 
 
 	var x *xorm.Engine
 	var x *xorm.Engine
 	var err error
 	var err error
@@ -247,6 +252,9 @@ func InitTestDB(t *testing.T) *xorm.Engine {
 		x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
 		x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
 	}
 	}
 
 
+	x.DatabaseTZ = time.UTC
+	x.TZLocation = time.UTC
+
 	// x.ShowSQL()
 	// x.ShowSQL()
 
 
 	if err != nil {
 	if err != nil {

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

@@ -315,6 +315,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
 	}
 	}
 
 
 	query.Result = m.UserProfileDTO{
 	query.Result = m.UserProfileDTO{
+		Id:             user.Id,
 		Name:           user.Name,
 		Name:           user.Name,
 		Email:          user.Email,
 		Email:          user.Email,
 		Login:          user.Login,
 		Login:          user.Login,

+ 4 - 1
pkg/setting/setting.go

@@ -131,7 +131,8 @@ var (
 	PluginAppsSkipVerifyTLS bool
 	PluginAppsSkipVerifyTLS bool
 
 
 	// Session settings.
 	// Session settings.
-	SessionOptions session.Options
+	SessionOptions         session.Options
+	SessionConnMaxLifetime int64
 
 
 	// Global setting objects.
 	// Global setting objects.
 	Cfg          *ini.File
 	Cfg          *ini.File
@@ -634,6 +635,8 @@ func readSessionConfig() {
 	if SessionOptions.CookiePath == "" {
 	if SessionOptions.CookiePath == "" {
 		SessionOptions.CookiePath = "/"
 		SessionOptions.CookiePath = "/"
 	}
 	}
+
+	SessionConnMaxLifetime = Cfg.Section("session").Key("conn_max_lifetime").MustInt64(14400)
 }
 }
 
 
 func initLogging() {
 func initLogging() {

+ 5 - 10
pkg/tsdb/mssql/macros.go

@@ -73,25 +73,20 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
 		}
 		return fmt.Sprintf("%s AS time", args[0]), nil
 		return fmt.Sprintf("%s AS time", args[0]), nil
-	case "__utcTime":
-		if len(args) == 0 {
-			return "", fmt.Errorf("missing time column argument for macro %v", name)
-		}
-		return fmt.Sprintf("DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s) AS time", args[0]), nil
 	case "__timeEpoch":
 	case "__timeEpoch":
 		if len(args) == 0 {
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
 		}
-		return fmt.Sprintf("DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s) ) AS time", args[0]), nil
+		return fmt.Sprintf("DATEDIFF(second, '1970-01-01', %s) AS time", args[0]), nil
 	case "__timeFilter":
 	case "__timeFilter":
 		if len(args) == 0 {
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
 		}
-		return fmt.Sprintf("%s >= DATEADD(s, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND %s <= DATEADD(s, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+		return fmt.Sprintf("%s >= DATEADD(s, %d, '1970-01-01') AND %s <= DATEADD(s, %d, '1970-01-01')", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
 	case "__timeFrom":
 	case "__timeFrom":
-		return fmt.Sprintf("DATEADD(second, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
+		return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
 	case "__timeTo":
 	case "__timeTo":
-		return fmt.Sprintf("DATEADD(second, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+		return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
 	case "__timeGroup":
 	case "__timeGroup":
 		if len(args) < 2 {
 		if len(args) < 2 {
 			return "", fmt.Errorf("macro %v needs time column and interval", name)
 			return "", fmt.Errorf("macro %v needs time column and interval", name)
@@ -113,7 +108,7 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 				m.Query.Model.Set("fillValue", floatVal)
 				m.Query.Model.Set("fillValue", floatVal)
 			}
 			}
 		}
 		}
-		return fmt.Sprintf("cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s))/%.0f as int)*%.0f as int)", args[0], interval.Seconds(), interval.Seconds()), nil
+		return fmt.Sprintf("CAST(ROUND(DATEDIFF(second, '1970-01-01', %s)/%.1f, 0) as bigint)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil
 	case "__unixEpochFilter":
 	case "__unixEpochFilter":
 		if len(args) == 0 {
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 			return "", fmt.Errorf("missing time column argument for macro %v", name)

+ 9 - 16
pkg/tsdb/mssql/macros_test.go

@@ -25,46 +25,39 @@ func TestMacroEngine(t *testing.T) {
 			So(sql, ShouldEqual, "select time_column AS time")
 			So(sql, ShouldEqual, "select time_column AS time")
 		})
 		})
 
 
-		Convey("interpolate __utcTime function", func() {
-			sql, err := engine.Interpolate(query, nil, "select $__utcTime(time_column)")
-			So(err, ShouldBeNil)
-
-			So(sql, ShouldEqual, "select DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) AS time")
-		})
-
 		Convey("interpolate __timeEpoch function", func() {
 		Convey("interpolate __timeEpoch function", func() {
 			sql, err := engine.Interpolate(query, nil, "select $__timeEpoch(time_column)")
 			sql, err := engine.Interpolate(query, nil, "select $__timeEpoch(time_column)")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			So(sql, ShouldEqual, "select DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) AS time")
+			So(sql, ShouldEqual, "select DATEDIFF(second, '1970-01-01', time_column) AS time")
 		})
 		})
 
 
 		Convey("interpolate __timeEpoch function wrapped in aggregation", func() {
 		Convey("interpolate __timeEpoch function wrapped in aggregation", func() {
 			sql, err := engine.Interpolate(query, nil, "select min($__timeEpoch(time_column))")
 			sql, err := engine.Interpolate(query, nil, "select min($__timeEpoch(time_column))")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			So(sql, ShouldEqual, "select min(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) AS time)")
+			So(sql, ShouldEqual, "select min(DATEDIFF(second, '1970-01-01', time_column) AS time)")
 		})
 		})
 
 
 		Convey("interpolate __timeFilter function", func() {
 		Convey("interpolate __timeFilter function", func() {
 			sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 			sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			So(sql, ShouldEqual, "WHERE time_column >= DATEADD(s, 18446744066914186738+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND time_column <= DATEADD(s, 18446744066914187038+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')")
+			So(sql, ShouldEqual, "WHERE time_column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND time_column <= DATEADD(s, 18446744066914187038, '1970-01-01')")
 		})
 		})
 
 
 		Convey("interpolate __timeGroup function", func() {
 		Convey("interpolate __timeGroup function", func() {
 			sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
 			sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			So(sql, ShouldEqual, "GROUP BY cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column))/300 as int)*300 as int)")
+			So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
 		})
 		})
 
 
 		Convey("interpolate __timeGroup function with spaces around arguments", func() {
 		Convey("interpolate __timeGroup function with spaces around arguments", func() {
 			sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
 			sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			So(sql, ShouldEqual, "GROUP BY cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column))/300 as int)*300 as int)")
+			So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
 		})
 		})
 
 
 		Convey("interpolate __timeGroup function with fill (value = NULL)", func() {
 		Convey("interpolate __timeGroup function with fill (value = NULL)", func() {
@@ -97,21 +90,21 @@ func TestMacroEngine(t *testing.T) {
 			sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
 			sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			So(sql, ShouldEqual, "select DATEADD(second, 18446744066914186738+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')")
+			So(sql, ShouldEqual, "select DATEADD(second, 18446744066914186738, '1970-01-01')")
 		})
 		})
 
 
 		Convey("interpolate __timeTo function", func() {
 		Convey("interpolate __timeTo function", func() {
 			sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
 			sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			So(sql, ShouldEqual, "select DATEADD(second, 18446744066914187038+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')")
+			So(sql, ShouldEqual, "select DATEADD(second, 18446744066914187038, '1970-01-01')")
 		})
 		})
 
 
 		Convey("interpolate __unixEpochFilter function", func() {
 		Convey("interpolate __unixEpochFilter function", func() {
-			sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(18446744066914186738)")
+			sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			So(sql, ShouldEqual, "select 18446744066914186738 >= 18446744066914186738 AND 18446744066914186738 <= 18446744066914187038")
+			So(sql, ShouldEqual, "select time_column >= 18446744066914186738 AND time_column <= 18446744066914187038")
 		})
 		})
 
 
 		Convey("interpolate __unixEpochFrom function", func() {
 		Convey("interpolate __unixEpochFrom function", func() {

+ 4 - 9
pkg/tsdb/mssql/mssql.go

@@ -119,15 +119,10 @@ func (e MssqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows,
 			return err
 			return err
 		}
 		}
 
 
-		// convert column named time to unix timestamp to make
-		// native datetime mssql types work in annotation queries
-		if timeIndex != -1 {
-			switch value := values[timeIndex].(type) {
-			case time.Time:
-				values[timeIndex] = float64(value.Unix())
-			}
-		}
-
+		// converts column named time to unix timestamp in milliseconds
+		// to make native mssql datetime types and epoch dates work in
+		// annotation and table queries.
+		tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
 		table.Rows = append(table.Rows, values)
 		table.Rows = append(table.Rows, values)
 	}
 	}
 
 

+ 388 - 198
pkg/tsdb/mssql/mssql_test.go

@@ -19,6 +19,8 @@ import (
 // and set up a MSSQL db named grafanatest and a user/password grafana/Password!
 // and set up a MSSQL db named grafanatest and a user/password grafana/Password!
 // Use the docker/blocks/mssql_tests/docker-compose.yaml to spin up a
 // Use the docker/blocks/mssql_tests/docker-compose.yaml to spin up a
 // preconfigured MSSQL server suitable for running these tests.
 // preconfigured MSSQL server suitable for running these tests.
+// Thers's also a dashboard.json in same directory that you can import to Grafana
+// once you've created a datasource for the test server/database.
 // If needed, change the variable below to the IP address of the database.
 // If needed, change the variable below to the IP address of the database.
 var serverIP string = "localhost"
 var serverIP string = "localhost"
 
 
@@ -37,44 +39,44 @@ func TestMSSQL(t *testing.T) {
 		sess := x.NewSession()
 		sess := x.NewSession()
 		defer sess.Close()
 		defer sess.Close()
 
 
-		fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC)
+		fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
 
 
 		Convey("Given a table with different native data types", func() {
 		Convey("Given a table with different native data types", func() {
 			sql := `
 			sql := `
-				IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL
-					DROP TABLE dbo.[mssql_types]
-
-				CREATE TABLE [mssql_types] (
-					c_bit bit,
-
-					c_tinyint tinyint,
-					c_smallint smallint,
-					c_int int,
-					c_bigint bigint,
-
-					c_money money,
-					c_smallmoney smallmoney,
-					c_numeric numeric(10,5),
-					c_real real,
-					c_decimal decimal(10,2),
-					c_float float,
-
-					c_char char(10),
-					c_varchar varchar(10),
-					c_text text,
-
-					c_nchar nchar(12),
-					c_nvarchar nvarchar(12),
-					c_ntext ntext,
-
-					c_datetime datetime,
-					c_datetime2 datetime2,
-					c_smalldatetime smalldatetime,
-					c_date date,
-					c_time time,
-					c_datetimeoffset datetimeoffset
-				)
-			`
+					IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL
+						DROP TABLE dbo.[mssql_types]
+
+					CREATE TABLE [mssql_types] (
+						c_bit bit,
+
+						c_tinyint tinyint,
+						c_smallint smallint,
+						c_int int,
+						c_bigint bigint,
+
+						c_money money,
+						c_smallmoney smallmoney,
+						c_numeric numeric(10,5),
+						c_real real,
+						c_decimal decimal(10,2),
+						c_float float,
+
+						c_char char(10),
+						c_varchar varchar(10),
+						c_text text,
+
+						c_nchar nchar(12),
+						c_nvarchar nvarchar(12),
+						c_ntext ntext,
+
+						c_datetime datetime,
+						c_datetime2 datetime2,
+						c_smalldatetime smalldatetime,
+						c_date date,
+						c_time time,
+						c_datetimeoffset datetimeoffset
+					)
+				`
 
 
 			_, err := sess.Exec(sql)
 			_, err := sess.Exec(sql)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
@@ -87,14 +89,14 @@ func TestMSSQL(t *testing.T) {
 			d2 := dt2.Format(dt2Format)
 			d2 := dt2.Format(dt2Format)
 
 
 			sql = fmt.Sprintf(`
 			sql = fmt.Sprintf(`
-			INSERT INTO [mssql_types]
-			SELECT
-        1, 5, 20020, 980300, 1420070400, '$20000.15', '£2.15', 12345.12,
-        1.11, 2.22, 3.33,
-				'char10', 'varchar10', 'text',
-				N'☺nchar12☺', N'☺nvarchar12☺', N'☺text☺',
-				CAST('%s' AS DATETIME), CAST('%s' AS DATETIME2), CAST('%s' AS SMALLDATETIME), CAST('%s' AS DATE), CAST('%s' AS TIME), SWITCHOFFSET(CAST('%s' AS DATETIMEOFFSET), '-07:00')
-    `, d, d2, d, d, d, d2)
+				INSERT INTO [mssql_types]
+				SELECT
+		    1, 5, 20020, 980300, 1420070400, '$20000.15', '£2.15', 12345.12,
+		    1.11, 2.22, 3.33,
+					'char10', 'varchar10', 'text',
+					N'☺nchar12☺', N'☺nvarchar12☺', N'☺text☺',
+					CAST('%s' AS DATETIME), CAST('%s' AS DATETIME2), CAST('%s' AS SMALLDATETIME), CAST('%s' AS DATE), CAST('%s' AS TIME), SWITCHOFFSET(CAST('%s' AS DATETIMEOFFSET), '-07:00')
+		`, d, d2, d, d, d, d2)
 
 
 			_, err = sess.Exec(sql)
 			_, err = sess.Exec(sql)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
@@ -151,14 +153,14 @@ func TestMSSQL(t *testing.T) {
 
 
 		Convey("Given a table with metrics that lacks data for some series ", func() {
 		Convey("Given a table with metrics that lacks data for some series ", func() {
 			sql := `
 			sql := `
-				IF OBJECT_ID('dbo.[metric]', 'U') IS NOT NULL
-					DROP TABLE dbo.[metric]
+					IF OBJECT_ID('dbo.[metric]', 'U') IS NOT NULL
+						DROP TABLE dbo.[metric]
 
 
-				CREATE TABLE [metric] (
-					time datetime,
-					value int
-				)
-			`
+					CREATE TABLE [metric] (
+						time datetime,
+						value int
+					)
+				`
 
 
 			_, err := sess.Exec(sql)
 			_, err := sess.Exec(sql)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
@@ -186,14 +188,8 @@ func TestMSSQL(t *testing.T) {
 				})
 				})
 			}
 			}
 
 
-			dtFormat := "2006-01-02 15:04:05.999999999"
 			for _, s := range series {
 			for _, s := range series {
-				sql = fmt.Sprintf(`
-					INSERT INTO metric (time, value)
-					VALUES(CAST('%s' AS DATETIME), %d)
-				`, s.Time.Format(dtFormat), s.Value)
-
-				_, err = sess.Exec(sql)
+				_, err = sess.Insert(s)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 			}
 			}
 
 
@@ -211,22 +207,32 @@ func TestMSSQL(t *testing.T) {
 				}
 				}
 
 
 				resp, err := endpoint.Query(nil, nil, query)
 				resp, err := endpoint.Query(nil, nil, query)
-				queryResult := resp.Results["A"]
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
 				So(queryResult.Error, ShouldBeNil)
 				So(queryResult.Error, ShouldBeNil)
 
 
 				points := queryResult.Series[0].Points
 				points := queryResult.Series[0].Points
+				So(len(points), ShouldEqual, 6)
+
+				dt := fromStart
 
 
-				So(len(points), ShouldEqual, 4)
-				actualValueFirst := points[0][0].Float64
-				actualTimeFirst := time.Unix(int64(points[0][1].Float64)/1000, 0)
-				So(actualValueFirst, ShouldEqual, 15)
-				So(actualTimeFirst, ShouldEqual, fromStart)
+				for i := 0; i < 3; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 15)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
 
 
-				actualValueLast := points[3][0].Float64
-				actualTimeLast := time.Unix(int64(points[3][1].Float64)/1000, 0)
-				So(actualValueLast, ShouldEqual, 20)
-				So(actualTimeLast, ShouldEqual, fromStart.Add(25*time.Minute))
+				// adjust for 5 minute gap
+				dt = dt.Add(5 * time.Minute)
+				for i := 3; i < 6; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 20)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
 			})
 			})
 
 
 			Convey("When doing a metric query using timeGroup with NULL fill enabled", func() {
 			Convey("When doing a metric query using timeGroup with NULL fill enabled", func() {
@@ -247,33 +253,34 @@ func TestMSSQL(t *testing.T) {
 				}
 				}
 
 
 				resp, err := endpoint.Query(nil, nil, query)
 				resp, err := endpoint.Query(nil, nil, query)
-				queryResult := resp.Results["A"]
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
 				So(queryResult.Error, ShouldBeNil)
 				So(queryResult.Error, ShouldBeNil)
 
 
 				points := queryResult.Series[0].Points
 				points := queryResult.Series[0].Points
-
 				So(len(points), ShouldEqual, 7)
 				So(len(points), ShouldEqual, 7)
-				actualValueFirst := points[0][0].Float64
-				actualTimeFirst := time.Unix(int64(points[0][1].Float64)/1000, 0)
-				So(actualValueFirst, ShouldEqual, 15)
-				So(actualTimeFirst, ShouldEqual, fromStart)
-
-				actualNullPoint := points[3][0]
-				actualNullTime := time.Unix(int64(points[3][1].Float64)/1000, 0)
-				So(actualNullPoint.Valid, ShouldBeFalse)
-				So(actualNullTime, ShouldEqual, fromStart.Add(15*time.Minute))
-
-				actualValueLast := points[5][0].Float64
-				actualTimeLast := time.Unix(int64(points[5][1].Float64)/1000, 0)
-				So(actualValueLast, ShouldEqual, 20)
-				So(actualTimeLast, ShouldEqual, fromStart.Add(25*time.Minute))
-
-				actualLastNullPoint := points[6][0]
-				actualLastNullTime := time.Unix(int64(points[6][1].Float64)/1000, 0)
-				So(actualLastNullPoint.Valid, ShouldBeFalse)
-				So(actualLastNullTime, ShouldEqual, fromStart.Add(30*time.Minute))
 
 
+				dt := fromStart
+
+				for i := 0; i < 3; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 15)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
+
+				So(points[3][0].Valid, ShouldBeFalse)
+
+				// adjust for 5 minute gap
+				dt = dt.Add(5 * time.Minute)
+				for i := 4; i < 7; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 20)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
 			})
 			})
 
 
 			Convey("When doing a metric query using timeGroup with float fill enabled", func() {
 			Convey("When doing a metric query using timeGroup with float fill enabled", func() {
@@ -294,53 +301,44 @@ func TestMSSQL(t *testing.T) {
 				}
 				}
 
 
 				resp, err := endpoint.Query(nil, nil, query)
 				resp, err := endpoint.Query(nil, nil, query)
-				queryResult := resp.Results["A"]
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
 				So(queryResult.Error, ShouldBeNil)
 				So(queryResult.Error, ShouldBeNil)
 
 
 				points := queryResult.Series[0].Points
 				points := queryResult.Series[0].Points
-
-				So(points[6][0].Float64, ShouldEqual, 1.5)
+				So(points[3][0].Float64, ShouldEqual, 1.5)
 			})
 			})
 		})
 		})
 
 
 		Convey("Given a table with metrics having multiple values and measurements", func() {
 		Convey("Given a table with metrics having multiple values and measurements", func() {
-			sql := `
-				IF OBJECT_ID('dbo.[metric_values]', 'U') IS NOT NULL
-					DROP TABLE dbo.[metric_values]
-
-				CREATE TABLE [metric_values] (
-					time datetime,
-					measurement nvarchar(100),
-					valueOne int,
-					valueTwo int,
-				)
-			`
-
-			_, err := sess.Exec(sql)
-			So(err, ShouldBeNil)
-
-			type metricValues struct {
+			type metric_values struct {
 				Time        time.Time
 				Time        time.Time
 				Measurement string
 				Measurement string
-				ValueOne    int64
-				ValueTwo    int64
+				ValueOne    int64 `xorm:"integer 'valueOne'"`
+				ValueTwo    int64 `xorm:"integer 'valueTwo'"`
+			}
+
+			if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist {
+				So(err, ShouldBeNil)
+				sess.DropTable(metric_values{})
 			}
 			}
+			err := sess.CreateTable(metric_values{})
+			So(err, ShouldBeNil)
 
 
 			rand.Seed(time.Now().Unix())
 			rand.Seed(time.Now().Unix())
 			rnd := func(min, max int64) int64 {
 			rnd := func(min, max int64) int64 {
 				return rand.Int63n(max-min) + min
 				return rand.Int63n(max-min) + min
 			}
 			}
 
 
-			series := []*metricValues{}
+			series := []*metric_values{}
 			for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
 			for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
-				series = append(series, &metricValues{
+				series = append(series, &metric_values{
 					Time:        t,
 					Time:        t,
 					Measurement: "Metric A",
 					Measurement: "Metric A",
 					ValueOne:    rnd(0, 100),
 					ValueOne:    rnd(0, 100),
 					ValueTwo:    rnd(0, 100),
 					ValueTwo:    rnd(0, 100),
 				})
 				})
-				series = append(series, &metricValues{
+				series = append(series, &metric_values{
 					Time:        t,
 					Time:        t,
 					Measurement: "Metric B",
 					Measurement: "Metric B",
 					ValueOne:    rnd(0, 100),
 					ValueOne:    rnd(0, 100),
@@ -348,14 +346,8 @@ func TestMSSQL(t *testing.T) {
 				})
 				})
 			}
 			}
 
 
-			dtFormat := "2006-01-02 15:04:05"
 			for _, s := range series {
 			for _, s := range series {
-				sql = fmt.Sprintf(`
-					INSERT metric_values (time, measurement, valueOne, valueTwo)
-					VALUES(CAST('%s' AS DATETIME), '%s', %d, %d)
-				`, s.Time.Format(dtFormat), s.Measurement, s.ValueOne, s.ValueTwo)
-
-				_, err = sess.Exec(sql)
+				_, err = sess.Insert(s)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 			}
 			}
 
 
@@ -373,8 +365,8 @@ func TestMSSQL(t *testing.T) {
 				}
 				}
 
 
 				resp, err := endpoint.Query(nil, nil, query)
 				resp, err := endpoint.Query(nil, nil, query)
-				queryResult := resp.Results["A"]
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
 				So(queryResult.Error, ShouldBeNil)
 				So(queryResult.Error, ShouldBeNil)
 
 
 				So(len(queryResult.Series), ShouldEqual, 2)
 				So(len(queryResult.Series), ShouldEqual, 2)
@@ -396,8 +388,8 @@ func TestMSSQL(t *testing.T) {
 				}
 				}
 
 
 				resp, err := endpoint.Query(nil, nil, query)
 				resp, err := endpoint.Query(nil, nil, query)
-				queryResult := resp.Results["A"]
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
 				So(queryResult.Error, ShouldBeNil)
 				So(queryResult.Error, ShouldBeNil)
 
 
 				So(len(queryResult.Series), ShouldEqual, 2)
 				So(len(queryResult.Series), ShouldEqual, 2)
@@ -407,45 +399,55 @@ func TestMSSQL(t *testing.T) {
 
 
 			Convey("Given a stored procedure that takes @from and @to in epoch time", func() {
 			Convey("Given a stored procedure that takes @from and @to in epoch time", func() {
 				sql := `
 				sql := `
-					IF object_id('sp_test_epoch') IS NOT NULL
-						DROP PROCEDURE sp_test_epoch
-				`
+						IF object_id('sp_test_epoch') IS NOT NULL
+							DROP PROCEDURE sp_test_epoch
+					`
 
 
 				_, err := sess.Exec(sql)
 				_, err := sess.Exec(sql)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
 				sql = `
 				sql = `
-					CREATE PROCEDURE sp_test_epoch(
-						@from int,
-						@to 	int
-					)	AS
-					BEGIN
-						SELECT
-							cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time,
-							measurement + ' - value one' as metric,
-							avg(valueOne) as value
-						FROM
-							metric_values
-						WHERE
-							time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01')
-						GROUP BY
-							cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int),
-							measurement
-						UNION ALL
-						SELECT
-							cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time,
-							measurement + ' - value two' as metric,
-							avg(valueTwo) as value
-						FROM
-							metric_values
-						WHERE
-							time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01')
-						GROUP BY
-							cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int),
-							measurement
-						ORDER BY 1
-					END
-				`
+						CREATE PROCEDURE sp_test_epoch(
+							@from 		int,
+							@to 			int,
+							@interval nvarchar(50) = '5m',
+							@metric 	nvarchar(200) = 'ALL'
+						)	AS
+						BEGIN
+							DECLARE @dInterval int
+							SELECT @dInterval = 300
+
+							IF @interval = '10m'
+								SELECT @dInterval = 600
+
+							SELECT
+								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
+								measurement + ' - value one' as metric,
+								avg(valueOne) as value
+							FROM
+								metric_values
+							WHERE
+								time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND
+								(@metric = 'ALL' OR measurement = @metric)
+							GROUP BY
+								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
+								measurement
+							UNION ALL
+							SELECT
+								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
+								measurement + ' - value two' as metric,
+								avg(valueTwo) as value
+							FROM
+								metric_values
+							WHERE
+								time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND
+								(@metric = 'ALL' OR measurement = @metric)
+							GROUP BY
+								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
+								measurement
+							ORDER BY 1
+						END
+					`
 
 
 				_, err = sess.Exec(sql)
 				_, err = sess.Exec(sql)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
@@ -456,10 +458,10 @@ func TestMSSQL(t *testing.T) {
 							{
 							{
 								Model: simplejson.NewFromAny(map[string]interface{}{
 								Model: simplejson.NewFromAny(map[string]interface{}{
 									"rawSql": `DECLARE
 									"rawSql": `DECLARE
-										@from int = $__unixEpochFrom(),
-										@to int = $__unixEpochTo()
+											@from int = $__unixEpochFrom(),
+											@to int = $__unixEpochTo()
 
 
-										EXEC dbo.sp_test_epoch @from, @to`,
+											EXEC dbo.sp_test_epoch @from, @to`,
 									"format": "time_series",
 									"format": "time_series",
 								}),
 								}),
 								RefId: "A",
 								RefId: "A",
@@ -474,6 +476,7 @@ func TestMSSQL(t *testing.T) {
 					resp, err := endpoint.Query(nil, nil, query)
 					resp, err := endpoint.Query(nil, nil, query)
 					queryResult := resp.Results["A"]
 					queryResult := resp.Results["A"]
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
+					fmt.Println("query", "sql", queryResult.Meta)
 					So(queryResult.Error, ShouldBeNil)
 					So(queryResult.Error, ShouldBeNil)
 
 
 					So(len(queryResult.Series), ShouldEqual, 4)
 					So(len(queryResult.Series), ShouldEqual, 4)
@@ -486,45 +489,55 @@ func TestMSSQL(t *testing.T) {
 
 
 			Convey("Given a stored procedure that takes @from and @to in datetime", func() {
 			Convey("Given a stored procedure that takes @from and @to in datetime", func() {
 				sql := `
 				sql := `
-					IF object_id('sp_test_datetime') IS NOT NULL
-						DROP PROCEDURE sp_test_datetime
-				`
+						IF object_id('sp_test_datetime') IS NOT NULL
+							DROP PROCEDURE sp_test_datetime
+					`
 
 
 				_, err := sess.Exec(sql)
 				_, err := sess.Exec(sql)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
 				sql = `
 				sql = `
-					CREATE PROCEDURE sp_test_datetime(
-						@from datetime,
-						@to 	datetime
-					)	AS
-					BEGIN
-						SELECT
-							cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time,
-							measurement + ' - value one' as metric,
-							avg(valueOne) as value
-						FROM
-							metric_values
-						WHERE
-							time >= @from AND time <= @to
-						GROUP BY
-							cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int),
-							measurement
-						UNION ALL
-						SELECT
-							cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time,
-							measurement + ' - value two' as metric,
-							avg(valueTwo) as value
-						FROM
-							metric_values
-						WHERE
-							time >= @from AND time <= @to
-						GROUP BY
-							cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int),
-							measurement
-						ORDER BY 1
-					END
-				`
+						CREATE PROCEDURE sp_test_datetime(
+							@from 		datetime,
+							@to 			datetime,
+							@interval nvarchar(50) = '5m',
+							@metric 	nvarchar(200) = 'ALL'
+						)	AS
+						BEGIN
+							DECLARE @dInterval int
+							SELECT @dInterval = 300
+
+							IF @interval = '10m'
+								SELECT @dInterval = 600
+
+							SELECT
+								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
+								measurement + ' - value one' as metric,
+								avg(valueOne) as value
+							FROM
+								metric_values
+							WHERE
+								time BETWEEN @from AND @to AND
+								(@metric = 'ALL' OR measurement = @metric)
+							GROUP BY
+								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
+								measurement
+							UNION ALL
+							SELECT
+								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
+								measurement + ' - value two' as metric,
+								avg(valueTwo) as value
+							FROM
+								metric_values
+							WHERE
+								time BETWEEN @from AND @to AND
+								(@metric = 'ALL' OR measurement = @metric)
+							GROUP BY
+								CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
+								measurement
+							ORDER BY 1
+						END
+					`
 
 
 				_, err = sess.Exec(sql)
 				_, err = sess.Exec(sql)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
@@ -535,10 +548,10 @@ func TestMSSQL(t *testing.T) {
 							{
 							{
 								Model: simplejson.NewFromAny(map[string]interface{}{
 								Model: simplejson.NewFromAny(map[string]interface{}{
 									"rawSql": `DECLARE
 									"rawSql": `DECLARE
-										@from int = $__unixEpochFrom(),
-										@to int = $__unixEpochTo()
+											@from int = $__unixEpochFrom(),
+											@to int = $__unixEpochTo()
 
 
-										EXEC dbo.sp_test_epoch @from, @to`,
+											EXEC dbo.sp_test_epoch @from, @to`,
 									"format": "time_series",
 									"format": "time_series",
 								}),
 								}),
 								RefId: "A",
 								RefId: "A",
@@ -570,7 +583,7 @@ func TestMSSQL(t *testing.T) {
 					DROP TABLE dbo.[event]
 					DROP TABLE dbo.[event]
 
 
 				CREATE TABLE [event] (
 				CREATE TABLE [event] (
-					time_sec bigint,
+					time_sec int,
 					description nvarchar(100),
 					description nvarchar(100),
 					tags nvarchar(100),
 					tags nvarchar(100),
 				)
 				)
@@ -654,12 +667,191 @@ func TestMSSQL(t *testing.T) {
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 				So(len(queryResult.Tables[0].Rows), ShouldEqual, 3)
 				So(len(queryResult.Tables[0].Rows), ShouldEqual, 3)
 			})
 			})
+
+			Convey("When doing an annotation query with a time column in datetime format", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
+				dtFormat := "2006-01-02 15:04:05.999999999"
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									CAST('%s' AS DATETIME) as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Format(dtFormat)),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000))
+			})
+
+			Convey("When doing an annotation query with a time column in epoch second format should return ms", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									 %d as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Unix()),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
+			})
+
+			Convey("When doing an annotation query with a time column in epoch second format (int) should return ms", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									 cast(%d as int) as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Unix()),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
+			})
+
+			Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									 %d as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Unix()*1000),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000))
+			})
+
+			Convey("When doing an annotation query with a time column holding a bigint null value should return nil", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT
+									 cast(null as bigint) as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`,
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0], ShouldBeNil)
+			})
+
+			Convey("When doing an annotation query with a time column holding a datetime null value should return nil", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT
+									 cast(null as datetime) as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`,
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0], ShouldBeNil)
+			})
 		})
 		})
 	})
 	})
 }
 }
 
 
 func InitMSSQLTestDB(t *testing.T) *xorm.Engine {
 func InitMSSQLTestDB(t *testing.T) *xorm.Engine {
 	x, err := xorm.NewEngine(sqlutil.TestDB_Mssql.DriverName, strings.Replace(sqlutil.TestDB_Mssql.ConnStr, "localhost", serverIP, 1))
 	x, err := xorm.NewEngine(sqlutil.TestDB_Mssql.DriverName, strings.Replace(sqlutil.TestDB_Mssql.ConnStr, "localhost", serverIP, 1))
+	x.DatabaseTZ = time.UTC
+	x.TZLocation = time.UTC
 
 
 	// x.ShowSQL()
 	// x.ShowSQL()
 
 
@@ -667,8 +859,6 @@ func InitMSSQLTestDB(t *testing.T) *xorm.Engine {
 		t.Fatalf("Failed to init mssql db %v", err)
 		t.Fatalf("Failed to init mssql db %v", err)
 	}
 	}
 
 
-	sqlutil.CleanDB(x)
-
 	return x
 	return x
 }
 }
 
 

+ 1 - 1
pkg/tsdb/mysql/macros.go

@@ -68,7 +68,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str
 
 
 func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
 func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
 	switch name {
 	switch name {
-	case "__time":
+	case "__timeEpoch", "__time":
 		if len(args) == 0 {
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
 		}

+ 99 - 96
pkg/tsdb/mysql/mysql.go

@@ -81,7 +81,7 @@ func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows,
 	// check if there is a column named time
 	// check if there is a column named time
 	for i, col := range columnNames {
 	for i, col := range columnNames {
 		switch col {
 		switch col {
-		case "time_sec":
+		case "time", "time_sec":
 			timeIndex = i
 			timeIndex = i
 		}
 		}
 	}
 	}
@@ -96,13 +96,10 @@ func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows,
 			return err
 			return err
 		}
 		}
 
 
-		// for annotations, convert to epoch
-		if timeIndex != -1 {
-			switch value := values[timeIndex].(type) {
-			case time.Time:
-				values[timeIndex] = float64(value.UnixNano() / 1e9)
-			}
-		}
+		// converts column named time to unix timestamp in milliseconds to make
+		// native mysql datetime types and epoch dates work in
+		// annotation and table queries.
+		tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
 
 
 		table.Rows = append(table.Rows, values)
 		table.Rows = append(table.Rows, values)
 	}
 	}
@@ -185,9 +182,37 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 		return err
 		return err
 	}
 	}
 
 
-	rowData := NewStringStringScan(columnNames)
+	columnTypes, err := rows.ColumnTypes()
+	if err != nil {
+		return err
+	}
+
 	rowLimit := 1000000
 	rowLimit := 1000000
 	rowCount := 0
 	rowCount := 0
+	timeIndex := -1
+	metricIndex := -1
+
+	// check columns of resultset: a column named time is mandatory
+	// the first text column is treated as metric name unless a column named metric is present
+	for i, col := range columnNames {
+		switch col {
+		case "time", "time_sec":
+			timeIndex = i
+		case "metric":
+			metricIndex = i
+		default:
+			if metricIndex == -1 {
+				switch columnTypes[i].DatabaseTypeName() {
+				case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT":
+					metricIndex = i
+				}
+			}
+		}
+	}
+
+	if timeIndex == -1 {
+		return fmt.Errorf("Found no column named time or time_sec")
+	}
 
 
 	fillMissing := query.Model.Get("fill").MustBool(false)
 	fillMissing := query.Model.Get("fill").MustBool(false)
 	var fillInterval float64
 	var fillInterval float64
@@ -198,53 +223,90 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Valid = true
 			fillValue.Valid = true
 		}
 		}
-
 	}
 	}
 
 
-	for ; rows.Next(); rowCount++ {
+	for rows.Next() {
+		var timestamp float64
+		var value null.Float
+		var metric string
+
 		if rowCount > rowLimit {
 		if rowCount > rowLimit {
-			return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
+			return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit)
 		}
 		}
 
 
-		err := rowData.Update(rows.Rows)
+		values, err := e.getTypedRowData(rows)
 		if err != nil {
 		if err != nil {
-			e.log.Error("MySQL response parsing", "error", err)
-			return fmt.Errorf("MySQL response parsing error %v", err)
+			return err
 		}
 		}
 
 
-		if rowData.metric == "" {
-			rowData.metric = "Unknown"
+		switch columnValue := values[timeIndex].(type) {
+		case int64:
+			timestamp = float64(columnValue * 1000)
+		case float64:
+			timestamp = columnValue * 1000
+		case time.Time:
+			timestamp = float64(columnValue.UnixNano() / 1e6)
+		default:
+			return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
 		}
 		}
 
 
-		if !rowData.time.Valid {
-			return fmt.Errorf("Found row with no time value")
+		if metricIndex >= 0 {
+			if columnValue, ok := values[metricIndex].(string); ok == true {
+				metric = columnValue
+			} else {
+				return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
+			}
 		}
 		}
 
 
-		series, exist := pointsBySeries[rowData.metric]
-		if exist == false {
-			series = &tsdb.TimeSeries{Name: rowData.metric}
-			pointsBySeries[rowData.metric] = series
-			seriesByQueryOrder.PushBack(rowData.metric)
-		}
+		for i, col := range columnNames {
+			if i == timeIndex || i == metricIndex {
+				continue
+			}
 
 
-		if fillMissing {
-			var intervalStart float64
+			switch columnValue := values[i].(type) {
+			case int64:
+				value = null.FloatFrom(float64(columnValue))
+			case float64:
+				value = null.FloatFrom(columnValue)
+			case nil:
+				value.Valid = false
+			default:
+				return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue)
+			}
+			if metricIndex == -1 {
+				metric = col
+			}
+
+			series, exist := pointsBySeries[metric]
 			if exist == false {
 			if exist == false {
-				intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
-			} else {
-				intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
+				series = &tsdb.TimeSeries{Name: metric}
+				pointsBySeries[metric] = series
+				seriesByQueryOrder.PushBack(metric)
 			}
 			}
 
 
-			// align interval start
-			intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
+			if fillMissing {
+				var intervalStart float64
+				if exist == false {
+					intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
+				} else {
+					intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
+				}
 
 
-			for i := intervalStart; i < rowData.time.Float64; i += fillInterval {
-				series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
-				rowCount++
+				// align interval start
+				intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
+
+				for i := intervalStart; i < timestamp; i += fillInterval {
+					series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
+					rowCount++
+				}
 			}
 			}
-		}
 
 
-		series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time})
+			series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)})
+
+			e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value)
+			rowCount++
+
+		}
 	}
 	}
 
 
 	for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
 	for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
@@ -269,62 +331,3 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
 	result.Meta.Set("rowCount", rowCount)
 	result.Meta.Set("rowCount", rowCount)
 	return nil
 	return nil
 }
 }
-
-type stringStringScan struct {
-	rowPtrs     []interface{}
-	rowValues   []string
-	columnNames []string
-	columnCount int
-
-	time   null.Float
-	value  null.Float
-	metric string
-}
-
-func NewStringStringScan(columnNames []string) *stringStringScan {
-	s := &stringStringScan{
-		columnCount: len(columnNames),
-		columnNames: columnNames,
-		rowPtrs:     make([]interface{}, len(columnNames)),
-		rowValues:   make([]string, len(columnNames)),
-	}
-
-	for i := 0; i < s.columnCount; i++ {
-		s.rowPtrs[i] = new(sql.RawBytes)
-	}
-
-	return s
-}
-
-func (s *stringStringScan) Update(rows *sql.Rows) error {
-	if err := rows.Scan(s.rowPtrs...); err != nil {
-		return err
-	}
-
-	s.time = null.FloatFromPtr(nil)
-	s.value = null.FloatFromPtr(nil)
-
-	for i := 0; i < s.columnCount; i++ {
-		if rb, ok := s.rowPtrs[i].(*sql.RawBytes); ok {
-			s.rowValues[i] = string(*rb)
-
-			switch s.columnNames[i] {
-			case "time_sec":
-				if sec, err := strconv.ParseInt(s.rowValues[i], 10, 64); err == nil {
-					s.time = null.FloatFrom(float64(sec * 1000))
-				}
-			case "value":
-				if value, err := strconv.ParseFloat(s.rowValues[i], 64); err == nil {
-					s.value = null.FloatFrom(value)
-				}
-			case "metric":
-				s.metric = s.rowValues[i]
-			}
-
-			*rb = nil // reset pointer to discard current value to avoid a bug
-		} else {
-			return fmt.Errorf("Cannot convert index %d column %s to type *sql.RawBytes", i, s.columnNames[i])
-		}
-	}
-	return nil
-}

+ 624 - 96
pkg/tsdb/mysql/mysql_test.go

@@ -1,6 +1,8 @@
 package mysql
 package mysql
 
 
 import (
 import (
+	"fmt"
+	"math/rand"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -14,6 +16,10 @@ import (
 
 
 // To run this test, remove the Skip from SkipConvey
 // To run this test, remove the Skip from SkipConvey
 // and set up a MySQL db named grafana_tests and a user/password grafana/password
 // and set up a MySQL db named grafana_tests and a user/password grafana/password
+// Use the docker/blocks/mysql_tests/docker-compose.yaml to spin up a
+// preconfigured MySQL server suitable for running these tests.
+// Thers's also a dashboard.json in same directory that you can import to Grafana
+// once you've created a datasource for the test server/database.
 func TestMySQL(t *testing.T) {
 func TestMySQL(t *testing.T) {
 	SkipConvey("MySQL", t, func() {
 	SkipConvey("MySQL", t, func() {
 		x := InitMySQLTestDB(t)
 		x := InitMySQLTestDB(t)
@@ -29,110 +35,621 @@ func TestMySQL(t *testing.T) {
 		sess := x.NewSession()
 		sess := x.NewSession()
 		defer sess.Close()
 		defer sess.Close()
 
 
-		sql := "CREATE TABLE `mysql_types` ("
-		sql += "`atinyint` tinyint(1) NOT NULL,"
-		sql += "`avarchar` varchar(3) NOT NULL,"
-		sql += "`achar` char(3),"
-		sql += "`amediumint` mediumint NOT NULL,"
-		sql += "`asmallint` smallint NOT NULL,"
-		sql += "`abigint` bigint NOT NULL,"
-		sql += "`aint` int(11) NOT NULL,"
-		sql += "`adouble` double(10,2),"
-		sql += "`anewdecimal` decimal(10,2),"
-		sql += "`afloat` float(10,2) NOT NULL,"
-		sql += "`atimestamp` timestamp NOT NULL,"
-		sql += "`adatetime` datetime NOT NULL,"
-		sql += "`atime` time NOT NULL,"
-		// sql += "`ayear` year," // Crashes xorm when running cleandb
-		sql += "`abit` bit(1),"
-		sql += "`atinytext` tinytext,"
-		sql += "`atinyblob` tinyblob,"
-		sql += "`atext` text,"
-		sql += "`ablob` blob,"
-		sql += "`amediumtext` mediumtext,"
-		sql += "`amediumblob` mediumblob,"
-		sql += "`alongtext` longtext,"
-		sql += "`alongblob` longblob,"
-		sql += "`aenum` enum('val1', 'val2'),"
-		sql += "`aset` set('a', 'b', 'c', 'd'),"
-		sql += "`adate` date,"
-		sql += "`time_sec` datetime(6),"
-		sql += "`aintnull` int(11),"
-		sql += "`afloatnull` float(10,2),"
-		sql += "`avarcharnull` varchar(3),"
-		sql += "`adecimalnull` decimal(10,2)"
-		sql += ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"
-		_, err := sess.Exec(sql)
-		So(err, ShouldBeNil)
-
-		sql = "INSERT INTO `mysql_types` "
-		sql += "(`atinyint`, `avarchar`, `achar`, `amediumint`, `asmallint`, `abigint`, `aint`, `adouble`, "
-		sql += "`anewdecimal`, `afloat`, `adatetime`, `atimestamp`, `atime`, `abit`, `atinytext`, "
-		sql += "`atinyblob`, `atext`, `ablob`, `amediumtext`, `amediumblob`, `alongtext`, `alongblob`, "
-		sql += "`aenum`, `aset`, `adate`, `time_sec`) "
-		sql += "VALUES(1, 'abc', 'def', 1, 10, 100, 1420070400, 1.11, "
-		sql += "2.22, 3.33, now(), current_timestamp(), '11:11:11', 1, 'tinytext', "
-		sql += "'tinyblob', 'text', 'blob', 'mediumtext', 'mediumblob', 'longtext', 'longblob', "
-		sql += "'val2', 'a,b', curdate(), '2018-01-01 00:01:01.123456');"
-		_, err = sess.Exec(sql)
-		So(err, ShouldBeNil)
-
-		Convey("Query with Table format should map MySQL column types to Go types", func() {
-			query := &tsdb.TsdbQuery{
-				Queries: []*tsdb.Query{
-					{
-						Model: simplejson.NewFromAny(map[string]interface{}{
-							"rawSql": "SELECT * FROM mysql_types",
-							"format": "table",
-						}),
-						RefId: "A",
-					},
-				},
+		fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.Local)
+
+		Convey("Given a table with different native data types", func() {
+			if exists, err := sess.IsTableExist("mysql_types"); err != nil || exists {
+				So(err, ShouldBeNil)
+				sess.DropTable("mysql_types")
 			}
 			}
 
 
-			resp, err := endpoint.Query(nil, nil, query)
-			queryResult := resp.Results["A"]
+			sql := "CREATE TABLE `mysql_types` ("
+			sql += "`atinyint` tinyint(1) NOT NULL,"
+			sql += "`avarchar` varchar(3) NOT NULL,"
+			sql += "`achar` char(3),"
+			sql += "`amediumint` mediumint NOT NULL,"
+			sql += "`asmallint` smallint NOT NULL,"
+			sql += "`abigint` bigint NOT NULL,"
+			sql += "`aint` int(11) NOT NULL,"
+			sql += "`adouble` double(10,2),"
+			sql += "`anewdecimal` decimal(10,2),"
+			sql += "`afloat` float(10,2) NOT NULL,"
+			sql += "`atimestamp` timestamp NOT NULL,"
+			sql += "`adatetime` datetime NOT NULL,"
+			sql += "`atime` time NOT NULL,"
+			sql += "`ayear` year," // Crashes xorm when running cleandb
+			sql += "`abit` bit(1),"
+			sql += "`atinytext` tinytext,"
+			sql += "`atinyblob` tinyblob,"
+			sql += "`atext` text,"
+			sql += "`ablob` blob,"
+			sql += "`amediumtext` mediumtext,"
+			sql += "`amediumblob` mediumblob,"
+			sql += "`alongtext` longtext,"
+			sql += "`alongblob` longblob,"
+			sql += "`aenum` enum('val1', 'val2'),"
+			sql += "`aset` set('a', 'b', 'c', 'd'),"
+			sql += "`adate` date,"
+			sql += "`time_sec` datetime(6),"
+			sql += "`aintnull` int(11),"
+			sql += "`afloatnull` float(10,2),"
+			sql += "`avarcharnull` varchar(3),"
+			sql += "`adecimalnull` decimal(10,2)"
+			sql += ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"
+			_, err := sess.Exec(sql)
+			So(err, ShouldBeNil)
+
+			sql = "INSERT INTO `mysql_types` "
+			sql += "(`atinyint`, `avarchar`, `achar`, `amediumint`, `asmallint`, `abigint`, `aint`, `adouble`, "
+			sql += "`anewdecimal`, `afloat`, `adatetime`, `atimestamp`, `atime`, `ayear`, `abit`, `atinytext`, "
+			sql += "`atinyblob`, `atext`, `ablob`, `amediumtext`, `amediumblob`, `alongtext`, `alongblob`, "
+			sql += "`aenum`, `aset`, `adate`, `time_sec`) "
+			sql += "VALUES(1, 'abc', 'def', 1, 10, 100, 1420070400, 1.11, "
+			sql += "2.22, 3.33, now(), current_timestamp(), '11:11:11', '2018', 1, 'tinytext', "
+			sql += "'tinyblob', 'text', 'blob', 'mediumtext', 'mediumblob', 'longtext', 'longblob', "
+			sql += "'val2', 'a,b', curdate(), '2018-01-01 00:01:01.123456');"
+			_, err = sess.Exec(sql)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			column := queryResult.Tables[0].Rows[0]
-
-			So(*column[0].(*int8), ShouldEqual, 1)
-			So(column[1].(string), ShouldEqual, "abc")
-			So(column[2].(string), ShouldEqual, "def")
-			So(*column[3].(*int32), ShouldEqual, 1)
-			So(*column[4].(*int16), ShouldEqual, 10)
-			So(*column[5].(*int64), ShouldEqual, 100)
-			So(*column[6].(*int32), ShouldEqual, 1420070400)
-			So(column[7].(float64), ShouldEqual, 1.11)
-			So(column[8].(float64), ShouldEqual, 2.22)
-			So(*column[9].(*float32), ShouldEqual, 3.33)
-			_, offset := time.Now().Zone()
-			So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
-			So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
-			So(column[12].(string), ShouldEqual, "11:11:11")
-			So(*column[13].(*[]byte), ShouldHaveSameTypeAs, []byte{1})
-			So(column[14].(string), ShouldEqual, "tinytext")
-			So(column[15].(string), ShouldEqual, "tinyblob")
-			So(column[16].(string), ShouldEqual, "text")
-			So(column[17].(string), ShouldEqual, "blob")
-			So(column[18].(string), ShouldEqual, "mediumtext")
-			So(column[19].(string), ShouldEqual, "mediumblob")
-			So(column[20].(string), ShouldEqual, "longtext")
-			So(column[21].(string), ShouldEqual, "longblob")
-			So(column[22].(string), ShouldEqual, "val2")
-			So(column[23].(string), ShouldEqual, "a,b")
-			So(column[24].(time.Time).Format("2006-01-02T00:00:00Z"), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z"))
-			So(column[25].(float64), ShouldEqual, 1514764861)
-			So(column[26], ShouldEqual, nil)
-			So(column[27], ShouldEqual, nil)
-			So(column[28], ShouldEqual, "")
-			So(column[29], ShouldEqual, nil)
+			Convey("Query with Table format should map MySQL column types to Go types", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": "SELECT * FROM mysql_types",
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				column := queryResult.Tables[0].Rows[0]
+
+				So(*column[0].(*int8), ShouldEqual, 1)
+				So(column[1].(string), ShouldEqual, "abc")
+				So(column[2].(string), ShouldEqual, "def")
+				So(*column[3].(*int32), ShouldEqual, 1)
+				So(*column[4].(*int16), ShouldEqual, 10)
+				So(*column[5].(*int64), ShouldEqual, 100)
+				So(*column[6].(*int32), ShouldEqual, 1420070400)
+				So(column[7].(float64), ShouldEqual, 1.11)
+				So(column[8].(float64), ShouldEqual, 2.22)
+				So(*column[9].(*float32), ShouldEqual, 3.33)
+				_, offset := time.Now().Zone()
+				So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
+				So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
+				So(column[12].(string), ShouldEqual, "11:11:11")
+				So(column[13].(int64), ShouldEqual, 2018)
+				So(*column[14].(*[]byte), ShouldHaveSameTypeAs, []byte{1})
+				So(column[15].(string), ShouldEqual, "tinytext")
+				So(column[16].(string), ShouldEqual, "tinyblob")
+				So(column[17].(string), ShouldEqual, "text")
+				So(column[18].(string), ShouldEqual, "blob")
+				So(column[19].(string), ShouldEqual, "mediumtext")
+				So(column[20].(string), ShouldEqual, "mediumblob")
+				So(column[21].(string), ShouldEqual, "longtext")
+				So(column[22].(string), ShouldEqual, "longblob")
+				So(column[23].(string), ShouldEqual, "val2")
+				So(column[24].(string), ShouldEqual, "a,b")
+				So(column[25].(time.Time).Format("2006-01-02T00:00:00Z"), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z"))
+				So(column[26].(float64), ShouldEqual, float64(1514764861000))
+				So(column[27], ShouldEqual, nil)
+				So(column[28], ShouldEqual, nil)
+				So(column[29], ShouldEqual, "")
+				So(column[30], ShouldEqual, nil)
+			})
+		})
+
+		Convey("Given a table with metrics that lacks data for some series ", func() {
+			type metric struct {
+				Time  time.Time
+				Value int64
+			}
+
+			if exist, err := sess.IsTableExist(metric{}); err != nil || exist {
+				So(err, ShouldBeNil)
+				sess.DropTable(metric{})
+			}
+			err := sess.CreateTable(metric{})
+			So(err, ShouldBeNil)
+
+			series := []*metric{}
+			firstRange := genTimeRangeByInterval(fromStart, 10*time.Minute, 10*time.Second)
+			secondRange := genTimeRangeByInterval(fromStart.Add(20*time.Minute), 10*time.Minute, 10*time.Second)
+
+			for _, t := range firstRange {
+				series = append(series, &metric{
+					Time:  t,
+					Value: 15,
+				})
+			}
+
+			for _, t := range secondRange {
+				series = append(series, &metric{
+					Time:  t,
+					Value: 20,
+				})
+			}
+
+			for _, s := range series {
+				_, err = sess.Insert(s)
+				So(err, ShouldBeNil)
+			}
+
+			Convey("When doing a metric query using timeGroup", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": "SELECT $__timeGroup(time, '5m') as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				points := queryResult.Series[0].Points
+				So(len(points), ShouldEqual, 6)
+
+				dt := fromStart
+
+				for i := 0; i < 3; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 15)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
+
+				// adjust for 5 minute gap
+				dt = dt.Add(5 * time.Minute)
+				for i := 3; i < 6; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 20)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
+			})
+
+			Convey("When doing a metric query using timeGroup with NULL fill enabled", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": "SELECT $__timeGroup(time, '5m', NULL) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+					TimeRange: &tsdb.TimeRange{
+						From: fmt.Sprintf("%v", fromStart.Unix()*1000),
+						To:   fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				points := queryResult.Series[0].Points
+				So(len(points), ShouldEqual, 7)
+
+				dt := fromStart
+
+				for i := 0; i < 3; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 15)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
+
+				So(points[3][0].Valid, ShouldBeFalse)
+
+				// adjust for 5 minute gap
+				dt = dt.Add(5 * time.Minute)
+				for i := 4; i < 7; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 20)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
+			})
+
+			Convey("When doing a metric query using timeGroup with float fill enabled", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": "SELECT $__timeGroup(time, '5m', 1.5) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+					TimeRange: &tsdb.TimeRange{
+						From: fmt.Sprintf("%v", fromStart.Unix()*1000),
+						To:   fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				points := queryResult.Series[0].Points
+				So(points[3][0].Float64, ShouldEqual, 1.5)
+			})
+		})
+
+		Convey("Given a table with metrics having multiple values and measurements", func() {
+			type metric_values struct {
+				Time        time.Time
+				Measurement string
+				ValueOne    int64 `xorm:"integer 'valueOne'"`
+				ValueTwo    int64 `xorm:"integer 'valueTwo'"`
+			}
+
+			if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist {
+				So(err, ShouldBeNil)
+				sess.DropTable(metric_values{})
+			}
+			err := sess.CreateTable(metric_values{})
+			So(err, ShouldBeNil)
+
+			rand.Seed(time.Now().Unix())
+			rnd := func(min, max int64) int64 {
+				return rand.Int63n(max-min) + min
+			}
+
+			series := []*metric_values{}
+			for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
+				series = append(series, &metric_values{
+					Time:        t,
+					Measurement: "Metric A",
+					ValueOne:    rnd(0, 100),
+					ValueTwo:    rnd(0, 100),
+				})
+				series = append(series, &metric_values{
+					Time:        t,
+					Measurement: "Metric B",
+					ValueOne:    rnd(0, 100),
+					ValueTwo:    rnd(0, 100),
+				})
+			}
+
+			for _, s := range series {
+				_, err := sess.Insert(s)
+				So(err, ShouldBeNil)
+			}
+
+			Convey("When doing a metric query grouping by time and select metric column should return correct series", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values ORDER BY 1`,
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				So(len(queryResult.Series), ShouldEqual, 2)
+				So(queryResult.Series[0].Name, ShouldEqual, "Metric B - value one")
+				So(queryResult.Series[1].Name, ShouldEqual, "Metric A - value one")
+			})
+
+			Convey("When doing a metric query grouping by time should return correct series", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT $__time(time), valueOne, valueTwo FROM metric_values ORDER BY 1`,
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				So(len(queryResult.Series), ShouldEqual, 2)
+				So(queryResult.Series[0].Name, ShouldEqual, "valueOne")
+				So(queryResult.Series[1].Name, ShouldEqual, "valueTwo")
+			})
+		})
+
+		Convey("Given a table with event data", func() {
+			type event struct {
+				TimeSec     int64
+				Description string
+				Tags        string
+			}
+
+			if exist, err := sess.IsTableExist(event{}); err != nil || exist {
+				So(err, ShouldBeNil)
+				sess.DropTable(event{})
+			}
+			err := sess.CreateTable(event{})
+			So(err, ShouldBeNil)
+
+			events := []*event{}
+			for _, t := range genTimeRangeByInterval(fromStart.Add(-20*time.Minute), 60*time.Minute, 25*time.Minute) {
+				events = append(events, &event{
+					TimeSec:     t.Unix(),
+					Description: "Someone deployed something",
+					Tags:        "deploy",
+				})
+				events = append(events, &event{
+					TimeSec:     t.Add(5 * time.Minute).Unix(),
+					Description: "New support ticket registered",
+					Tags:        "ticket",
+				})
+			}
+
+			for _, e := range events {
+				_, err = sess.Insert(e)
+				So(err, ShouldBeNil)
+			}
+
+			Convey("When doing an annotation query of deploy events should return expected result", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT time_sec, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC`,
+								"format": "table",
+							}),
+							RefId: "Deploys",
+						},
+					},
+					TimeRange: &tsdb.TimeRange{
+						From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000),
+						To:   fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000),
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				queryResult := resp.Results["Deploys"]
+				So(err, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 3)
+			})
+
+			Convey("When doing an annotation query of ticket events should return expected result", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT time_sec, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC`,
+								"format": "table",
+							}),
+							RefId: "Tickets",
+						},
+					},
+					TimeRange: &tsdb.TimeRange{
+						From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000),
+						To:   fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000),
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				queryResult := resp.Results["Tickets"]
+				So(err, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 3)
+			})
+
+			Convey("When doing an annotation query with a time column in datetime format", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 0, time.UTC)
+				dtFormat := "2006-01-02 15:04:05.999999999"
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									CAST('%s' as datetime) as time_sec,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Format(dtFormat)),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000))
+			})
+
+			Convey("When doing an annotation query with a time column in epoch second format should return ms", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									 %d as time_sec,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Unix()),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(int64), ShouldEqual, dt.Unix()*1000)
+			})
+
+			Convey("When doing an annotation query with a time column in epoch second format (signed integer) should return ms", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 0, time.Local)
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									 CAST('%d' as signed integer) as time_sec,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Unix()),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
+			})
+
+			Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									 %d as time_sec,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Unix()*1000),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(int64), ShouldEqual, dt.Unix()*1000)
+			})
+
+			Convey("When doing an annotation query with a time column holding a unsigned integer null value should return nil", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT
+									 cast(null as unsigned integer) as time_sec,
+									'message' as text,
+									'tag1,tag2' as tags
+								`,
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0], ShouldBeNil)
+			})
+
+			Convey("When doing an annotation query with a time column holding a DATETIME null value should return nil", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT
+									 cast(null as DATETIME) as time_sec,
+									'message' as text,
+									'tag1,tag2' as tags
+								`,
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0], ShouldBeNil)
+			})
 		})
 		})
 	})
 	})
 }
 }
 
 
 func InitMySQLTestDB(t *testing.T) *xorm.Engine {
 func InitMySQLTestDB(t *testing.T) *xorm.Engine {
 	x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr+"&parseTime=true")
 	x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr+"&parseTime=true")
+	x.DatabaseTZ = time.Local
+	x.TZLocation = time.Local
 
 
 	// x.ShowSQL()
 	// x.ShowSQL()
 
 
@@ -140,7 +657,18 @@ func InitMySQLTestDB(t *testing.T) *xorm.Engine {
 		t.Fatalf("Failed to init mysql db %v", err)
 		t.Fatalf("Failed to init mysql db %v", err)
 	}
 	}
 
 
-	sqlutil.CleanDB(x)
-
 	return x
 	return x
 }
 }
+
+func genTimeRangeByInterval(from time.Time, duration time.Duration, interval time.Duration) []time.Time {
+	durationSec := int64(duration.Seconds())
+	intervalSec := int64(interval.Seconds())
+	timeRange := []time.Time{}
+
+	for i := int64(0); i < durationSec; i += intervalSec {
+		timeRange = append(timeRange, from)
+		from = from.Add(time.Duration(int64(time.Second) * intervalSec))
+	}
+
+	return timeRange
+}

+ 4 - 11
pkg/tsdb/postgres/postgres.go

@@ -63,7 +63,6 @@ func (e *PostgresQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSo
 }
 }
 
 
 func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
 func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
-
 	columnNames, err := rows.Columns()
 	columnNames, err := rows.Columns()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -100,14 +99,10 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro
 			return err
 			return err
 		}
 		}
 
 
-		// convert column named time to unix timestamp to make
-		// native datetime postgres types work in annotation queries
-		if timeIndex != -1 {
-			switch value := values[timeIndex].(type) {
-			case time.Time:
-				values[timeIndex] = float64(value.UnixNano() / 1e9)
-			}
-		}
+		// converts column named time to unix timestamp in milliseconds to make
+		// native postgres datetime types and epoch dates work in
+		// annotation and table queries.
+		tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
 
 
 		table.Rows = append(table.Rows, values)
 		table.Rows = append(table.Rows, values)
 	}
 	}
@@ -118,7 +113,6 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro
 }
 }
 
 
 func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) {
 func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) {
-
 	types, err := rows.ColumnTypes()
 	types, err := rows.ColumnTypes()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -209,7 +203,6 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
 			fillValue.Valid = true
 			fillValue.Valid = true
 		}
 		}
-
 	}
 	}
 
 
 	for rows.Next() {
 	for rows.Next() {

+ 603 - 75
pkg/tsdb/postgres/postgres_test.go

@@ -1,6 +1,8 @@
 package postgres
 package postgres
 
 
 import (
 import (
+	"fmt"
+	"math/rand"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -14,7 +16,11 @@ import (
 )
 )
 
 
 // To run this test, remove the Skip from SkipConvey
 // To run this test, remove the Skip from SkipConvey
-// and set up a PostgreSQL db named grafanatest and a user/password grafanatest/grafanatest
+// and set up a PostgreSQL db named grafanatest and a user/password grafanatest/grafanatest!
+// Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a
+// preconfigured Postgres server suitable for running these tests.
+// Thers's also a dashboard.json in same directory that you can import to Grafana
+// once you've created a datasource for the test server/database.
 func TestPostgres(t *testing.T) {
 func TestPostgres(t *testing.T) {
 	SkipConvey("PostgreSQL", t, func() {
 	SkipConvey("PostgreSQL", t, func() {
 		x := InitPostgresTestDB(t)
 		x := InitPostgresTestDB(t)
@@ -30,88 +36,599 @@ func TestPostgres(t *testing.T) {
 		sess := x.NewSession()
 		sess := x.NewSession()
 		defer sess.Close()
 		defer sess.Close()
 
 
-		sql := `
-      CREATE TABLE postgres_types(
-        c00_smallint smallint,
-        c01_integer integer,
-        c02_bigint bigint,
-
-        c03_real real,
-        c04_double double precision,
-        c05_decimal decimal(10,2),
-        c06_numeric numeric(10,2),
-
-        c07_char char(10),
-        c08_varchar varchar(10),
-        c09_text text,
-
-        c10_timestamp timestamp without time zone,
-        c11_timestamptz timestamp with time zone,
-        c12_date date,
-        c13_time time without time zone,
-        c14_timetz time with time zone,
-        c15_interval interval
-      );
-    `
-		_, err := sess.Exec(sql)
-		So(err, ShouldBeNil)
-
-		sql = `
-      INSERT INTO postgres_types VALUES(
-        1,2,3,
-        4.5,6.7,1.1,1.2,
-        'char10','varchar10','text',
-
-        now(),now(),now(),now(),now(),'15m'::interval
-      );
-    `
-		_, err = sess.Exec(sql)
-		So(err, ShouldBeNil)
-
-		Convey("Query with Table format should map PostgreSQL column types to Go types", func() {
-			query := &tsdb.TsdbQuery{
-				Queries: []*tsdb.Query{
-					{
-						Model: simplejson.NewFromAny(map[string]interface{}{
-							"rawSql": "SELECT * FROM postgres_types",
-							"format": "table",
-						}),
-						RefId: "A",
-					},
-				},
+		fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
+
+		Convey("Given a table with different native data types", func() {
+			sql := `
+				DROP TABLE IF EXISTS postgres_types;
+				CREATE TABLE postgres_types(
+					c00_smallint smallint,
+					c01_integer integer,
+					c02_bigint bigint,
+
+					c03_real real,
+					c04_double double precision,
+					c05_decimal decimal(10,2),
+					c06_numeric numeric(10,2),
+
+					c07_char char(10),
+					c08_varchar varchar(10),
+					c09_text text,
+
+					c10_timestamp timestamp without time zone,
+					c11_timestamptz timestamp with time zone,
+					c12_date date,
+					c13_time time without time zone,
+					c14_timetz time with time zone,
+
+					c15_interval interval
+				);
+			`
+			_, err := sess.Exec(sql)
+			So(err, ShouldBeNil)
+
+			sql = `
+				INSERT INTO postgres_types VALUES(
+					1,2,3,
+					4.5,6.7,1.1,1.2,
+					'char10','varchar10','text',
+
+					now(),now(),now(),now(),now(),'15m'::interval
+				);
+			`
+			_, err = sess.Exec(sql)
+			So(err, ShouldBeNil)
+
+			Convey("When doing a table query should map Postgres column types to Go types", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": "SELECT * FROM postgres_types",
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				column := queryResult.Tables[0].Rows[0]
+				So(column[0].(int64), ShouldEqual, 1)
+				So(column[1].(int64), ShouldEqual, 2)
+				So(column[2].(int64), ShouldEqual, 3)
+
+				So(column[3].(float64), ShouldEqual, 4.5)
+				So(column[4].(float64), ShouldEqual, 6.7)
+				So(column[5].(float64), ShouldEqual, 1.1)
+				So(column[6].(float64), ShouldEqual, 1.2)
+
+				So(column[7].(string), ShouldEqual, "char10    ")
+				So(column[8].(string), ShouldEqual, "varchar10")
+				So(column[9].(string), ShouldEqual, "text")
+
+				So(column[10].(time.Time), ShouldHaveSameTypeAs, time.Now())
+				So(column[11].(time.Time), ShouldHaveSameTypeAs, time.Now())
+				So(column[12].(time.Time), ShouldHaveSameTypeAs, time.Now())
+				So(column[13].(time.Time), ShouldHaveSameTypeAs, time.Now())
+				So(column[14].(time.Time), ShouldHaveSameTypeAs, time.Now())
+
+				So(column[15].(string), ShouldEqual, "00:15:00")
+			})
+		})
+
+		Convey("Given a table with metrics that lacks data for some series ", func() {
+			sql := `
+					DROP TABLE IF EXISTS metric;
+					CREATE TABLE metric (
+						time timestamp,
+						value integer
+					)
+				`
+
+			_, err := sess.Exec(sql)
+			So(err, ShouldBeNil)
+
+			type metric struct {
+				Time  time.Time
+				Value int64
+			}
+
+			series := []*metric{}
+			firstRange := genTimeRangeByInterval(fromStart, 10*time.Minute, 10*time.Second)
+			secondRange := genTimeRangeByInterval(fromStart.Add(20*time.Minute), 10*time.Minute, 10*time.Second)
+
+			for _, t := range firstRange {
+				series = append(series, &metric{
+					Time:  t,
+					Value: 15,
+				})
+			}
+
+			for _, t := range secondRange {
+				series = append(series, &metric{
+					Time:  t,
+					Value: 20,
+				})
+			}
+
+			for _, s := range series {
+				_, err = sess.Insert(s)
+				So(err, ShouldBeNil)
+			}
+
+			Convey("When doing a metric query using timeGroup", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				points := queryResult.Series[0].Points
+				So(len(points), ShouldEqual, 6)
+
+				dt := fromStart
+
+				for i := 0; i < 3; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 15)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
+
+				// adjust for 5 minute gap
+				dt = dt.Add(5 * time.Minute)
+				for i := 3; i < 6; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 20)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
+			})
+
+			Convey("When doing a metric query using timeGroup with NULL fill enabled", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+					TimeRange: &tsdb.TimeRange{
+						From: fmt.Sprintf("%v", fromStart.Unix()*1000),
+						To:   fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				points := queryResult.Series[0].Points
+				So(len(points), ShouldEqual, 7)
+
+				dt := fromStart
+
+				for i := 0; i < 3; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 15)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
+
+				So(points[3][0].Valid, ShouldBeFalse)
+
+				// adjust for 5 minute gap
+				dt = dt.Add(5 * time.Minute)
+				for i := 4; i < 7; i++ {
+					aValue := points[i][0].Float64
+					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
+					So(aValue, ShouldEqual, 20)
+					So(aTime, ShouldEqual, dt)
+					dt = dt.Add(5 * time.Minute)
+				}
+			})
+
+			Convey("When doing a metric query using timeGroup with float fill enabled", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": "SELECT $__timeGroup(time, '5m', 1.5), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+					TimeRange: &tsdb.TimeRange{
+						From: fmt.Sprintf("%v", fromStart.Unix()*1000),
+						To:   fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				points := queryResult.Series[0].Points
+				So(points[3][0].Float64, ShouldEqual, 1.5)
+			})
+		})
+
+		Convey("Given a table with metrics having multiple values and measurements", func() {
+			type metric_values struct {
+				Time        time.Time
+				Measurement string
+				ValueOne    int64 `xorm:"integer 'valueOne'"`
+				ValueTwo    int64 `xorm:"integer 'valueTwo'"`
 			}
 			}
 
 
-			resp, err := endpoint.Query(nil, nil, query)
-			queryResult := resp.Results["A"]
+			if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist {
+				So(err, ShouldBeNil)
+				sess.DropTable(metric_values{})
+			}
+			err := sess.CreateTable(metric_values{})
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			column := queryResult.Tables[0].Rows[0]
-			So(column[0].(int64), ShouldEqual, 1)
-			So(column[1].(int64), ShouldEqual, 2)
-			So(column[2].(int64), ShouldEqual, 3)
-			So(column[3].(float64), ShouldEqual, 4.5)
-			So(column[4].(float64), ShouldEqual, 6.7)
-			// libpq doesnt properly convert decimal, numeric and char to go types but returns []uint8 instead
-			//			So(column[5].(float64), ShouldEqual, 1.1)
-			//			So(column[6].(float64), ShouldEqual, 1.2)
-			//			So(column[7].(string), ShouldEqual, "char")
-			So(column[8].(string), ShouldEqual, "varchar10")
-			So(column[9].(string), ShouldEqual, "text")
-
-			So(column[10].(time.Time), ShouldHaveSameTypeAs, time.Now())
-			So(column[11].(time.Time), ShouldHaveSameTypeAs, time.Now())
-			So(column[12].(time.Time), ShouldHaveSameTypeAs, time.Now())
-			So(column[13].(time.Time), ShouldHaveSameTypeAs, time.Now())
-			So(column[14].(time.Time), ShouldHaveSameTypeAs, time.Now())
-
-			// libpq doesnt properly convert interval to go types but returns []uint8 instead
-			//			So(column[15].(time.Time), ShouldHaveSameTypeAs, time.Now())
+			rand.Seed(time.Now().Unix())
+			rnd := func(min, max int64) int64 {
+				return rand.Int63n(max-min) + min
+			}
+
+			series := []*metric_values{}
+			for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
+				series = append(series, &metric_values{
+					Time:        t,
+					Measurement: "Metric A",
+					ValueOne:    rnd(0, 100),
+					ValueTwo:    rnd(0, 100),
+				})
+				series = append(series, &metric_values{
+					Time:        t,
+					Measurement: "Metric B",
+					ValueOne:    rnd(0, 100),
+					ValueTwo:    rnd(0, 100),
+				})
+			}
+
+			for _, s := range series {
+				_, err := sess.Insert(s)
+				So(err, ShouldBeNil)
+			}
+
+			Convey("When doing a metric query grouping by time and select metric column should return correct series", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT $__timeEpoch(time), measurement || ' - value one' as metric, "valueOne" FROM metric_values ORDER BY 1`,
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				So(len(queryResult.Series), ShouldEqual, 2)
+				So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one")
+				So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one")
+			})
+
+			Convey("When doing a metric query grouping by time should return correct series", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT $__timeEpoch(time), "valueOne", "valueTwo" FROM metric_values ORDER BY 1`,
+								"format": "time_series",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+
+				So(len(queryResult.Series), ShouldEqual, 2)
+				So(queryResult.Series[0].Name, ShouldEqual, "valueOne")
+				So(queryResult.Series[1].Name, ShouldEqual, "valueTwo")
+			})
+		})
+
+		Convey("Given a table with event data", func() {
+			type event struct {
+				TimeSec     int64
+				Description string
+				Tags        string
+			}
+
+			if exist, err := sess.IsTableExist(event{}); err != nil || exist {
+				So(err, ShouldBeNil)
+				sess.DropTable(event{})
+			}
+			err := sess.CreateTable(event{})
+			So(err, ShouldBeNil)
+
+			events := []*event{}
+			for _, t := range genTimeRangeByInterval(fromStart.Add(-20*time.Minute), 60*time.Minute, 25*time.Minute) {
+				events = append(events, &event{
+					TimeSec:     t.Unix(),
+					Description: "Someone deployed something",
+					Tags:        "deploy",
+				})
+				events = append(events, &event{
+					TimeSec:     t.Add(5 * time.Minute).Unix(),
+					Description: "New support ticket registered",
+					Tags:        "ticket",
+				})
+			}
+
+			for _, e := range events {
+				_, err = sess.Insert(e)
+				So(err, ShouldBeNil)
+			}
+
+			Convey("When doing an annotation query of deploy events should return expected result", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC`,
+								"format": "table",
+							}),
+							RefId: "Deploys",
+						},
+					},
+					TimeRange: &tsdb.TimeRange{
+						From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000),
+						To:   fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000),
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				queryResult := resp.Results["Deploys"]
+				So(err, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 3)
+			})
+
+			Convey("When doing an annotation query of ticket events should return expected result", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC`,
+								"format": "table",
+							}),
+							RefId: "Tickets",
+						},
+					},
+					TimeRange: &tsdb.TimeRange{
+						From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000),
+						To:   fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000),
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				queryResult := resp.Results["Tickets"]
+				So(err, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 3)
+			})
+
+			Convey("When doing an annotation query with a time column in datetime format", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
+				dtFormat := "2006-01-02 15:04:05.999999999"
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									CAST('%s' AS TIMESTAMP) as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Format(dtFormat)),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000))
+			})
+
+			Convey("When doing an annotation query with a time column in epoch second format should return ms", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									 %d as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Unix()),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
+			})
+
+			Convey("When doing an annotation query with a time column in epoch second format (int) should return ms", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									 cast(%d as bigint) as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Unix()),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
+			})
+
+			Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() {
+				dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
+
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": fmt.Sprintf(`SELECT
+									 %d as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`, dt.Unix()*1000),
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0].(int64), ShouldEqual, dt.Unix()*1000)
+			})
+
+			Convey("When doing an annotation query with a time column holding a bigint null value should return nil", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT
+									 cast(null as bigint) as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`,
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0], ShouldBeNil)
+			})
+
+			Convey("When doing an annotation query with a time column holding a timestamp null value should return nil", func() {
+				query := &tsdb.TsdbQuery{
+					Queries: []*tsdb.Query{
+						{
+							Model: simplejson.NewFromAny(map[string]interface{}{
+								"rawSql": `SELECT
+									 cast(null as timestamp) as time,
+									'message' as text,
+									'tag1,tag2' as tags
+								`,
+								"format": "table",
+							}),
+							RefId: "A",
+						},
+					},
+				}
+
+				resp, err := endpoint.Query(nil, nil, query)
+				So(err, ShouldBeNil)
+				queryResult := resp.Results["A"]
+				So(queryResult.Error, ShouldBeNil)
+				So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
+				columns := queryResult.Tables[0].Rows[0]
+
+				//Should be in milliseconds
+				So(columns[0], ShouldBeNil)
+			})
 		})
 		})
 	})
 	})
 }
 }
 
 
 func InitPostgresTestDB(t *testing.T) *xorm.Engine {
 func InitPostgresTestDB(t *testing.T) *xorm.Engine {
 	x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
 	x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
+	x.DatabaseTZ = time.UTC
+	x.TZLocation = time.UTC
 
 
 	// x.ShowSQL()
 	// x.ShowSQL()
 
 
@@ -119,7 +636,18 @@ func InitPostgresTestDB(t *testing.T) *xorm.Engine {
 		t.Fatalf("Failed to init postgres db %v", err)
 		t.Fatalf("Failed to init postgres db %v", err)
 	}
 	}
 
 
-	sqlutil.CleanDB(x)
-
 	return x
 	return x
 }
 }
+
+func genTimeRangeByInterval(from time.Time, duration time.Duration, interval time.Duration) []time.Time {
+	durationSec := int64(duration.Seconds())
+	intervalSec := int64(interval.Seconds())
+	timeRange := []time.Time{}
+
+	for i := int64(0); i < durationSec; i += intervalSec {
+		timeRange = append(timeRange, from)
+		from = from.Add(time.Duration(int64(time.Second) * intervalSec))
+	}
+
+	return timeRange
+}

+ 28 - 0
pkg/tsdb/sql_engine.go

@@ -3,6 +3,7 @@ package tsdb
 import (
 import (
 	"context"
 	"context"
 	"sync"
 	"sync"
+	"time"
 
 
 	"github.com/go-xorm/core"
 	"github.com/go-xorm/core"
 	"github.com/go-xorm/xorm"
 	"github.com/go-xorm/xorm"
@@ -133,3 +134,30 @@ func (e *DefaultSqlEngine) Query(
 
 
 	return result, nil
 	return result, nil
 }
 }
+
+// ConvertTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds
+// to make native datetime types and epoch dates work in annotation and table queries.
+func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) {
+	if timeIndex >= 0 {
+		switch value := values[timeIndex].(type) {
+		case time.Time:
+			values[timeIndex] = EpochPrecisionToMs(float64(value.Unix()))
+		case *time.Time:
+			if value != nil {
+				values[timeIndex] = EpochPrecisionToMs(float64((*value).Unix()))
+			}
+		case int64:
+			values[timeIndex] = int64(EpochPrecisionToMs(float64(value)))
+		case *int64:
+			if value != nil {
+				values[timeIndex] = int64(EpochPrecisionToMs(float64(*value)))
+			}
+		case float64:
+			values[timeIndex] = EpochPrecisionToMs(value)
+		case *float64:
+			if value != nil {
+				values[timeIndex] = EpochPrecisionToMs(*value)
+			}
+		}
+	}
+}

+ 46 - 0
pkg/tsdb/sql_engine_test.go

@@ -0,0 +1,46 @@
+package tsdb
+
+import (
+	"testing"
+	"time"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestSqlEngine(t *testing.T) {
+	Convey("SqlEngine", t, func() {
+		Convey("Given row values with time columns when converting them", func() {
+			dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
+			fixtures := make([]interface{}, 8)
+			fixtures[0] = dt
+			fixtures[1] = dt.Unix() * 1000
+			fixtures[2] = dt.Unix()
+			fixtures[3] = float64(dt.Unix() * 1000)
+			fixtures[4] = float64(dt.Unix())
+
+			var nilDt *time.Time
+			var nilInt64 *int64
+			var nilFloat64 *float64
+			fixtures[5] = nilDt
+			fixtures[6] = nilInt64
+			fixtures[7] = nilFloat64
+
+			for i := range fixtures {
+				ConvertSqlTimeColumnToEpochMs(fixtures, i)
+			}
+
+			Convey("Should convert sql time columns to epoch time in ms ", func() {
+				expected := float64(dt.Unix() * 1000)
+				So(fixtures[0].(float64), ShouldEqual, expected)
+				So(fixtures[1].(int64), ShouldEqual, expected)
+				So(fixtures[2].(int64), ShouldEqual, expected)
+				So(fixtures[3].(float64), ShouldEqual, expected)
+				So(fixtures[4].(float64), ShouldEqual, expected)
+
+				So(fixtures[5], ShouldBeNil)
+				So(fixtures[6], ShouldBeNil)
+				So(fixtures[7], ShouldBeNil)
+			})
+		})
+	})
+}

+ 10 - 0
pkg/tsdb/time_range.go

@@ -88,3 +88,13 @@ func (tr *TimeRange) ParseTo() (time.Time, error) {
 
 
 	return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To)
 	return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To)
 }
 }
+
+// EpochPrecisionToMs converts epoch precision to millisecond, if needed.
+// Only seconds to milliseconds supported right now
+func EpochPrecisionToMs(value float64) float64 {
+	if int64(value)/1e10 == 0 {
+		return float64(value * 1e3)
+	}
+
+	return float64(value)
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов