diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5398481b198..bd7c3fad139 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,23 +1,27 @@
# 4.1-beta (unreleased)
+### Enhancements
+* **Postgres**: Add support for Certs for Postgres database [#6655](https://github.com/grafana/grafana/issues/6655)
+* **Victorops**: Add VictorOps notification integration [#6411](https://github.com/grafana/grafana/issues/6411)
+* **Opsgenie**: Add OpsGenie notification integratiion (by [@kylemcc](https://github.com/kylemcc)) [#6687](https://github.com/grafana/grafana/issues/6687)
+* **Singlestat**: New aggregation on singlestat panel [#6740](https://github.com/grafana/grafana/pull/6740)
+* **Cloudwatch**: Make it possible to specify access and secret key on the data source config page [#6697](https://github.com/grafana/grafana/issues/6697)
+* **Table**: Added Hidden Column Style for Table Panel [#5677](https://github.com/grafana/grafana/pull/5677)
+* **Graph**: Shared crosshair option renamed to shared tooltip, shows tooltip on all graphs as you hover over one graph. [#1578](https://github.com/grafana/grafana/pull/1578), [#6274](https://github.com/grafana/grafana/pull/6274)
+* **Elasticsearch**: Added support for Missing option (bucket) for terms aggregation [#4244](https://github.com/grafana/grafana/pull/4244), thx @shanielh
+
### Bugfixes
* **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679)
* **Dashboard**: Posting empty dashboard result in corrupted dashboard [#5443](https://github.com/grafana/grafana/issues/5443)
-### Enhancements
-* **Postgres**: Add support for Certs for Postgres database [#6655](https://github.com/grafana/grafana/issues/6655)
-* **Victorops**: Add VictorOps Notification Integration [#6411](https://github.com/grafana/grafana/issues/6411)
-* **Singlestat**: New aggregation on singlestat panel [#6740](https://github.com/grafana/grafana/pull/6740)
-* **Cloudwatch**: Make it possible to specify access and secret key on the data source config page [#6697](https://github.com/grafana/grafana/issues/6697)
-* **Table**: Added Hidden Column Style for Table Panel [#5677](https://github.com/grafana/grafana/pull/5677)
-
-# 4.0.2 (unreleased)
+# 4.0.2 (2016-12-08)
### Enhancements
* **Playlist**: Add support for kiosk mode [#6727](https://github.com/grafana/grafana/issues/6727)
### Bugfixes
* **Alerting**: Add alert message to webhook notifications [#6807](https://github.com/grafana/grafana/issues/6807)
+* **Alerting**: Fixes a bug where avg() reducer treated null as zero. [#6879](https://github.com/grafana/grafana/issues/6879)
* **PNG Rendering**: Fix for server side rendering when using non default http addr bind and domain setting [#6813](https://github.com/grafana/grafana/issues/6813)
* **PNG Rendering**: Fix for server side rendering when setting enforce_domain to true [#6769](https://github.com/grafana/grafana/issues/6769)
* **Webhooks**: Add content type json to outgoing webhooks [#6822](https://github.com/grafana/grafana/issues/6822)
diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md
index dfe84f142ec..1411bcf756f 100644
--- a/docs/sources/alerting/notifications.md
+++ b/docs/sources/alerting/notifications.md
@@ -91,7 +91,7 @@ Auto resolve incidents | Resolve incidents in pagerduty once the alert goes back
# Enable images in notifications {#external-image-store}
-Grafan can render the panel associated with the alert rule and include that in the notification. Some types
+Grafana can render the panel associated with the alert rule and include that in the notification. Some types
of notifications require that this image be publicly accessable (Slack for example). In order to support
images in notifications like Slack Grafana can upload the image to an image store. It currently supports
Amazon S3 for this and Webdav. So to set that up you need to configure the
diff --git a/docs/sources/http_api/org.md b/docs/sources/http_api/org.md
index 340d97438ae..72c995adedf 100644
--- a/docs/sources/http_api/org.md
+++ b/docs/sources/http_api/org.md
@@ -93,11 +93,11 @@ parent = "http_api"
## Create Organisation
-`POST /api/org`
+`POST /api/orgs`
**Example Request**:
- POST /api/org HTTP/1.1
+ POST /api/orgs HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md
index ab51e0ac124..903e03228ed 100644
--- a/docs/sources/installation/debian.md
+++ b/docs/sources/installation/debian.md
@@ -14,14 +14,14 @@ weight = 1
Description | Download
------------ | -------------
-Stable for Debian-based Linux | [4.0.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.1-1480694114_amd64.deb)
+Stable for Debian-based Linux | [4.0.2 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.2-1481203731_amd64.deb)
## Install Stable
```
-$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.1-1480694114_amd64.deb
+$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.2-1481203731_amd64.deb
$ sudo apt-get install -y adduser libfontconfig
-$ sudo dpkg -i grafana_4.0.1-1480694114_amd64.deb
+$ sudo dpkg -i grafana_4.0.2-1481203731_amd64.deb
```
## APT Repository
diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md
index 370bac44cf2..8ccaa43bc79 100644
--- a/docs/sources/installation/rpm.md
+++ b/docs/sources/installation/rpm.md
@@ -14,24 +14,24 @@ weight = 2
Description | Download
------------ | -------------
-Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.0.1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.1-1480694114.x86_64.rpm)
+Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.0.2 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm)
## Install Stable
You can install Grafana using Yum directly.
- $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.1-1480694114.x86_64.rpm
+ $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm
Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat:
$ sudo yum install initscripts fontconfig
- $ sudo rpm -Uvh grafana-4.0.1-1480694114.x86_64.rpm
+ $ sudo rpm -Uvh grafana-4.0.2-1481203731.x86_64.rpm
#### On OpenSuse:
- $ sudo rpm -i --nodeps grafana-4.0.1-1480694114.x86_64.rpm
+ $ sudo rpm -i --nodeps grafana-4.0.2-1481203731.x86_64.rpm
## Install via YUM Repository
diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md
index fc1495ec311..5217181f1ee 100644
--- a/docs/sources/installation/windows.md
+++ b/docs/sources/installation/windows.md
@@ -13,7 +13,7 @@ weight = 3
Description | Download
------------ | -------------
-Latest stable package for Windows | [grafana.4.0.1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.1.windows-x64.zip)
+Latest stable package for Windows | [grafana.4.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2.windows-x64.zip)
## Configure
diff --git a/docs/sources/reference/export_import.md b/docs/sources/reference/export_import.md
index 7430f4891a6..bc05795c030 100644
--- a/docs/sources/reference/export_import.md
+++ b/docs/sources/reference/export_import.md
@@ -9,7 +9,7 @@ weight = 8
# Export and Import
-Grafana Dashboads can easily be exported and imported, either from the UI or from the HTTP API.
+Grafana Dashboards can easily be exported and imported, either from the UI or from the HTTP API.
## Exporting a dashboard
@@ -22,9 +22,9 @@ The export feature is accessed from the share menu.
### Making a dashboard portable
If you want to export a dashboard for others to use then it could be a good idea to
-add template variables for things like a metric prefix (use contant variable) and server name.
+add template variables for things like a metric prefix (use constant variable) and server name.
-A template varible of the type `Constant` will automatically be hidden in
+A template variable of the type `Constant` will automatically be hidden in
the dashboard, and will also be added as an required input when the dashboard is imported.
## Importing a dashboard
@@ -43,7 +43,7 @@ data source you want the dashboard to use and specify any metric prefixes (if th
## Discover dashboards on Grafana.net
-Find dashboads for common server applications at [Grafana.net/dashboards](https://grafana.net/dashboards).
+Find dashboards for common server applications at [Grafana.net/dashboards](https://grafana.net/dashboards).
diff --git a/docs/sources/reference/singlestat.md b/docs/sources/reference/singlestat.md
index 1fb0c254c01..ed0a956b7eb 100644
--- a/docs/sources/reference/singlestat.md
+++ b/docs/sources/reference/singlestat.md
@@ -1,5 +1,5 @@
+++
-title = "Singletat Panel"
+title = "Singlestat Panel"
keywords = ["grafana", "dashboard", "documentation", "panels", "singlestat"]
type = "docs"
[menu.docs]
diff --git a/latest.json b/latest.json
index dd8e0ce33da..78fd48769e8 100644
--- a/latest.json
+++ b/latest.json
@@ -1,4 +1,4 @@
{
- "stable": "4.0.1",
- "testing": "4.0.1"
+ "stable": "4.0.2",
+ "testing": "4.0.2"
}
diff --git a/packaging/publish/publish.sh b/packaging/publish/publish.sh
index 7619dd2e0f4..57adcbd3481 100755
--- a/packaging/publish/publish.sh
+++ b/packaging/publish/publish.sh
@@ -1,6 +1,6 @@
#! /usr/bin/env bash
-deb_ver=4.0.0-1480439068
-rpm_ver=4.0.0-1480439068
+deb_ver=4.0.2-1481203731
+rpm_ver=4.0.2-1481203731
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 4fa28f799b0..90e0e1bdfa8 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -113,6 +113,9 @@ func Register(r *macaron.Macaron) {
r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
r.Get("/quotas", wrap(GetUserQuotas))
+ r.Put("/helpflags/:id", wrap(SetHelpFlag))
+ // For dev purpose
+ r.Get("/helpflags/clear", wrap(ClearHelpFlags))
r.Get("/preferences", wrap(GetUserPreferences))
r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
@@ -193,7 +196,7 @@ func Register(r *macaron.Macaron) {
r.Group("/datasources", func() {
r.Get("/", GetDataSources)
r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
- r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource)
+ r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
r.Delete("/:id", DeleteDataSource)
r.Get("/:id", wrap(GetDataSourceById))
r.Get("/name/:name", wrap(GetDataSourceByName))
diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go
index 0ed77fe982e..55925c4faf6 100644
--- a/pkg/api/dashboard.go
+++ b/pkg/api/dashboard.go
@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
@@ -216,9 +217,28 @@ func GetHomeDashboard(c *middleware.Context) Response {
return ApiError(500, "Failed to load home dashboard", err)
}
+ if c.HasUserRole(m.ROLE_ADMIN) && !c.HasHelpFlag(m.HelpFlagGettingStartedPanelDismissed) {
+ addGettingStartedPanelToHomeDashboard(dash.Dashboard)
+ }
+
return Json(200, &dash)
}
+func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
+ rows := dash.Get("rows").MustArray()
+ row := simplejson.NewFromAny(rows[0])
+
+ newpanel := simplejson.NewFromAny(map[string]interface{}{
+ "type": "gettingstarted",
+ "id": 123123,
+ "span": 12,
+ })
+
+ panels := row.Get("panels").MustArray()
+ panels = append(panels, newpanel)
+ row.Set("panels", panels)
+}
+
func GetDashboardFromJsonFile(c *middleware.Context) {
file := c.Params(":file")
diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go
index 39de6911008..db4c5166feb 100644
--- a/pkg/api/dataproxy.go
+++ b/pkg/api/dataproxy.go
@@ -1,13 +1,9 @@
package api
import (
- "crypto/tls"
- "crypto/x509"
- "net"
"net/http"
"net/http/httputil"
"net/url"
- "sync"
"time"
"github.com/grafana/grafana/pkg/api/cloudwatch"
@@ -19,75 +15,6 @@ import (
"github.com/grafana/grafana/pkg/util"
)
-type proxyTransportCache struct {
- cache map[int64]cachedTransport
- sync.Mutex
-}
-
-type cachedTransport struct {
- updated time.Time
-
- *http.Transport
-}
-
-var ptc = proxyTransportCache{
- cache: make(map[int64]cachedTransport),
-}
-
-func DataProxyTransport(ds *m.DataSource) (*http.Transport, error) {
- ptc.Lock()
- defer ptc.Unlock()
-
- if t, present := ptc.cache[ds.Id]; present && ds.Updated.Equal(t.updated) {
- return t.Transport, nil
- }
-
- transport := &http.Transport{
- TLSClientConfig: &tls.Config{
- InsecureSkipVerify: true,
- },
- Proxy: http.ProxyFromEnvironment,
- Dial: (&net.Dialer{
- Timeout: 30 * time.Second,
- KeepAlive: 30 * time.Second,
- }).Dial,
- TLSHandshakeTimeout: 10 * time.Second,
- }
-
- var tlsAuth, tlsAuthWithCACert bool
- if ds.JsonData != nil {
- tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
- tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
- }
-
- if tlsAuth {
- transport.TLSClientConfig.InsecureSkipVerify = false
-
- decrypted := ds.SecureJsonData.Decrypt()
-
- if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
- caPool := x509.NewCertPool()
- ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
- if ok {
- transport.TLSClientConfig.RootCAs = caPool
- }
- }
-
- cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
- if err != nil {
- return nil, err
- }
- transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
- }
-
- ptc.cache[ds.Id] = cachedTransport{
- Transport: transport,
- updated: ds.Updated,
- }
-
- return transport, nil
-}
-
func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
director := func(req *http.Request) {
req.URL.Scheme = targetUrl.Scheme
@@ -189,7 +116,7 @@ func ProxyDataSourceRequest(c *middleware.Context) {
}
proxy := NewReverseProxy(ds, proxyPath, targetUrl)
- proxy.Transport, err = DataProxyTransport(ds)
+ proxy.Transport, err = ds.GetHttpTransport()
if err != nil {
c.JsonApiErr(400, "Unable to load TLS certificate", err)
return
diff --git a/pkg/api/dataproxy_test.go b/pkg/api/dataproxy_test.go
index 50abf317e8a..f3ed6994cff 100644
--- a/pkg/api/dataproxy_test.go
+++ b/pkg/api/dataproxy_test.go
@@ -4,24 +4,18 @@ import (
"net/http"
"net/url"
"testing"
- "time"
. "github.com/smartystreets/goconvey/convey"
- "github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/setting"
- "github.com/grafana/grafana/pkg/util"
)
func TestDataSourceProxy(t *testing.T) {
-
Convey("When getting graphite datasource proxy", t, func() {
- clearCache()
ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
targetUrl, err := url.Parse(ds.Url)
proxy := NewReverseProxy(&ds, "/render", targetUrl)
- proxy.Transport, err = DataProxyTransport(&ds)
+ proxy.Transport, err = ds.GetHttpTransport()
So(err, ShouldBeNil)
transport, ok := proxy.Transport.(*http.Transport)
@@ -40,7 +34,6 @@ func TestDataSourceProxy(t *testing.T) {
})
Convey("When getting influxdb datasource proxy", t, func() {
- clearCache()
ds := m.DataSource{
Type: m.DS_INFLUXDB_08,
Url: "http://influxdb:8083",
@@ -67,148 +60,4 @@ func TestDataSourceProxy(t *testing.T) {
So(queryVals["p"][0], ShouldEqual, "password")
})
})
-
- Convey("When caching a datasource proxy", t, func() {
- clearCache()
- ds := m.DataSource{
- Id: 1,
- Url: "http://k8s:8001",
- Type: "Kubernetes",
- }
-
- t1, err := DataProxyTransport(&ds)
- So(err, ShouldBeNil)
-
- t2, err := DataProxyTransport(&ds)
- So(err, ShouldBeNil)
-
- Convey("Should be using the cached proxy", func() {
- So(t2, ShouldEqual, t1)
- })
- })
-
- Convey("When getting kubernetes datasource proxy", t, func() {
- clearCache()
- setting.SecretKey = "password"
-
- json := simplejson.New()
- json.Set("tlsAuth", true)
- json.Set("tlsAuthWithCACert", true)
-
- t := time.Now()
- ds := m.DataSource{
- Url: "http://k8s:8001",
- Type: "Kubernetes",
- Updated: t.Add(-2 * time.Minute),
- }
-
- transport, err := DataProxyTransport(&ds)
- So(err, ShouldBeNil)
-
- Convey("Should have no cert", func() {
- So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
- })
-
- ds.JsonData = json
- ds.SecureJsonData = map[string][]byte{
- "tlsCACert": util.Encrypt([]byte(caCert), "password"),
- "tlsClientCert": util.Encrypt([]byte(clientCert), "password"),
- "tlsClientKey": util.Encrypt([]byte(clientKey), "password"),
- }
- ds.Updated = t.Add(-1 * time.Minute)
-
- transport, err = DataProxyTransport(&ds)
- So(err, ShouldBeNil)
-
- Convey("Should add cert", func() {
- So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
- So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1)
- })
-
- ds.JsonData = nil
- ds.SecureJsonData = map[string][]byte{}
- ds.Updated = t
-
- transport, err = DataProxyTransport(&ds)
- So(err, ShouldBeNil)
-
- Convey("Should remove cert", func() {
- So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
- So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 0)
- })
- })
-
}
-
-func clearCache() {
- ptc.Lock()
- defer ptc.Unlock()
-
- ptc.cache = make(map[int64]cachedTransport)
-}
-
-const caCert string = `-----BEGIN CERTIFICATE-----
-MIIDATCCAemgAwIBAgIJAMQ5hC3CPDTeMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
-BAMMDGNhLWs4cy1zdGhsbTAeFw0xNjEwMjcwODQyMjdaFw00NDAzMTQwODQyMjda
-MBcxFTATBgNVBAMMDGNhLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
-ADCCAQoCggEBAMLe2AmJ6IleeUt69vgNchOjjmxIIxz5sp1vFu94m1vUip7CqnOg
-QkpUsHeBPrGYv8UGloARCL1xEWS+9FVZeXWQoDmbC0SxXhFwRIESNCET7Q8KMi/4
-4YPvnMLGZi3Fjwxa8BdUBCN1cx4WEooMVTWXm7RFMtZgDfuOAn3TNXla732sfT/d
-1HNFrh48b0wA+HhmA3nXoBnBEblA665hCeo7lIAdRr0zJxJpnFnWXkyTClsAUTMN
-iL905LdBiiIRenojipfKXvMz88XSaWTI7JjZYU3BvhyXndkT6f12cef3I96NY3WJ
-0uIK4k04WrbzdYXMU3rN6NqlvbHqnI+E7aMCAwEAAaNQME4wHQYDVR0OBBYEFHHx
-2+vSPw9bECHj3O51KNo5VdWOMB8GA1UdIwQYMBaAFHHx2+vSPw9bECHj3O51KNo5
-VdWOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH2eV5NcV3LBJHs9
-I+adbiTPg2vyumrGWwy73T0X8Dtchgt8wU7Q9b9Ucg2fOTmSSyS0iMqEu1Yb2ORB
-CknM9mixHC9PwEBbkGCom3VVkqdLwSP6gdILZgyLoH4i8sTUz+S1yGPepi+Vzhs7
-adOXtryjcGnwft6HdfKPNklMOHFnjw6uqpho54oj/z55jUpicY/8glDHdrr1bh3k
-MHuiWLGewHXPvxfG6UoUx1te65IhifVcJGFZDQwfEmhBflfCmtAJlZEsgTLlBBCh
-FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n
-3lb92xM=
------END CERTIFICATE-----`
-
-const clientCert string = `-----BEGIN CERTIFICATE-----
-MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
-YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
-GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD
-ggEPADCCAQoCggEBAOMliaWyNEUJKM37vWCl5bGub3lMicyRAqGQyY/qxD9yKKM2
-FbucVcmWmg5vvTqQVl5rlQ+c7GI8OD6ptmFl8a26coEki7bFr8bkpSyBSEc5p27b
-Z0ORFSqBHWHQbr9PkxPLYW6T3gZYUtRYv3OQgGxLXlvUh85n/mQfuR3N1FgmShHo
-GtAFi/ht6leXa0Ms+jNSDLCmXpJm1GIEqgyKX7K3+g3vzo9coYqXq4XTa8Efs2v8
-SCwqWfBC3rHfgs/5DLB8WT4Kul8QzxkytzcaBQfRfzhSV6bkgm7oTzt2/1eRRsf4
-YnXzLE9YkCC9sAn+Owzqf+TYC1KRluWDfqqBTJUCAwEAATANBgkqhkiG9w0BAQsF
-AAOCAQEAdMsZg6edWGC+xngizn0uamrUg1ViaDqUsz0vpzY5NWLA4MsBc4EtxWRP
-ueQvjUimZ3U3+AX0YWNLIrH1FCVos2jdij/xkTUmHcwzr8rQy+B17cFi+a8jtpgw
-AU6WWoaAIEhhbWQfth/Diz3mivl1ARB+YqiWca2mjRPLTPcKJEURDVddQ423el0Q
-4JNxS5icu7T2zYTYHAo/cT9zVdLZl0xuLxYm3asK1IONJ/evxyVZima3il6MPvhe
-58Hwz+m+HdqHxi24b/1J/VKYbISG4huOQCdLzeNXgvwFlGPUmHSnnKo1/KbQDAR5
-llG/Sw5+FquFuChaA6l5KWy7F3bQyA==
------END CERTIFICATE-----`
-
-const clientKey string = `-----BEGIN RSA PRIVATE KEY-----
-MIIEpQIBAAKCAQEA4yWJpbI0RQkozfu9YKXlsa5veUyJzJECoZDJj+rEP3IoozYV
-u5xVyZaaDm+9OpBWXmuVD5zsYjw4Pqm2YWXxrbpygSSLtsWvxuSlLIFIRzmnbttn
-Q5EVKoEdYdBuv0+TE8thbpPeBlhS1Fi/c5CAbEteW9SHzmf+ZB+5Hc3UWCZKEega
-0AWL+G3qV5drQyz6M1IMsKZekmbUYgSqDIpfsrf6De/Oj1yhiperhdNrwR+za/xI
-LCpZ8ELesd+Cz/kMsHxZPgq6XxDPGTK3NxoFB9F/OFJXpuSCbuhPO3b/V5FGx/hi
-dfMsT1iQIL2wCf47DOp/5NgLUpGW5YN+qoFMlQIDAQABAoIBAQCzy4u312XeW1Cs
-Mx6EuOwmh59/ESFmBkZh4rxZKYgrfE5EWlQ7i5SwG4BX+wR6rbNfy6JSmHDXlTkk
-CKvvToVNcW6fYHEivDnVojhIERFIJ4+rhQmpBtcNLOQ3/4cZ8X/GxE6b+3lb5l+x
-64mnjPLKRaIr5/+TVuebEy0xNTJmjnJ7yiB2HRz7uXEQaVSk/P7KAkkyl/9J3/LM
-8N9AX1w6qDaNQZ4/P0++1H4SQenosM/b/GqGTomarEk/GE0NcB9rzmR9VCXa7FRh
-WV5jyt9vUrwIEiK/6nUnOkGO8Ei3kB7Y+e+2m6WdaNoU5RAfqXmXa0Q/a0lLRruf
-vTMo2WrBAoGBAPRaK4cx76Q+3SJ/wfznaPsMM06OSR8A3ctKdV+ip/lyKtb1W8Pz
-k8MYQDH7GwPtSu5QD8doL00pPjugZL/ba7X9nAsI+pinyEErfnB9y7ORNEjIYYzs
-DiqDKup7ANgw1gZvznWvb9Ge0WUSXvWS0pFkgootQAf+RmnnbWGH6l6RAoGBAO35
-aGUrLro5u9RD24uSXNU3NmojINIQFK5dHAT3yl0BBYstL43AEsye9lX95uMPTvOQ
-Cqcn42Hjp/bSe3n0ObyOZeXVrWcDFAfE0wwB1BkvL1lpgnFO9+VQORlH4w3Ppnpo
-jcPkR2TFeDaAYtvckhxe/Bk3OnuFmnsQ3VzM75fFAoGBAI6PvS2XeNU+yA3EtA01
-hg5SQ+zlHswz2TMuMeSmJZJnhY78f5mHlwIQOAPxGQXlf/4iP9J7en1uPpzTK3S0
-M9duK4hUqMA/w5oiIhbHjf0qDnMYVbG+V1V+SZ+cPBXmCDihKreGr5qBKnHpkfV8
-v9WL6o1rcRw4wiQvnaV1gsvBAoGBALtzVTczr6gDKCAIn5wuWy+cQSGTsBunjRLX
-xuVm5iEiV+KMYkPvAx/pKzMLP96lRVR3ptyKgAKwl7LFk3u50+zh4gQLr35QH2wL
-Lw7rNc3srAhrItPsFzqrWX6/cGuFoKYVS239l/sZzRppQPXcpb7xVvTp2whHcir0
-Wtnpl+TdAoGAGqKqo2KU3JoY3IuTDUk1dsNAm8jd9EWDh+s1x4aG4N79mwcss5GD
-FF8MbFPneK7xQd8L6HisKUDAUi2NOyynM81LAftPkvN6ZuUVeFDfCL4vCA0HUXLD
-+VrOhtUZkNNJlLMiVRJuQKUOGlg8PpObqYbstQAf/0/yFJMRHG82Tcg=
------END RSA PRIVATE KEY-----`
diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go
index cc305221817..f0c022c1622 100644
--- a/pkg/api/datasources.go
+++ b/pkg/api/datasources.go
@@ -5,10 +5,9 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/plugins"
- //"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util"
)
@@ -118,7 +117,7 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp
return ApiError(500, "Failed to update datasource", err)
}
- return Json(200, "Datasource updated")
+ return Json(200, util.DynMap{"message": "Datasource updated"})
}
func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go
index 17ac39fa344..e9efd1b7c01 100644
--- a/pkg/api/dtos/models.go
+++ b/pkg/api/dtos/models.go
@@ -22,19 +22,20 @@ type LoginCommand struct {
}
type CurrentUser struct {
- IsSignedIn bool `json:"isSignedIn"`
- Id int64 `json:"id"`
- Login string `json:"login"`
- Email string `json:"email"`
- Name string `json:"name"`
- LightTheme bool `json:"lightTheme"`
- OrgId int64 `json:"orgId"`
- OrgName string `json:"orgName"`
- OrgRole m.RoleType `json:"orgRole"`
- IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
- GravatarUrl string `json:"gravatarUrl"`
- Timezone string `json:"timezone"`
- Locale string `json:"locale"`
+ IsSignedIn bool `json:"isSignedIn"`
+ Id int64 `json:"id"`
+ Login string `json:"login"`
+ Email string `json:"email"`
+ Name string `json:"name"`
+ LightTheme bool `json:"lightTheme"`
+ OrgId int64 `json:"orgId"`
+ OrgName string `json:"orgName"`
+ OrgRole m.RoleType `json:"orgRole"`
+ IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
+ GravatarUrl string `json:"gravatarUrl"`
+ Timezone string `json:"timezone"`
+ Locale string `json:"locale"`
+ HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
}
type DashboardMeta struct {
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index 384dd7f0ea1..a3dd9f9ce61 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -122,12 +122,13 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
panels := map[string]interface{}{}
for _, panel := range enabledPlugins.Panels {
panels[panel.Id] = map[string]interface{}{
- "module": panel.Module,
- "baseUrl": panel.BaseUrl,
- "name": panel.Name,
- "id": panel.Id,
- "info": panel.Info,
- "sort": getPanelSort(panel.Id),
+ "module": panel.Module,
+ "baseUrl": panel.BaseUrl,
+ "name": panel.Name,
+ "id": panel.Id,
+ "info": panel.Info,
+ "hideFromList": panel.HideFromList,
+ "sort": getPanelSort(panel.Id),
}
}
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 99a5f78f9c9..5bc4344a8ba 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -58,6 +58,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
LightTheme: prefs.Theme == "light",
Timezone: prefs.Timezone,
Locale: locale,
+ HelpFlags1: c.HelpFlags1,
},
Settings: settings,
AppUrl: appUrl,
diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go
index 5516bdd52a5..dad3b159ad6 100644
--- a/pkg/api/metrics.go
+++ b/pkg/api/metrics.go
@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/tsdb/testdata"
"github.com/grafana/grafana/pkg/util"
@@ -25,9 +26,9 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
IntervalMs: query.Get("intervalMs").MustInt64(1000),
Model: query,
- DataSource: &tsdb.DataSourceInfo{
- Name: "Grafana TestDataDB",
- PluginId: "grafana-testdata-datasource",
+ DataSource: &models.DataSource{
+ Name: "Grafana TestDataDB",
+ Type: "grafana-testdata-datasource",
},
})
}
diff --git a/pkg/api/user.go b/pkg/api/user.go
index f98eec02c40..715103aacbd 100644
--- a/pkg/api/user.go
+++ b/pkg/api/user.go
@@ -180,3 +180,34 @@ func SearchUsers(c *middleware.Context) Response {
return Json(200, query.Result)
}
+
+func SetHelpFlag(c *middleware.Context) Response {
+ flag := c.ParamsInt64(":id")
+
+ bitmask := &c.HelpFlags1
+ bitmask.AddFlag(m.HelpFlags1(flag))
+
+ cmd := m.SetUserHelpFlagCommand{
+ UserId: c.UserId,
+ HelpFlags1: *bitmask,
+ }
+
+ if err := bus.Dispatch(&cmd); err != nil {
+ return ApiError(500, "Failed to update help flag", err)
+ }
+
+ return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
+}
+
+func ClearHelpFlags(c *middleware.Context) Response {
+ cmd := m.SetUserHelpFlagCommand{
+ UserId: c.UserId,
+ HelpFlags1: m.HelpFlags1(0),
+ }
+
+ if err := bus.Dispatch(&cmd); err != nil {
+ return ApiError(500, "Failed to update help flag", err)
+ }
+
+ return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
+}
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 8eb4eb4baea..3c6c362f949 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -46,6 +46,7 @@ var (
M_Alerting_Notification_Sent_Webhook Counter
M_Alerting_Notification_Sent_PagerDuty Counter
M_Alerting_Notification_Sent_Victorops Counter
+ M_Alerting_Notification_Sent_OpsGenie Counter
// Timers
M_DataSource_ProxyReq_Timer Timer
@@ -110,6 +111,7 @@ func initMetricVars(settings *MetricSettings) {
M_Alerting_Notification_Sent_Webhook = RegCounter("alerting.notifications_sent", "type", "webhook")
M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty")
M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
+ M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie")
// Timers
M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go
index a546d7e76fc..7a64656b0ee 100644
--- a/pkg/middleware/middleware.go
+++ b/pkg/middleware/middleware.go
@@ -229,6 +229,10 @@ func (ctx *Context) HasUserRole(role m.RoleType) bool {
return ctx.OrgRole.Includes(role)
}
+func (ctx *Context) HasHelpFlag(flag m.HelpFlags1) bool {
+ return ctx.HelpFlags1.HasFlag(flag)
+}
+
func (ctx *Context) TimeRequest(timer metrics.Timer) {
ctx.Data["perfmon.timer"] = timer
}
diff --git a/pkg/models/datasource_cache.go b/pkg/models/datasource_cache.go
new file mode 100644
index 00000000000..e32c0ac9e7c
--- /dev/null
+++ b/pkg/models/datasource_cache.go
@@ -0,0 +1,95 @@
+package models
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "net"
+ "net/http"
+ "sync"
+ "time"
+)
+
+type proxyTransportCache struct {
+ cache map[int64]cachedTransport
+ sync.Mutex
+}
+
+type cachedTransport struct {
+ updated time.Time
+
+ *http.Transport
+}
+
+var ptc = proxyTransportCache{
+ cache: make(map[int64]cachedTransport),
+}
+
+func (ds *DataSource) GetHttpClient() (*http.Client, error) {
+ transport, err := ds.GetHttpTransport()
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &http.Client{
+ Timeout: time.Duration(30 * time.Second),
+ Transport: transport,
+ }, nil
+}
+
+func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
+ ptc.Lock()
+ defer ptc.Unlock()
+
+ if t, present := ptc.cache[ds.Id]; present && ds.Updated.Equal(t.updated) {
+ return t.Transport, nil
+ }
+
+ transport := &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ Proxy: http.ProxyFromEnvironment,
+ Dial: (&net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+ }).Dial,
+ TLSHandshakeTimeout: 10 * time.Second,
+ ExpectContinueTimeout: 1 * time.Second,
+ MaxIdleConns: 100,
+ IdleConnTimeout: 90 * time.Second,
+ }
+
+ var tlsAuth, tlsAuthWithCACert bool
+ if ds.JsonData != nil {
+ tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
+ tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
+ }
+
+ if tlsAuth {
+ transport.TLSClientConfig.InsecureSkipVerify = false
+
+ decrypted := ds.SecureJsonData.Decrypt()
+
+ if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
+ caPool := x509.NewCertPool()
+ ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
+ if ok {
+ transport.TLSClientConfig.RootCAs = caPool
+ }
+ }
+
+ cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
+ if err != nil {
+ return nil, err
+ }
+ transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
+ }
+
+ ptc.cache[ds.Id] = cachedTransport{
+ Transport: transport,
+ updated: ds.Updated,
+ }
+
+ return transport, nil
+}
diff --git a/pkg/models/datasource_cache_test.go b/pkg/models/datasource_cache_test.go
new file mode 100644
index 00000000000..25fee55529d
--- /dev/null
+++ b/pkg/models/datasource_cache_test.go
@@ -0,0 +1,157 @@
+package models
+
+import (
+ "testing"
+ "time"
+
+ . "github.com/smartystreets/goconvey/convey"
+
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/setting"
+ "github.com/grafana/grafana/pkg/util"
+)
+
+func TestDataSourceCache(t *testing.T) {
+ Convey("When caching a datasource proxy", t, func() {
+ clearCache()
+ ds := DataSource{
+ Id: 1,
+ Url: "http://k8s:8001",
+ Type: "Kubernetes",
+ }
+
+ t1, err := ds.GetHttpTransport()
+ So(err, ShouldBeNil)
+
+ t2, err := ds.GetHttpTransport()
+ So(err, ShouldBeNil)
+
+ Convey("Should be using the cached proxy", func() {
+ So(t2, ShouldEqual, t1)
+ })
+ })
+
+ Convey("When getting kubernetes datasource proxy", t, func() {
+ clearCache()
+ setting.SecretKey = "password"
+
+ json := simplejson.New()
+ json.Set("tlsAuth", true)
+ json.Set("tlsAuthWithCACert", true)
+
+ t := time.Now()
+ ds := DataSource{
+ Url: "http://k8s:8001",
+ Type: "Kubernetes",
+ Updated: t.Add(-2 * time.Minute),
+ }
+
+ transport, err := ds.GetHttpTransport()
+ So(err, ShouldBeNil)
+
+ Convey("Should have no cert", func() {
+ So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
+ })
+
+ ds.JsonData = json
+ ds.SecureJsonData = map[string][]byte{
+ "tlsCACert": util.Encrypt([]byte(caCert), "password"),
+ "tlsClientCert": util.Encrypt([]byte(clientCert), "password"),
+ "tlsClientKey": util.Encrypt([]byte(clientKey), "password"),
+ }
+ ds.Updated = t.Add(-1 * time.Minute)
+
+ transport, err = ds.GetHttpTransport()
+ So(err, ShouldBeNil)
+
+ Convey("Should add cert", func() {
+ So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
+ So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1)
+ })
+
+ ds.JsonData = nil
+ ds.SecureJsonData = map[string][]byte{}
+ ds.Updated = t
+
+ transport, err = ds.GetHttpTransport()
+ So(err, ShouldBeNil)
+
+ Convey("Should remove cert", func() {
+ So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
+ So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 0)
+ })
+ })
+}
+
+func clearCache() {
+ ptc.Lock()
+ defer ptc.Unlock()
+
+ ptc.cache = make(map[int64]cachedTransport)
+}
+
+const caCert string = `-----BEGIN CERTIFICATE-----
+MIIDATCCAemgAwIBAgIJAMQ5hC3CPDTeMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
+BAMMDGNhLWs4cy1zdGhsbTAeFw0xNjEwMjcwODQyMjdaFw00NDAzMTQwODQyMjda
+MBcxFTATBgNVBAMMDGNhLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAMLe2AmJ6IleeUt69vgNchOjjmxIIxz5sp1vFu94m1vUip7CqnOg
+QkpUsHeBPrGYv8UGloARCL1xEWS+9FVZeXWQoDmbC0SxXhFwRIESNCET7Q8KMi/4
+4YPvnMLGZi3Fjwxa8BdUBCN1cx4WEooMVTWXm7RFMtZgDfuOAn3TNXla732sfT/d
+1HNFrh48b0wA+HhmA3nXoBnBEblA665hCeo7lIAdRr0zJxJpnFnWXkyTClsAUTMN
+iL905LdBiiIRenojipfKXvMz88XSaWTI7JjZYU3BvhyXndkT6f12cef3I96NY3WJ
+0uIK4k04WrbzdYXMU3rN6NqlvbHqnI+E7aMCAwEAAaNQME4wHQYDVR0OBBYEFHHx
+2+vSPw9bECHj3O51KNo5VdWOMB8GA1UdIwQYMBaAFHHx2+vSPw9bECHj3O51KNo5
+VdWOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH2eV5NcV3LBJHs9
+I+adbiTPg2vyumrGWwy73T0X8Dtchgt8wU7Q9b9Ucg2fOTmSSyS0iMqEu1Yb2ORB
+CknM9mixHC9PwEBbkGCom3VVkqdLwSP6gdILZgyLoH4i8sTUz+S1yGPepi+Vzhs7
+adOXtryjcGnwft6HdfKPNklMOHFnjw6uqpho54oj/z55jUpicY/8glDHdrr1bh3k
+MHuiWLGewHXPvxfG6UoUx1te65IhifVcJGFZDQwfEmhBflfCmtAJlZEsgTLlBBCh
+FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n
+3lb92xM=
+-----END CERTIFICATE-----`
+
+const clientCert string = `-----BEGIN CERTIFICATE-----
+MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
+YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
+GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAOMliaWyNEUJKM37vWCl5bGub3lMicyRAqGQyY/qxD9yKKM2
+FbucVcmWmg5vvTqQVl5rlQ+c7GI8OD6ptmFl8a26coEki7bFr8bkpSyBSEc5p27b
+Z0ORFSqBHWHQbr9PkxPLYW6T3gZYUtRYv3OQgGxLXlvUh85n/mQfuR3N1FgmShHo
+GtAFi/ht6leXa0Ms+jNSDLCmXpJm1GIEqgyKX7K3+g3vzo9coYqXq4XTa8Efs2v8
+SCwqWfBC3rHfgs/5DLB8WT4Kul8QzxkytzcaBQfRfzhSV6bkgm7oTzt2/1eRRsf4
+YnXzLE9YkCC9sAn+Owzqf+TYC1KRluWDfqqBTJUCAwEAATANBgkqhkiG9w0BAQsF
+AAOCAQEAdMsZg6edWGC+xngizn0uamrUg1ViaDqUsz0vpzY5NWLA4MsBc4EtxWRP
+ueQvjUimZ3U3+AX0YWNLIrH1FCVos2jdij/xkTUmHcwzr8rQy+B17cFi+a8jtpgw
+AU6WWoaAIEhhbWQfth/Diz3mivl1ARB+YqiWca2mjRPLTPcKJEURDVddQ423el0Q
+4JNxS5icu7T2zYTYHAo/cT9zVdLZl0xuLxYm3asK1IONJ/evxyVZima3il6MPvhe
+58Hwz+m+HdqHxi24b/1J/VKYbISG4huOQCdLzeNXgvwFlGPUmHSnnKo1/KbQDAR5
+llG/Sw5+FquFuChaA6l5KWy7F3bQyA==
+-----END CERTIFICATE-----`
+
+const clientKey string = `-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEA4yWJpbI0RQkozfu9YKXlsa5veUyJzJECoZDJj+rEP3IoozYV
+u5xVyZaaDm+9OpBWXmuVD5zsYjw4Pqm2YWXxrbpygSSLtsWvxuSlLIFIRzmnbttn
+Q5EVKoEdYdBuv0+TE8thbpPeBlhS1Fi/c5CAbEteW9SHzmf+ZB+5Hc3UWCZKEega
+0AWL+G3qV5drQyz6M1IMsKZekmbUYgSqDIpfsrf6De/Oj1yhiperhdNrwR+za/xI
+LCpZ8ELesd+Cz/kMsHxZPgq6XxDPGTK3NxoFB9F/OFJXpuSCbuhPO3b/V5FGx/hi
+dfMsT1iQIL2wCf47DOp/5NgLUpGW5YN+qoFMlQIDAQABAoIBAQCzy4u312XeW1Cs
+Mx6EuOwmh59/ESFmBkZh4rxZKYgrfE5EWlQ7i5SwG4BX+wR6rbNfy6JSmHDXlTkk
+CKvvToVNcW6fYHEivDnVojhIERFIJ4+rhQmpBtcNLOQ3/4cZ8X/GxE6b+3lb5l+x
+64mnjPLKRaIr5/+TVuebEy0xNTJmjnJ7yiB2HRz7uXEQaVSk/P7KAkkyl/9J3/LM
+8N9AX1w6qDaNQZ4/P0++1H4SQenosM/b/GqGTomarEk/GE0NcB9rzmR9VCXa7FRh
+WV5jyt9vUrwIEiK/6nUnOkGO8Ei3kB7Y+e+2m6WdaNoU5RAfqXmXa0Q/a0lLRruf
+vTMo2WrBAoGBAPRaK4cx76Q+3SJ/wfznaPsMM06OSR8A3ctKdV+ip/lyKtb1W8Pz
+k8MYQDH7GwPtSu5QD8doL00pPjugZL/ba7X9nAsI+pinyEErfnB9y7ORNEjIYYzs
+DiqDKup7ANgw1gZvznWvb9Ge0WUSXvWS0pFkgootQAf+RmnnbWGH6l6RAoGBAO35
+aGUrLro5u9RD24uSXNU3NmojINIQFK5dHAT3yl0BBYstL43AEsye9lX95uMPTvOQ
+Cqcn42Hjp/bSe3n0ObyOZeXVrWcDFAfE0wwB1BkvL1lpgnFO9+VQORlH4w3Ppnpo
+jcPkR2TFeDaAYtvckhxe/Bk3OnuFmnsQ3VzM75fFAoGBAI6PvS2XeNU+yA3EtA01
+hg5SQ+zlHswz2TMuMeSmJZJnhY78f5mHlwIQOAPxGQXlf/4iP9J7en1uPpzTK3S0
+M9duK4hUqMA/w5oiIhbHjf0qDnMYVbG+V1V+SZ+cPBXmCDihKreGr5qBKnHpkfV8
+v9WL6o1rcRw4wiQvnaV1gsvBAoGBALtzVTczr6gDKCAIn5wuWy+cQSGTsBunjRLX
+xuVm5iEiV+KMYkPvAx/pKzMLP96lRVR3ptyKgAKwl7LFk3u50+zh4gQLr35QH2wL
+Lw7rNc3srAhrItPsFzqrWX6/cGuFoKYVS239l/sZzRppQPXcpb7xVvTp2whHcir0
+Wtnpl+TdAoGAGqKqo2KU3JoY3IuTDUk1dsNAm8jd9EWDh+s1x4aG4N79mwcss5GD
+FF8MbFPneK7xQd8L6HisKUDAUi2NOyynM81LAftPkvN6ZuUVeFDfCL4vCA0HUXLD
++VrOhtUZkNNJlLMiVRJuQKUOGlg8PpObqYbstQAf/0/yFJMRHG82Tcg=
+-----END RSA PRIVATE KEY-----`
diff --git a/pkg/models/helpflags.go b/pkg/models/helpflags.go
new file mode 100644
index 00000000000..1bab730e265
--- /dev/null
+++ b/pkg/models/helpflags.go
@@ -0,0 +1,18 @@
+package models
+
+type HelpFlags1 uint64
+
+const (
+ HelpFlagGettingStartedPanelDismissed HelpFlags1 = 1 << iota
+ HelpFlagDashboardHelp1
+)
+
+func (f HelpFlags1) HasFlag(flag HelpFlags1) bool { return f&flag != 0 }
+func (f *HelpFlags1) AddFlag(flag HelpFlags1) { *f |= flag }
+func (f *HelpFlags1) ClearFlag(flag HelpFlags1) { *f &= ^flag }
+func (f *HelpFlags1) ToggleFlag(flag HelpFlags1) { *f ^= flag }
+
+type SetUserHelpFlagCommand struct {
+ HelpFlags1 HelpFlags1
+ UserId int64
+}
diff --git a/pkg/models/user.go b/pkg/models/user.go
index 1f99f866c86..8bfad7c2fb4 100644
--- a/pkg/models/user.go
+++ b/pkg/models/user.go
@@ -22,6 +22,7 @@ type User struct {
Company string
EmailVerified bool
Theme string
+ HelpFlags1 HelpFlags1
IsAdmin bool
OrgId int64
@@ -144,6 +145,7 @@ type SignedInUser struct {
Email string
ApiKeyId int64
IsGrafanaAdmin bool
+ HelpFlags1 HelpFlags1
}
type UserProfileDTO struct {
diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go
index ca60662ade8..7de12b4036c 100644
--- a/pkg/plugins/models.go
+++ b/pkg/plugins/models.go
@@ -38,6 +38,7 @@ type PluginBase struct {
Includes []*PluginInclude `json:"includes"`
Module string `json:"module"`
BaseUrl string `json:"baseUrl"`
+ HideFromList bool `json:"hideFromList"`
IncludedInAppId string `json:"-"`
PluginDir string `json:"-"`
diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go
index e58dbf1d583..d7e0a0c29e3 100644
--- a/pkg/services/alerting/conditions/query.go
+++ b/pkg/services/alerting/conditions/query.go
@@ -119,21 +119,9 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRa
TimeRange: timeRange,
Queries: []*tsdb.Query{
{
- RefId: "A",
- Model: c.Query.Model,
- DataSource: &tsdb.DataSourceInfo{
- Id: datasource.Id,
- Name: datasource.Name,
- PluginId: datasource.Type,
- Url: datasource.Url,
- User: datasource.User,
- Password: datasource.Password,
- Database: datasource.Database,
- BasicAuth: datasource.BasicAuth,
- BasicAuthUser: datasource.BasicAuthUser,
- BasicAuthPassword: datasource.BasicAuthPassword,
- JsonData: datasource.JsonData,
- },
+ RefId: "A",
+ Model: c.Query.Model,
+ DataSource: datasource,
},
},
}
diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go
index badf868e8fc..47a13a5a33d 100644
--- a/pkg/services/alerting/conditions/reducer_test.go
+++ b/pkg/services/alerting/conditions/reducer_test.go
@@ -11,25 +11,6 @@ import (
func TestSimpleReducer(t *testing.T) {
Convey("Test simple reducer by calculating", t, func() {
- Convey("avg", func() {
- result := testReducer("avg", 1, 2, 3)
- So(result, ShouldEqual, float64(2))
- })
-
- Convey("avg of none null data", func() {
- reducer := NewSimpleReducer("avg")
- series := &tsdb.TimeSeries{
- Name: "test time serie",
- }
-
- series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 1))
- series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
- series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3))
- series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 4))
-
- So(reducer.Reduce(series).Float64, ShouldEqual, float64(3))
- })
-
Convey("sum", func() {
result := testReducer("sum", 1, 2, 3)
So(result, ShouldEqual, float64(6))
@@ -69,6 +50,25 @@ func TestSimpleReducer(t *testing.T) {
result := testReducer("median", 1)
So(result, ShouldEqual, float64(1))
})
+
+ Convey("avg", func() {
+ result := testReducer("avg", 1, 2, 3)
+ So(result, ShouldEqual, float64(2))
+ })
+
+ Convey("avg of number values and null values should ignore nulls", func() {
+ reducer := NewSimpleReducer("avg")
+ series := &tsdb.TimeSeries{
+ Name: "test time serie",
+ }
+
+ series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 1))
+ series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
+ series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3))
+ series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 4))
+
+ So(reducer.Reduce(series).Float64, ShouldEqual, float64(3))
+ })
})
}
diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go
new file mode 100644
index 00000000000..e16cda4bc5b
--- /dev/null
+++ b/pkg/services/alerting/notifiers/opsgenie.go
@@ -0,0 +1,118 @@
+package notifiers
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/metrics"
+ m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/alerting"
+)
+
+func init() {
+ alerting.RegisterNotifier("opsgenie", NewOpsGenieNotifier)
+}
+
+var (
+ opsgenieCreateAlertURL string = "https://api.opsgenie.com/v1/json/alert"
+ opsgenieCloseAlertURL string = "https://api.opsgenie.com/v1/json/alert/close"
+)
+
+func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+ autoClose := model.Settings.Get("autoClose").MustBool(true)
+ apiKey := model.Settings.Get("apiKey").MustString()
+ if apiKey == "" {
+ return nil, alerting.ValidationError{Reason: "Could not find api key property in settings"}
+ }
+
+ return &OpsGenieNotifier{
+ NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+ ApiKey: apiKey,
+ AutoClose: autoClose,
+ log: log.New("alerting.notifier.opsgenie"),
+ }, nil
+}
+
+type OpsGenieNotifier struct {
+ NotifierBase
+ ApiKey string
+ AutoClose bool
+ log log.Logger
+}
+
+func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error {
+ metrics.M_Alerting_Notification_Sent_OpsGenie.Inc(1)
+
+ var err error
+ switch evalContext.Rule.State {
+ case m.AlertStateOK:
+ if this.AutoClose {
+ err = this.closeAlert(evalContext)
+ }
+ case m.AlertStateAlerting:
+ err = this.createAlert(evalContext)
+ }
+ return err
+}
+
+func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) error {
+ this.log.Info("Creating OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+
+ ruleUrl, err := evalContext.GetRuleUrl()
+ if err != nil {
+ this.log.Error("Failed get rule link", "error", err)
+ return err
+ }
+
+ bodyJSON := simplejson.New()
+ bodyJSON.Set("apiKey", this.ApiKey)
+ bodyJSON.Set("message", evalContext.Rule.Name)
+ bodyJSON.Set("source", "Grafana")
+ bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
+ bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message))
+
+ details := simplejson.New()
+ details.Set("url", ruleUrl)
+ if evalContext.ImagePublicUrl != "" {
+ details.Set("image", evalContext.ImagePublicUrl)
+ }
+
+ bodyJSON.Set("details", details)
+ body, _ := bodyJSON.MarshalJSON()
+
+ cmd := &m.SendWebhookSync{
+ Url: opsgenieCreateAlertURL,
+ Body: string(body),
+ HttpMethod: "POST",
+ }
+
+ if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+ this.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body))
+ }
+
+ return nil
+}
+
+func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) error {
+ this.log.Info("Closing OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name)
+
+ bodyJSON := simplejson.New()
+ bodyJSON.Set("apiKey", this.ApiKey)
+ bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
+ body, _ := bodyJSON.MarshalJSON()
+
+ cmd := &m.SendWebhookSync{
+ Url: opsgenieCloseAlertURL,
+ Body: string(body),
+ HttpMethod: "POST",
+ }
+
+ if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+ this.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body))
+ }
+
+ return nil
+}
diff --git a/pkg/services/alerting/notifiers/opsgenie_test.go b/pkg/services/alerting/notifiers/opsgenie_test.go
new file mode 100644
index 00000000000..9dcb9f3c600
--- /dev/null
+++ b/pkg/services/alerting/notifiers/opsgenie_test.go
@@ -0,0 +1,52 @@
+package notifiers
+
+import (
+ "testing"
+
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ m "github.com/grafana/grafana/pkg/models"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func TestOpsGenieNotifier(t *testing.T) {
+ Convey("OpsGenie notifier tests", t, func() {
+
+ Convey("Parsing alert notification from settings", func() {
+ Convey("empty settings should return error", func() {
+ json := `{ }`
+
+ settingsJSON, _ := simplejson.NewJson([]byte(json))
+ model := &m.AlertNotification{
+ Name: "opsgenie_testing",
+ Type: "opsgenie",
+ Settings: settingsJSON,
+ }
+
+ _, err := NewOpsGenieNotifier(model)
+ So(err, ShouldNotBeNil)
+ })
+
+ Convey("settings should trigger incident", func() {
+ json := `
+ {
+ "apiKey": "abcdefgh0123456789"
+ }`
+
+ settingsJSON, _ := simplejson.NewJson([]byte(json))
+ model := &m.AlertNotification{
+ Name: "opsgenie_testing",
+ Type: "opsgenie",
+ Settings: settingsJSON,
+ }
+
+ not, err := NewOpsGenieNotifier(model)
+ opsgenieNotifier := not.(*OpsGenieNotifier)
+
+ So(err, ShouldBeNil)
+ So(opsgenieNotifier.Name, ShouldEqual, "opsgenie_testing")
+ So(opsgenieNotifier.Type, ShouldEqual, "opsgenie")
+ So(opsgenieNotifier.ApiKey, ShouldEqual, "abcdefgh0123456789")
+ })
+ })
+ })
+}
diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go
index 51db4d74857..67446f64d1a 100644
--- a/pkg/services/sqlstore/migrations/user_mig.go
+++ b/pkg/services/sqlstore/migrations/user_mig.go
@@ -88,4 +88,8 @@ func addUserMigrations(mg *Migrator) {
}))
mg.AddMigration("Drop old table user_v1", NewDropTableMigration("user_v1"))
+
+ mg.AddMigration("Add column help_flags1 to user table", NewAddColumnMigration(userV2, &Column{
+ Name: "help_flags1", Type: DB_BigInt, Nullable: false, Default: "0",
+ }))
}
diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go
index bb21995f54a..b26a9153f55 100644
--- a/pkg/services/sqlstore/user.go
+++ b/pkg/services/sqlstore/user.go
@@ -28,6 +28,7 @@ func init() {
bus.AddHandler("sql", DeleteUser)
bus.AddHandler("sql", SetUsingOrg)
bus.AddHandler("sql", UpdateUserPermissions)
+ bus.AddHandler("sql", SetUserHelpFlag)
}
func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *session) (int64, error) {
@@ -207,7 +208,7 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error {
if err != nil {
return err
} else if has == false {
- return m.ErrUserNotFound
+ return m.ErrUserNotFound
}
query.Result = user
@@ -308,6 +309,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
u.email as email,
u.login as login,
u.name as name,
+ u.help_flags1 as help_flags1,
org.name as org_name,
org_user.role as org_role,
org.id as org_id
@@ -380,3 +382,20 @@ func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error {
return err
})
}
+
+func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
+ return inTransaction2(func(sess *session) error {
+
+ user := m.User{
+ Id: cmd.UserId,
+ HelpFlags1: cmd.HelpFlags1,
+ Updated: time.Now(),
+ }
+
+ if _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
diff --git a/pkg/tsdb/batch.go b/pkg/tsdb/batch.go
index 284a158bf5f..8130ac94b0d 100644
--- a/pkg/tsdb/batch.go
+++ b/pkg/tsdb/batch.go
@@ -1,9 +1,6 @@
package tsdb
-import (
- "context"
- "errors"
-)
+import "context"
type Batch struct {
DataSourceId int64
@@ -24,12 +21,12 @@ func newBatch(dsId int64, queries QuerySlice) *Batch {
}
func (bg *Batch) process(ctx context.Context, queryContext *QueryContext) {
- executor := getExecutorFor(bg.Queries[0].DataSource)
+ executor, err := getExecutorFor(bg.Queries[0].DataSource)
- if executor == nil {
+ if err != nil {
bg.Done = true
result := &BatchResult{
- Error: errors.New("Could not find executor for data source type: " + bg.Queries[0].DataSource.PluginId),
+ Error: err,
QueryResults: make(map[string]*QueryResult),
}
for _, query := range bg.Queries {
diff --git a/pkg/tsdb/executor.go b/pkg/tsdb/executor.go
index cc1d592dcd5..251b3dc947a 100644
--- a/pkg/tsdb/executor.go
+++ b/pkg/tsdb/executor.go
@@ -1,6 +1,11 @@
package tsdb
-import "context"
+import (
+ "context"
+ "fmt"
+
+ "github.com/grafana/grafana/pkg/models"
+)
type Executor interface {
Execute(ctx context.Context, queries QuerySlice, query *QueryContext) *BatchResult
@@ -8,17 +13,22 @@ type Executor interface {
var registry map[string]GetExecutorFn
-type GetExecutorFn func(dsInfo *DataSourceInfo) Executor
+type GetExecutorFn func(dsInfo *models.DataSource) (Executor, error)
func init() {
registry = make(map[string]GetExecutorFn)
}
-func getExecutorFor(dsInfo *DataSourceInfo) Executor {
- if fn, exists := registry[dsInfo.PluginId]; exists {
- return fn(dsInfo)
+func getExecutorFor(dsInfo *models.DataSource) (Executor, error) {
+ if fn, exists := registry[dsInfo.Type]; exists {
+ executor, err := fn(dsInfo)
+ if err != nil {
+ return nil, err
+ }
+
+ return executor, nil
}
- return nil
+ return nil, fmt.Errorf("Could not find executor for data source type: %s", dsInfo.Type)
}
func RegisterExecutor(pluginId string, fn GetExecutorFn) {
diff --git a/pkg/tsdb/fake_test.go b/pkg/tsdb/fake_test.go
index c403fdba4fb..3c773971240 100644
--- a/pkg/tsdb/fake_test.go
+++ b/pkg/tsdb/fake_test.go
@@ -1,6 +1,10 @@
package tsdb
-import "context"
+import (
+ "context"
+
+ "github.com/grafana/grafana/pkg/models"
+)
type FakeExecutor struct {
results map[string]*QueryResult
@@ -9,11 +13,11 @@ type FakeExecutor struct {
type ResultsFn func(context *QueryContext) *QueryResult
-func NewFakeExecutor(dsInfo *DataSourceInfo) *FakeExecutor {
+func NewFakeExecutor(dsInfo *models.DataSource) (*FakeExecutor, error) {
return &FakeExecutor{
results: make(map[string]*QueryResult),
resultsFn: make(map[string]ResultsFn),
- }
+ }, nil
}
func (e *FakeExecutor) Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult {
diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go
index 3d6af5beb5b..a467e839b82 100644
--- a/pkg/tsdb/graphite/graphite.go
+++ b/pkg/tsdb/graphite/graphite.go
@@ -14,28 +14,36 @@ import (
"golang.org/x/net/context/ctxhttp"
"github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
)
type GraphiteExecutor struct {
- *tsdb.DataSourceInfo
+ *models.DataSource
+ HttpClient *http.Client
}
-func NewGraphiteExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
- return &GraphiteExecutor{dsInfo}
+func NewGraphiteExecutor(datasource *models.DataSource) (tsdb.Executor, error) {
+ httpClient, err := datasource.GetHttpClient()
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &GraphiteExecutor{
+ DataSource: datasource,
+ HttpClient: httpClient,
+ }, nil
}
var (
- glog log.Logger
- HttpClient *http.Client
+ glog log.Logger
)
func init() {
glog = log.New("tsdb.graphite")
tsdb.RegisterExecutor("graphite", NewGraphiteExecutor)
-
- HttpClient = tsdb.GetDefaultClient()
}
func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
@@ -66,7 +74,7 @@ func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
return result
}
- res, err := ctxhttp.Do(ctx, HttpClient, req)
+ res, err := ctxhttp.Do(ctx, e.HttpClient, req)
if err != nil {
result.Error = err
return result
diff --git a/pkg/tsdb/http.go b/pkg/tsdb/http.go
deleted file mode 100644
index f5de146d470..00000000000
--- a/pkg/tsdb/http.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package tsdb
-
-import (
- "crypto/tls"
- "net"
- "net/http"
- "time"
-)
-
-func GetDefaultClient() *http.Client {
- tr := &http.Transport{
- Proxy: http.ProxyFromEnvironment,
- DialContext: (&net.Dialer{
- Timeout: 30 * time.Second,
- KeepAlive: 30 * time.Second,
- }).DialContext,
- MaxIdleConns: 100,
- IdleConnTimeout: 90 * time.Second,
- TLSHandshakeTimeout: 10 * time.Second,
- ExpectContinueTimeout: 1 * time.Second,
-
- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
- }
-
- return &http.Client{
- Timeout: time.Duration(30 * time.Second),
- Transport: tr,
- }
-}
diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go
index 658fbcff023..21d359d24bf 100644
--- a/pkg/tsdb/influxdb/influxdb.go
+++ b/pkg/tsdb/influxdb/influxdb.go
@@ -11,34 +11,40 @@ import (
"golang.org/x/net/context/ctxhttp"
"github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
)
type InfluxDBExecutor struct {
- *tsdb.DataSourceInfo
+ *models.DataSource
QueryParser *InfluxdbQueryParser
ResponseParser *ResponseParser
+ HttpClient *http.Client
}
-func NewInfluxDBExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
+func NewInfluxDBExecutor(datasource *models.DataSource) (tsdb.Executor, error) {
+ httpClient, err := datasource.GetHttpClient()
+
+ if err != nil {
+ return nil, err
+ }
+
return &InfluxDBExecutor{
- DataSourceInfo: dsInfo,
+ DataSource: datasource,
QueryParser: &InfluxdbQueryParser{},
ResponseParser: &ResponseParser{},
- }
+ HttpClient: httpClient,
+ }, nil
}
var (
- glog log.Logger
- HttpClient *http.Client
+ glog log.Logger
)
func init() {
glog = log.New("tsdb.influxdb")
tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor)
-
- HttpClient = tsdb.GetDefaultClient()
}
func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
@@ -63,7 +69,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
return result.WithError(err)
}
- resp, err := ctxhttp.Do(ctx, HttpClient, req)
+ resp, err := ctxhttp.Do(ctx, e.HttpClient, req)
if err != nil {
return result.WithError(err)
}
@@ -95,7 +101,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
func (e *InfluxDBExecutor) getQuery(queries tsdb.QuerySlice, context *tsdb.QueryContext) (*Query, error) {
for _, v := range queries {
- query, err := e.QueryParser.Parse(v.Model, e.DataSourceInfo)
+ query, err := e.QueryParser.Parse(v.Model, e.DataSource)
if err != nil {
return nil, err
}
diff --git a/pkg/tsdb/influxdb/model_parser.go b/pkg/tsdb/influxdb/model_parser.go
index 410bf8f6e82..f3d87739e5b 100644
--- a/pkg/tsdb/influxdb/model_parser.go
+++ b/pkg/tsdb/influxdb/model_parser.go
@@ -4,12 +4,12 @@ import (
"strconv"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/tsdb"
+ "github.com/grafana/grafana/pkg/models"
)
type InfluxdbQueryParser struct{}
-func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) {
+func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *models.DataSource) (*Query, error) {
policy := model.Get("policy").MustString("default")
rawQuery := model.Get("query").MustString("")
useRawQuery := model.Get("rawQuery").MustBool(false)
diff --git a/pkg/tsdb/influxdb/model_parser_test.go b/pkg/tsdb/influxdb/model_parser_test.go
index eae6e163cc0..f7049efb9a7 100644
--- a/pkg/tsdb/influxdb/model_parser_test.go
+++ b/pkg/tsdb/influxdb/model_parser_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/tsdb"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -12,7 +12,7 @@ func TestInfluxdbQueryParser(t *testing.T) {
Convey("Influxdb query parser", t, func() {
parser := &InfluxdbQueryParser{}
- dsInfo := &tsdb.DataSourceInfo{
+ dsInfo := &models.DataSource{
JsonData: simplejson.New(),
}
diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go
index 366709cfba7..e95713e7077 100644
--- a/pkg/tsdb/models.go
+++ b/pkg/tsdb/models.go
@@ -2,6 +2,7 @@ package tsdb
import (
"github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/models"
"gopkg.in/guregu/null.v3"
)
@@ -9,7 +10,7 @@ type Query struct {
RefId string
Model *simplejson.Json
Depends []string
- DataSource *DataSourceInfo
+ DataSource *models.DataSource
Results []*TimeSeries
Exclude bool
MaxDataPoints int64
@@ -28,20 +29,6 @@ type Response struct {
Results map[string]*QueryResult `json:"results"`
}
-type DataSourceInfo struct {
- Id int64
- Name string
- PluginId string
- Url string
- Password string
- User string
- Database string
- BasicAuth bool
- BasicAuthUser string
- BasicAuthPassword string
- JsonData *simplejson.Json
-}
-
type BatchTiming struct {
TimeElapsed int64
}
diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go
index c5e5b0020d1..f5b6ffda7d2 100644
--- a/pkg/tsdb/opentsdb/opentsdb.go
+++ b/pkg/tsdb/opentsdb/opentsdb.go
@@ -17,28 +17,36 @@ import (
"gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
)
type OpenTsdbExecutor struct {
- *tsdb.DataSourceInfo
+ *models.DataSource
+ httpClient *http.Client
}
-func NewOpenTsdbExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
- return &OpenTsdbExecutor{dsInfo}
+func NewOpenTsdbExecutor(datasource *models.DataSource) (tsdb.Executor, error) {
+ httpClient, err := datasource.GetHttpClient()
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &OpenTsdbExecutor{
+ DataSource: datasource,
+ httpClient: httpClient,
+ }, nil
}
var (
- plog log.Logger
- HttpClient *http.Client
+ plog log.Logger
)
func init() {
plog = log.New("tsdb.opentsdb")
tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor)
-
- HttpClient = tsdb.GetDefaultClient()
}
func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
@@ -64,7 +72,7 @@ func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
return result
}
- res, err := ctxhttp.Do(ctx, HttpClient, req)
+ res, err := ctxhttp.Do(ctx, e.httpClient, req)
if err != nil {
result.Error = err
return result
diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go
index 21e1dfdef24..c391f00920a 100644
--- a/pkg/tsdb/prometheus/prometheus.go
+++ b/pkg/tsdb/prometheus/prometheus.go
@@ -9,18 +9,30 @@ import (
"gopkg.in/guregu/null.v3"
+ "net/http"
+
"github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/prometheus/client_golang/api/prometheus"
pmodel "github.com/prometheus/common/model"
)
type PrometheusExecutor struct {
- *tsdb.DataSourceInfo
+ *models.DataSource
+ Transport *http.Transport
}
-func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
- return &PrometheusExecutor{dsInfo}
+func NewPrometheusExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) {
+ transport, err := dsInfo.GetHttpTransport()
+ if err != nil {
+ return nil, err
+ }
+
+ return &PrometheusExecutor{
+ DataSource: dsInfo,
+ Transport: transport,
+ }, nil
}
var (
@@ -36,7 +48,8 @@ func init() {
func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) {
cfg := prometheus.Config{
- Address: e.DataSourceInfo.Url,
+ Address: e.DataSource.Url,
+ Transport: e.Transport,
}
client, err := prometheus.New(cfg)
diff --git a/pkg/tsdb/testdata/testdata.go b/pkg/tsdb/testdata/testdata.go
index cf2dcc0f898..6aefd8686d8 100644
--- a/pkg/tsdb/testdata/testdata.go
+++ b/pkg/tsdb/testdata/testdata.go
@@ -4,19 +4,20 @@ import (
"context"
"github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
)
type TestDataExecutor struct {
- *tsdb.DataSourceInfo
+ *models.DataSource
log log.Logger
}
-func NewTestDataExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
+func NewTestDataExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) {
return &TestDataExecutor{
- DataSourceInfo: dsInfo,
- log: log.New("tsdb.testdata"),
- }
+ DataSource: dsInfo,
+ log: log.New("tsdb.testdata"),
+ }, nil
}
func init() {
diff --git a/pkg/tsdb/tsdb_test.go b/pkg/tsdb/tsdb_test.go
index 998f59a6b9d..2b1a2372cd6 100644
--- a/pkg/tsdb/tsdb_test.go
+++ b/pkg/tsdb/tsdb_test.go
@@ -5,6 +5,7 @@ import (
"testing"
"time"
+ "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -15,9 +16,9 @@ func TestMetricQuery(t *testing.T) {
Convey("Given 3 queries for 2 data sources", func() {
request := &Request{
Queries: QuerySlice{
- {RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
- {RefId: "B", DataSource: &DataSourceInfo{Id: 1}},
- {RefId: "C", DataSource: &DataSourceInfo{Id: 2}},
+ {RefId: "A", DataSource: &models.DataSource{Id: 1}},
+ {RefId: "B", DataSource: &models.DataSource{Id: 1}},
+ {RefId: "C", DataSource: &models.DataSource{Id: 2}},
},
}
@@ -32,9 +33,9 @@ func TestMetricQuery(t *testing.T) {
Convey("Given query 2 depends on query 1", func() {
request := &Request{
Queries: QuerySlice{
- {RefId: "A", DataSource: &DataSourceInfo{Id: 1}},
- {RefId: "B", DataSource: &DataSourceInfo{Id: 2}},
- {RefId: "C", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}},
+ {RefId: "A", DataSource: &models.DataSource{Id: 1}},
+ {RefId: "B", DataSource: &models.DataSource{Id: 2}},
+ {RefId: "C", DataSource: &models.DataSource{Id: 3}, Depends: []string{"A", "B"}},
},
}
@@ -56,7 +57,7 @@ func TestMetricQuery(t *testing.T) {
Convey("When executing request with one query", t, func() {
req := &Request{
Queries: QuerySlice{
- {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
+ {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}},
},
}
@@ -75,8 +76,8 @@ func TestMetricQuery(t *testing.T) {
Convey("When executing one request with two queries from same data source", t, func() {
req := &Request{
Queries: QuerySlice{
- {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
- {RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
+ {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}},
+ {RefId: "B", DataSource: &models.DataSource{Id: 1, Type: "test"}},
},
}
@@ -101,9 +102,9 @@ func TestMetricQuery(t *testing.T) {
Convey("When executing one request with three queries from different datasources", t, func() {
req := &Request{
Queries: QuerySlice{
- {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
- {RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}},
- {RefId: "C", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}},
+ {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}},
+ {RefId: "B", DataSource: &models.DataSource{Id: 1, Type: "test"}},
+ {RefId: "C", DataSource: &models.DataSource{Id: 2, Type: "test"}},
},
}
@@ -118,7 +119,7 @@ func TestMetricQuery(t *testing.T) {
Convey("When query uses data source of unknown type", t, func() {
req := &Request{
Queries: QuerySlice{
- {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}},
+ {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "asdasdas"}},
},
}
@@ -130,10 +131,10 @@ func TestMetricQuery(t *testing.T) {
req := &Request{
Queries: QuerySlice{
{
- RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"},
+ RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"},
},
{
- RefId: "B", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"},
+ RefId: "B", DataSource: &models.DataSource{Id: 2, Type: "test"}, Depends: []string{"A"},
},
},
}
@@ -167,9 +168,9 @@ func TestMetricQuery(t *testing.T) {
}
func registerFakeExecutor() *FakeExecutor {
- executor := NewFakeExecutor(nil)
- RegisterExecutor("test", func(dsInfo *DataSourceInfo) Executor {
- return executor
+ executor, _ := NewFakeExecutor(nil)
+ RegisterExecutor("test", func(dsInfo *models.DataSource) (Executor, error) {
+ return executor, nil
})
return executor
diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts
index 1e620e88216..9a5ec5d219b 100644
--- a/public/app/core/services/backend_srv.ts
+++ b/public/app/core/services/backend_srv.ts
@@ -74,7 +74,9 @@ export class BackendSrv {
return this.$http(options).then(results => {
if (options.method !== 'GET') {
if (results && results.data.message) {
- this.alertSrv.set(results.data.message, '', 'success', 3000);
+ if (options.showSuccessAlert !== false) {
+ this.alertSrv.set(results.data.message, '', 'success', 3000);
+ }
}
}
return results.data;
diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts
index ac00528db20..3d048e4f869 100644
--- a/public/app/core/services/context_srv.ts
+++ b/public/app/core/services/context_srv.ts
@@ -10,6 +10,7 @@ export class User {
isSignedIn: any;
orgRole: any;
timezone: string;
+ helpFlags1: number;
constructor() {
if (config.bootData.user) {
diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index c1109fa2b2e..2b509db822a 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -89,6 +89,7 @@ export class KeybindingSrv {
this.bind('mod+o', () => {
dashboard.sharedCrosshair = !dashboard.sharedCrosshair;
+ appEvents.emit('graph-hover-clear');
scope.broadcastRefresh();
});
diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts
index 426ff62382f..c6683556745 100644
--- a/public/app/features/alerting/alert_tab_ctrl.ts
+++ b/public/app/features/alerting/alert_tab_ctrl.ts
@@ -94,6 +94,7 @@ export class AlertTabCtrl {
case "victorops": return "fa fa-pagelines";
case "webhook": return "fa fa-cubes";
case "pagerduty": return "fa fa-bullhorn";
+ case "opsgenie": return "fa fa-bell";
}
}
diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html
index ac4afbf56fc..b5076efd792 100644
--- a/public/app/features/alerting/partials/notification_edit.html
+++ b/public/app/features/alerting/partials/notification_edit.html
@@ -19,7 +19,7 @@