diff --git a/CHANGELOG.md b/CHANGELOG.md index 62e0ce1173c..ef6c3ffc273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ * **Prometheus**: Adds /metrics endpoint for exposing Grafana metrics. [#9187](https://github.com/grafana/grafana/pull/9187) * **Graph**: Add support for local formating in axis. [#1395](https://github.com/grafana/grafana/issues/1395), thx [@m0nhawk](https://github.com/m0nhawk) * **Jaeger**: Add support for open tracing using jaeger in Grafana. [#9213](https://github.com/grafana/grafana/pull/9213) -* **Unit types**: New date & time unit types added, useful in singlestat to show dates & times. [#3678](https://github.com/grafana/grafana/issues/3678), [#6710](https://github.com/grafana/grafana/issues/6710), [#2764](https://github.com/grafana/grafana/issues/6710) +* **Unit types**: New date & time unit types added, useful in singlestat to show dates & times. [#3678](https://github.com/grafana/grafana/issues/3678), [#6710](https://github.com/grafana/grafana/issues/6710), [#2764](https://github.com/grafana/grafana/issues/2764) * **CLI**: Make it possible to install plugins from any url [#5873](https://github.com/grafana/grafana/issues/5873) * **Prometheus**: Add support for instant queries [#5765](https://github.com/grafana/grafana/issues/5765), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: Add support for alerting using the cloudwatch datasource [#8050](https://github.com/grafana/grafana/pull/8050), thx [@mtanda](https://github.com/mtanda) @@ -23,6 +23,8 @@ * **Prometheus**: Align $__interval with the step parameters. [#9226](https://github.com/grafana/grafana/pull/9226), thx [@alin-amana](https://github.com/alin-amana) * **Prometheus**: Autocomplete for label name and label value [#9208](https://github.com/grafana/grafana/pull/9208), thx [@mtanda](https://github.com/mtanda) * **Postgres**: New Postgres data source [#9209](https://github.com/grafana/grafana/pull/9209), thx [@svenklemm](https://github.com/svenklemm) +* **Datasources**: Make datasource HTTP requests verify TLS by default. closes [#9371](https://github.com/grafana/grafana/issues/9371), [#5334](https://github.com/grafana/grafana/issues/5334), [#8812](https://github.com/grafana/grafana/issues/8812), thx [@mattbostock](https://github.com/mattbostock) +* **OAuth**: Verify TLS during OAuth callback [#9373](https://github.com/grafana/grafana/issues/9373), thx [@mattbostock](https://github.com/mattbostock) ## Minor * **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319) @@ -33,9 +35,11 @@ * **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn) * **Table**: Add support for displaying the timestamp with milliseconds [#9429](https://github.com/grafana/grafana/pull/9429), thx [@s1061123](https://github.com/s1061123) * **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo) +* **Kafka**: Add support for sending alert notifications to kafka [#7104](https://github.com/grafana/grafana/issues/7104), thx [@utkarshcmu](https://github.com/utkarshcmu) ## Tech * **Go**: Grafana is now built using golang 1.9 +* **Webpack**: Changed from systemjs to webpack (see readme or building from source guide for new build instructions). Systemjs is still used to load plugins but now plugins can only import a limited set of dependencies. See [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) for more details on how this can effect some plugins. # 4.5.2 (2017-09-22) diff --git a/PLUGIN_DEV.md b/PLUGIN_DEV.md new file mode 100644 index 00000000000..1f672aa5a3d --- /dev/null +++ b/PLUGIN_DEV.md @@ -0,0 +1,28 @@ +# Plugin Development + +This document is not meant as complete guide for developing plugins but more as a changelog for changes in +Grafana that can impact plugin development. When ever you as plugin author encounter an issue with your plugin after +upgrading Grafana please check here before creating an issue. + +## Links + +- [Datasource plugin written in typescript](https://github.com/grafana/typescript-template-datasource) +- [Simple json dataource plugin](https://github.com/grafana/simple-json-datasource) +- [Plugin development guide](http://docs.grafana.org/plugins/developing/development/) + +## Changes in v4.6 + +This version of Grafana has big changes that will impact a limited set of plugins. We moved from systemjs to webpack +for built-in plugins & everything internal. External plugins still use systemjs but now with a limited +set of Grafana components they can import. Plugins can depend on libs like lodash & moment and internal components +like before using the same import paths. However since everything in Grafana is no longer accessible, a few plugins could encounter issues when importing a Grafana dependency. + +[List of exposed components plugins can import/require](https://github.com/grafana/grafana/blob/master/public/app/features/plugins/plugin_loader.ts#L48) + +If you think we missed exposing a crucial lib or Grafana component let us know by opening an issue. + +### Deprecated components + +The angular directive `` is no deprecated (will still work for a version more) but we recommend plugin authors +to upgrade to new `` + diff --git a/README.md b/README.md index 40ec43e1c16..28f8bfc1ba2 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,17 @@ You only need to add the options you want to override. Config files are applied In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode = development`. ## Contribute + If you have any idea for an improvement or found a bug do not hesitate to open an issue. And if you have time clone this repo and submit a pull request and help me make Grafana the kickass metrics & devops dashboard we all dream about! +## Plugin development + +Checkout the [Plugin Development Guide](http://docs.grafana.org/plugins/developing/development/) and checkout the [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) file for changes in Grafana that relate to +plugin development. + ## License + Grafana is distributed under Apache 2.0 License. -Work in progress Grafana 2.0 (with included Grafana backend) + diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index de9e5abd472..102134f34dd 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -115,6 +115,17 @@ In DingTalk PC Client: Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `text` message type is supported. +### Kafka + +Notifications can be sent to a Kafka topic from Grafana using [Kafka REST Proxy](https://docs.confluent.io/1.0/kafka-rest/docs/index.html). +There are couple of configurations options which need to be set in Grafana UI under Kafka Settings: + +1. Kafka REST Proxy endpoint. + +2. Kafka Topic. + +Once these two properties are set, you can send the alerts to Kafka for further processing or throttling them. + ### Other Supported Notification Channels Grafana also supports the following Notification Channels: diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md index be4e3e78e49..6ce17113a9b 100644 --- a/docs/sources/features/datasources/elasticsearch.md +++ b/docs/sources/features/datasources/elasticsearch.md @@ -135,6 +135,5 @@ Name | Description ------------ | ------------- Query | You can leave the search query blank or specify a lucene query Time | The name of the time field, needs to be date field. -Title | The name of the field to use for the event title. +Text | Event description field. Tags | Optional field name to use for event tags (can be an array or a CSV string). -Text | Optional field name to use event text body. diff --git a/docs/sources/features/panels/alertlist.md b/docs/sources/features/panels/alertlist.md index 8d235c20669..9307bb71391 100644 --- a/docs/sources/features/panels/alertlist.md +++ b/docs/sources/features/panels/alertlist.md @@ -18,7 +18,7 @@ The alert list panel allows you to display your dashbords alerts. The list can b ## Alert List Options -{{< docs-imagebox img="/img/docs/v45/alert-list-options.png" max-width="600px" class="docs-image--no-shadow docs-image--right">}} +{{< docs-imagebox img="/img/docs/v45/alert-list-options.png" max-width="600px" class="docs-image--no-shadow docs-image--right" >}} 1. **Show**: Lets you choose between current state or recent state changes. 2. **Max Items**: Max items set the maximum of items in a list. diff --git a/docs/sources/guides/whats-new-in-v4-6.md b/docs/sources/guides/whats-new-in-v4-6.md new file mode 100644 index 00000000000..3bc80799afc --- /dev/null +++ b/docs/sources/guides/whats-new-in-v4-6.md @@ -0,0 +1,74 @@ ++++ +title = "What's New in Grafana v4.6" +description = "Feature & improvement highlights for Grafana v4.6" +keywords = ["grafana", "new", "documentation", "4.6"] +type = "docs" +[menu.docs] +name = "Version 4.6" +identifier = "v4.6" +parent = "whatsnew" +weight = -5 ++++ + +# What's New in Grafana v4.6 + +Grafana v4.6 brings many enhancements to Annotations, Cloudwatch & Prometheus. It also adds support for Postgres as metric & table data source! + +### Annotations + +{{< docs-imagebox img="/img/docs/v46/add_annotation_region.png" max-width= "800px" >}} + +You can now add annotation events and regions right from the graph panel! Just hold CTRL/CMD + click or drag region to open the **Add Annotation** view. The +[Annotations]({{< relref "reference/annotations.md" >}}) documentation is updated to include details on this new exciting feature. + +### Cloudwatch + +Cloudwatch now supports alerting. Setup alert rules for any Cloudwatch metric! + +{{< docs-imagebox img="/img/docs/v46/cloudwatch_alerting.png" max-width= "800px" >}} + +### Postgres + +Grafana v4.6 now ships with a built-in datasource plugin for Postgres. Have logs or metric data in Postgres? You can now visualize that data and +define alert rules on it like any of our other data sources. + +{{< docs-imagebox img="/img/docs/v46/postgres_table_query.png" max-width= "800px" >}} + +### Prometheus + +New enhancements include support for **instant queries** and improvements to query editor in the form of autocomplete for label names and label values. +This makes exploring and filtering Prometheus data much easier. + +## Changelog + +### New Features + +* **GCS**: Adds support for Google Cloud Storage [#8370](https://github.com/grafana/grafana/issues/8370) thx [@chuhlomin](https://github.com/chuhlomin) +* **Prometheus**: Adds /metrics endpoint for exposing Grafana metrics. [#9187](https://github.com/grafana/grafana/pull/9187) +* **Graph**: Add support for local formating in axis. [#1395](https://github.com/grafana/grafana/issues/1395), thx [@m0nhawk](https://github.com/m0nhawk) +* **Jaeger**: Add support for open tracing using jaeger in Grafana. [#9213](https://github.com/grafana/grafana/pull/9213) +* **Unit types**: New date & time unit types added, useful in singlestat to show dates & times. [#3678](https://github.com/grafana/grafana/issues/3678), [#6710](https://github.com/grafana/grafana/issues/6710), [#2764](https://github.com/grafana/grafana/issues/2764) +* **CLI**: Make it possible to install plugins from any url [#5873](https://github.com/grafana/grafana/issues/5873) +* **Prometheus**: Add support for instant queries [#5765](https://github.com/grafana/grafana/issues/5765), thx [@mtanda](https://github.com/mtanda) +* **Cloudwatch**: Add support for alerting using the cloudwatch datasource [#8050](https://github.com/grafana/grafana/pull/8050), thx [@mtanda](https://github.com/mtanda) +* **Pagerduty**: Include triggering series in pagerduty notification [#8479](https://github.com/grafana/grafana/issues/8479), thx [@rickymoorhouse](https://github.com/rickymoorhouse) +* **Timezone**: Time ranges like Today & Yesterday now work correctly when timezone setting is set to UTC [#8916](https://github.com/grafana/grafana/issues/8916), thx [@ctide](https://github.com/ctide) +* **Prometheus**: Align $__interval with the step parameters. [#9226](https://github.com/grafana/grafana/pull/9226), thx [@alin-amana](https://github.com/alin-amana) +* **Prometheus**: Autocomplete for label name and label value [#9208](https://github.com/grafana/grafana/pull/9208), thx [@mtanda](https://github.com/mtanda) +* **Postgres**: New Postgres data source [#9209](https://github.com/grafana/grafana/pull/9209), thx [@svenklemm](https://github.com/svenklemm) +* **Datasources**: closes [#9371](https://github.com/grafana/grafana/issues/9371), [#5334](https://github.com/grafana/grafana/issues/5334), [#8812](https://github.com/grafana/grafana/issues/8812), thx [@mattbostock](https://github.com/mattbostock) + +### Minor Changes + +* **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319) +* **Dataproxy**: Allow grafan to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250) +* **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367) +* **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739) +* **Slack**: Allow images to be uploaded to slack when Token is precent [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8) +* **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn) +* **Table**: Add support for displaying the timestamp with milliseconds [#9429](https://github.com/grafana/grafana/pull/9429), thx [@s1061123](https://github.com/s1061123) +* **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo) + +### Tech +* **Go**: Grafana is now built using golang 1.9 + diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index b72e5c78d2f..cf08173f3e9 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -57,8 +57,7 @@ baseurl=https://packagecloud.io/grafana/stable/el/6/$basearch repo_gpgcheck=1 enabled=1 gpgcheck=1 -gpgkey=https://packagecloud.io/gpg.key -https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana +gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt ``` diff --git a/docs/sources/reference/annotations.md b/docs/sources/reference/annotations.md index fd30e520d07..1b904bc7c4a 100644 --- a/docs/sources/reference/annotations.md +++ b/docs/sources/reference/annotations.md @@ -10,34 +10,37 @@ weight = 2 # Annotations +{{< docs-imagebox img="/img/docs/v46/annotations.png" max-width="800px" >}} + Annotations provide a way to mark points on the graph with rich events. When you hover over an annotation you can get event description and event tags. The text field can include links to other systems with more detail. -![](/img/docs/annotations/toggles.png) - ## Native annotations -Grafana v4.6+ comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the [HTTP API]({{< relref "http_api/annotations.md" >}}) +Grafana v4.6+ comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the [HTTP API]({{< relref "http_api/annotations.md" >}}). ## Adding annotations -by holding down CTRL/CMD + mouse click. Add tags to the annotation will make it searchable from other dashboards. +By holding down CTRL/CMD + mouse click. Add tags to the annotation will make it searchable from other dashboards. - +{{< docs-imagebox img="/img/docs/annotations/annotation-still.png" +max-width="600px" animated-gif="/img/docs/annotations/annotation.gif" >}} ### Adding regions events You can also hold down CTRL/CMD and select region to create a region annotation. - +{{< docs-imagebox img="/img/docs/annotations/region-annotation-still.png" +max-width="600px" animated-gif="/img/docs/annotations/region-annotation.gif" >}} ### Built in query -After you added an an annotation they will be still be visible. This is due to the built in annotation query that exists on all dashboards. This annotation query will +After you added an annotation they will still be visible. This is due to the built in annotation query that exists on all dashboards. This annotation query will fetch all annotation events that originate from the current dashboard and show them on the panel where they where created. This includes alert state history annotations. You can stop annotations from being fetched & drawn by opening the **Annotations** settings (via Dashboard cogs menu) and modifying the query named `Annotations & Alerts (Built-in)`. - +When you copy a dashboard using the **Save As** feature it will get a new dashboard id so annotations created on source dashboard will no longer be visible on the copy. You +can still show them if you add a new **Annotation Query** and filter by tags. But this only works if the annotations on the source dashboard had tags to filter by. ### Query by tag diff --git a/package.json b/package.json index bffbea9f547..e037b0211cf 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "4.6.0-pre1", + "version": "4.6.0-beta1", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 300fa7f2cdc..be069f2b07e 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -1,6 +1,10 @@ package api import ( + "fmt" + "strings" + "time" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/middleware" @@ -78,6 +82,60 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response return ApiSuccess("Annotation added") } +type GraphiteAnnotationError struct { + message string +} + +func (e *GraphiteAnnotationError) Error() string { + return e.message +} + +func formatGraphiteAnnotation(what string, data string) string { + return fmt.Sprintf("%s\n%s", what, data) +} + +func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotationsCmd) Response { + repo := annotations.GetRepository() + + if cmd.When == 0 { + cmd.When = time.Now().Unix() + } + text := formatGraphiteAnnotation(cmd.What, cmd.Data) + + // Support tags in prior to Graphite 0.10.0 format (string of tags separated by space) + var tagsArray []string + switch tags := cmd.Tags.(type) { + case string: + tagsArray = strings.Split(tags, " ") + case []interface{}: + for _, t := range tags { + if tagStr, ok := t.(string); ok { + tagsArray = append(tagsArray, tagStr) + } else { + err := &GraphiteAnnotationError{"tag should be a string"} + return ApiError(500, "Failed to save Graphite annotation", err) + } + } + default: + err := &GraphiteAnnotationError{"unsupported tags format"} + return ApiError(500, "Failed to save Graphite annotation", err) + } + + item := annotations.Item{ + OrgId: c.OrgId, + UserId: c.UserId, + Epoch: cmd.When, + Text: text, + Tags: tagsArray, + } + + if err := repo.Save(&item); err != nil { + return ApiError(500, "Failed to save Graphite annotation", err) + } + + return ApiSuccess("Graphite Annotation added") +} + func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Response { annotationId := c.ParamsInt64(":annotationId") diff --git a/pkg/api/api.go b/pkg/api/api.go index 802d6e2d028..8c91120facc 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -292,6 +292,7 @@ func (hs *HttpServer) registerRoutes() { annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById)) annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation)) annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion)) + annotationsRoute.Post("/graphite", bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation)) }, reqEditorRole) // error test diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index 8992f8f66d6..0440c880979 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.go @@ -6,31 +6,33 @@ import ( "net/http" "time" - "gopkg.in/macaron.v1" - "github.com/grafana/grafana/pkg/api/pluginproxy" "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/setting" "github.com/grafana/grafana/pkg/util" + macaron "gopkg.in/macaron.v1" ) -var pluginProxyTransport = &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - Renegotiation: tls.RenegotiateFreelyAsClient, - }, - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, -} +var pluginProxyTransport *http.Transport func InitAppPluginRoutes(r *macaron.Macaron) { + pluginProxyTransport = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: setting.PluginAppsSkipVerifyTLS, + Renegotiation: tls.RenegotiateFreelyAsClient, + }, + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + } + for _, plugin := range plugins.Apps { for _, route := range plugin.Routes { url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, route.Path) diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go index ee5f6915b66..c917b0d9feb 100644 --- a/pkg/api/dtos/annotations.go +++ b/pkg/api/dtos/annotations.go @@ -29,3 +29,10 @@ type DeleteAnnotationsCmd struct { AnnotationId int64 `json:"annotationId"` RegionId int64 `json:"regionId"` } + +type PostGraphiteAnnotationsCmd struct { + When int64 `json:"when"` + What string `json:"what"` + Data string `json:"data"` + Tags interface{} `json:"tags"` +} diff --git a/pkg/api/grafana_com_proxy.go b/pkg/api/grafana_com_proxy.go index 015f690adda..a2a446b48eb 100644 --- a/pkg/api/grafana_com_proxy.go +++ b/pkg/api/grafana_com_proxy.go @@ -1,7 +1,6 @@ package api import ( - "crypto/tls" "net" "net/http" "net/http/httputil" @@ -14,8 +13,7 @@ import ( ) var grafanaComProxyTransport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, - Proxy: http.ProxyFromEnvironment, + Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index b43d55b2a8f..3107dcf7e4b 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -11,6 +11,8 @@ import ( "path" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" gocache "github.com/patrickmn/go-cache" @@ -19,7 +21,6 @@ import ( "github.com/grafana/grafana/pkg/api/live" httpstatic "github.com/grafana/grafana/pkg/api/static" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/middleware" @@ -153,7 +154,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron { for _, route := range plugins.StaticRoutes { pluginRoute := path.Join("/public/plugins/", route.PluginId) - logger.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory) + hs.log.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory) hs.mapStatic(m, route.Directory, "", pluginRoute) } @@ -187,7 +188,9 @@ func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) { return } - promhttp.Handler().ServeHTTP(ctx.Resp, ctx.Req.Request) + promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{ + DisableCompression: true, + }).ServeHTTP(ctx.Resp, ctx.Req.Request) } func (hs *HttpServer) healthHandler(ctx *macaron.Context) { diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 4be49915fd9..847f09f0eb8 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io/ioutil" - "log" "net/http" "net/url" @@ -16,6 +15,7 @@ import ( "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -29,6 +29,7 @@ var ( ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter") ErrUsersQuotaReached = errors.New("Users quota reached") ErrNoEmail = errors.New("Login provider didn't return an email address") + oauthLogger = log.New("oauth.login") ) func GenStateString() string { @@ -50,10 +51,11 @@ func OAuthLogin(ctx *middleware.Context) { return } - error := ctx.Query("error") - if error != "" { + errorParam := ctx.Query("error") + if errorParam != "" { errorDesc := ctx.Query("error_description") - redirectWithError(ctx, ErrProviderDeniedRequest, "error", error, "errorDesc", errorDesc) + oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc) + redirectWithError(ctx, ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc) return } @@ -69,8 +71,12 @@ func OAuthLogin(ctx *middleware.Context) { return } - // verify state string - savedState := ctx.Session.Get(middleware.SESS_KEY_OAUTH_STATE).(string) + savedState, ok := ctx.Session.Get(middleware.SESS_KEY_OAUTH_STATE).(string) + if !ok { + ctx.Handle(500, "login.OAuthLogin(missing saved state)", nil) + return + } + queryState := ctx.Query("state") if savedState != queryState { ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil) @@ -78,36 +84,37 @@ func OAuthLogin(ctx *middleware.Context) { } // handle call back + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: setting.OAuthService.OAuthInfos[name].TlsSkipVerify, + }, + } + oauthClient := &http.Client{ + Transport: tr, + } - // initialize oauth2 context - oauthCtx := oauth2.NoContext - if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" { + if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" || setting.OAuthService.OAuthInfos[name].TlsClientKey != "" { cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey) if err != nil { - log.Fatal(err) + log.Fatal(1, "Failed to setup TlsClientCert", "oauth provider", name, "error", err) } - // Load CA cert + tr.TLSClientConfig.Certificates = append(tr.TLSClientConfig.Certificates, cert) + } + + if setting.OAuthService.OAuthInfos[name].TlsClientCa != "" { caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa) if err != nil { - log.Fatal(err) + log.Fatal(1, "Failed to setup TlsClientCa", "oauth provider", name, "error", err) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) - tr := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - Certificates: []tls.Certificate{cert}, - RootCAs: caCertPool, - }, - } - sslcli := &http.Client{Transport: tr} - - oauthCtx = context.Background() - oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli) + tr.TLSClientConfig.RootCAs = caCertPool } + oauthCtx := context.WithValue(context.Background(), oauth2.HTTPClient, oauthClient) + // get token from provider token, err := connect.Exchange(oauthCtx, code) if err != nil { diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index 73548c3b159..86eb6bc271a 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -17,8 +17,6 @@ var version = "master" func main() { setupLogging() - services.Init(version) - app := cli.NewApp() app.Name = "Grafana cli" app.Usage = "" @@ -44,12 +42,20 @@ func main() { Value: "", EnvVar: "GF_PLUGIN_URL", }, + cli.BoolFlag{ + Name: "insecure", + Usage: "Skip TLS verification (insecure)", + }, cli.BoolFlag{ Name: "debug, d", Usage: "enable debug logging", }, } + app.Before = func(c *cli.Context) error { + services.Init(version, c.GlobalBool("insecure")) + return nil + } app.Commands = commands.Commands app.CommandNotFound = cmdNotFound diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index d3a05430944..d13e90d6a2f 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -22,7 +22,7 @@ var ( grafanaVersion string ) -func Init(version string) { +func Init(version string, skipTLSVerify bool) { grafanaVersion = version tr := &http.Transport{ @@ -36,8 +36,9 @@ func Init(version string) { IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, - - TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: skipTLSVerify, + }, } HttpClient = http.Client{ diff --git a/pkg/models/datasource_cache.go b/pkg/models/datasource_cache.go index 158018b0f0a..b4a4e7f8a4d 100644 --- a/pkg/models/datasource_cache.go +++ b/pkg/models/datasource_cache.go @@ -3,6 +3,7 @@ package models import ( "crypto/tls" "crypto/x509" + "errors" "net" "net/http" "sync" @@ -45,9 +46,16 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { return t.Transport, nil } + var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool + if ds.JsonData != nil { + tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false) + tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false) + tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false) + } + transport := &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: tlsSkipVerify, Renegotiation: tls.RenegotiateFreelyAsClient, }, Proxy: http.ProxyFromEnvironment, @@ -62,30 +70,24 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { 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 - + if tlsClientAuth || tlsAuthWithCACert { 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 + if !ok { + return nil, errors.New("Failed to parse TLS CA PEM certificate") } + transport.TLSClientConfig.RootCAs = caPool } - cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"])) - if err != nil { - return nil, err + if tlsClientAuth { + cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"])) + if err != nil { + return nil, err + } + transport.TLSClientConfig.Certificates = []tls.Certificate{cert} } - transport.TLSClientConfig.Certificates = []tls.Certificate{cert} } ptc.cache[ds.Id] = cachedTransport{ diff --git a/pkg/models/datasource_cache_test.go b/pkg/models/datasource_cache_test.go index 5e821ea28c4..85ece0bbdcc 100644 --- a/pkg/models/datasource_cache_test.go +++ b/pkg/models/datasource_cache_test.go @@ -29,61 +29,140 @@ func TestDataSourceCache(t *testing.T) { Convey("Should be using the cached proxy", func() { So(t2, ShouldEqual, t1) }) + Convey("Should verify TLS by default", func() { + So(t1.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false) + }) + Convey("Should have no TLS client certificate configured", func() { + So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0) + }) + Convey("Should have no user-supplied TLS CA onfigured", func() { + So(t1.TLSClientConfig.RootCAs, ShouldBeNil) + }) }) - Convey("When getting kubernetes datasource proxy", t, func() { + Convey("When caching a datasource proxy then updating it", t, func() { + clearCache() + setting.SecretKey = "password" + + json := simplejson.New() + json.Set("tlsAuthWithCACert", true) + + tlsCaCert, err := util.Encrypt([]byte(caCert), "password") + So(err, ShouldBeNil) + ds := DataSource{ + Id: 1, + Url: "http://k8s:8001", + Type: "Kubernetes", + SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert}, + Updated: time.Now().Add(-2 * time.Minute), + } + + t1, err := ds.GetHttpTransport() + So(err, ShouldBeNil) + + Convey("Should verify TLS by default", func() { + So(t1.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false) + }) + Convey("Should have no TLS client certificate configured", func() { + So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0) + }) + Convey("Should have no user-supplied TLS CA configured", func() { + So(t1.TLSClientConfig.RootCAs, ShouldBeNil) + }) + + ds.JsonData = nil + ds.SecureJsonData = map[string][]byte{} + ds.Updated = time.Now() + + t2, err := ds.GetHttpTransport() + So(err, ShouldBeNil) + + Convey("Should have no user-supplied TLS CA configured after the update", func() { + So(t2.TLSClientConfig.RootCAs, ShouldBeNil) + }) + }) + + Convey("When caching a datasource proxy with TLS client authentication enabled", t, func() { clearCache() setting.SecretKey = "password" json := simplejson.New() json.Set("tlsAuth", true) + + tlsClientCert, err := util.Encrypt([]byte(clientCert), "password") + So(err, ShouldBeNil) + tlsClientKey, err := util.Encrypt([]byte(clientKey), "password") + So(err, ShouldBeNil) + + ds := DataSource{ + Id: 1, + Url: "http://k8s:8001", + Type: "Kubernetes", + JsonData: json, + SecureJsonData: map[string][]byte{ + "tlsClientCert": tlsClientCert, + "tlsClientKey": tlsClientKey, + }, + } + + tr, err := ds.GetHttpTransport() + So(err, ShouldBeNil) + + Convey("Should verify TLS by default", func() { + So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false) + }) + Convey("Should have a TLS client certificate configured", func() { + So(len(tr.TLSClientConfig.Certificates), ShouldEqual, 1) + }) + }) + + Convey("When caching a datasource proxy with a user-supplied TLS CA", t, func() { + clearCache() + setting.SecretKey = "password" + + json := simplejson.New() json.Set("tlsAuthWithCACert", true) - t := time.Now() + tlsCaCert, err := util.Encrypt([]byte(caCert), "password") + So(err, ShouldBeNil) + ds := DataSource{ - Url: "http://k8s:8001", - Type: "Kubernetes", - Updated: t.Add(-2 * time.Minute), + Id: 1, + Url: "http://k8s:8001", + Type: "Kubernetes", + JsonData: json, + SecureJsonData: map[string][]byte{"tlsCACert": tlsCaCert}, } - transport, err := ds.GetHttpTransport() + tr, err := ds.GetHttpTransport() So(err, ShouldBeNil) - Convey("Should have no cert", func() { - So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true) + Convey("Should verify TLS by default", func() { + So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false) }) + Convey("Should have a TLS CA configured", func() { + So(len(tr.TLSClientConfig.RootCAs.Subjects()), ShouldEqual, 1) + }) + }) - ds.JsonData = json + Convey("When caching a datasource proxy when user skips TLS verification", t, func() { + clearCache() - tlsCaCert, _ := util.Encrypt([]byte(caCert), "password") - tlsClientCert, _ := util.Encrypt([]byte(clientCert), "password") - tlsClientKey, _ := util.Encrypt([]byte(clientKey), "password") + json := simplejson.New() + json.Set("tlsSkipVerify", true) - ds.SecureJsonData = map[string][]byte{ - "tlsCACert": tlsCaCert, - "tlsClientCert": tlsClientCert, - "tlsClientKey": tlsClientKey, + ds := DataSource{ + Id: 1, + Url: "http://k8s:8001", + Type: "Kubernetes", + JsonData: json, } - ds.Updated = t.Add(-1 * time.Minute) - transport, err = ds.GetHttpTransport() + tr, 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) + Convey("Should skip TLS verification", func() { + So(tr.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true) }) }) } @@ -115,7 +194,8 @@ FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n 3lb92xM= -----END CERTIFICATE-----` -const clientCert string = `-----BEGIN CERTIFICATE----- +const clientCert string = ` +-----BEGIN CERTIFICATE----- MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go new file mode 100644 index 00000000000..92f6489106b --- /dev/null +++ b/pkg/services/alerting/notifiers/kafka.go @@ -0,0 +1,120 @@ +package notifiers + +import ( + "strconv" + + "fmt" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "kafka", + Name: "Kafka REST Proxy", + Description: "Sends notifications to Kafka Rest Proxy", + Factory: NewKafkaNotifier, + OptionsTemplate: ` +

Kafka settings

+
+ Kafka REST Proxy + +
+
+ Topic + +
+ `, + }) +} + +func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + endpoint := model.Settings.Get("kafkaRestProxy").MustString() + if endpoint == "" { + return nil, alerting.ValidationError{Reason: "Could not find kafka rest proxy endpoint property in settings"} + } + topic := model.Settings.Get("kafkaTopic").MustString() + if topic == "" { + return nil, alerting.ValidationError{Reason: "Could not find kafka topic property in settings"} + } + + return &KafkaNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + Endpoint: endpoint, + Topic: topic, + log: log.New("alerting.notifier.kafka"), + }, nil +} + +type KafkaNotifier struct { + NotifierBase + Endpoint string + Topic string + log log.Logger +} + +func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error { + + state := evalContext.Rule.State + + customData := "Triggered metrics:\n\n" + for _, evt := range evalContext.EvalMatches { + customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value) + } + + this.log.Info("Notifying Kafka", "alert_state", state) + + recordJSON := simplejson.New() + records := make([]interface{}, 1) + + bodyJSON := simplejson.New() + bodyJSON.Set("description", evalContext.Rule.Name+" - "+evalContext.Rule.Message) + bodyJSON.Set("client", "Grafana") + bodyJSON.Set("details", customData) + bodyJSON.Set("incident_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) + + ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("Failed get rule link", "error", err) + return err + } + bodyJSON.Set("client_url", ruleUrl) + + if evalContext.ImagePublicUrl != "" { + contexts := make([]interface{}, 1) + imageJSON := simplejson.New() + imageJSON.Set("type", "image") + imageJSON.Set("src", evalContext.ImagePublicUrl) + contexts[0] = imageJSON + bodyJSON.Set("contexts", contexts) + } + + valueJSON := simplejson.New() + valueJSON.Set("value", bodyJSON) + records[0] = valueJSON + recordJSON.Set("records", records) + body, _ := recordJSON.MarshalJSON() + + topicUrl := this.Endpoint + "/topics/" + this.Topic + + cmd := &m.SendWebhookSync{ + Url: topicUrl, + Body: string(body), + HttpMethod: "POST", + HttpHeader: map[string]string{ + "Content-Type": "application/vnd.kafka.json.v2+json", + "Accept": "application/vnd.kafka.v2+json", + }, + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send notification to Kafka", "error", err, "body", string(body)) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/kafka_test.go b/pkg/services/alerting/notifiers/kafka_test.go new file mode 100644 index 00000000000..045976cb14b --- /dev/null +++ b/pkg/services/alerting/notifiers/kafka_test.go @@ -0,0 +1,55 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestKafkaNotifier(t *testing.T) { + Convey("Kafka 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: "kafka_testing", + Type: "kafka", + Settings: settingsJSON, + } + + _, err := NewKafkaNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("settings should send an event to kafka", func() { + json := ` + { + "kafkaRestProxy": "http://localhost:8082", + "kafkaTopic": "topic1" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "kafka_testing", + Type: "kafka", + Settings: settingsJSON, + } + + not, err := NewKafkaNotifier(model) + kafkaNotifier := not.(*KafkaNotifier) + + So(err, ShouldBeNil) + So(kafkaNotifier.Name, ShouldEqual, "kafka_testing") + So(kafkaNotifier.Type, ShouldEqual, "kafka") + So(kafkaNotifier.Endpoint, ShouldEqual, "http://localhost:8082") + So(kafkaNotifier.Topic, ShouldEqual, "topic1") + }) + + }) + }) +} diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index bc589f89c14..33a4cae53c2 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -100,13 +100,13 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { sql.WriteString(")") } + sql.WriteString(" ORDER BY name ASC") + if query.Limit != 0 { sql.WriteString(" LIMIT ?") params = append(params, query.Limit) } - sql.WriteString(" ORDER BY name ASC") - alerts := make([]*m.Alert, 0) if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil { return err diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index f2ba16fa675..ca65fe581af 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -122,6 +122,9 @@ var ( // Basic Auth BasicAuthEnabled bool + // Plugin settings + PluginAppsSkipVerifyTLS bool + // Session settings. SessionOptions session.Options @@ -560,6 +563,9 @@ func NewConfigContext(args *CommandLineArgs) error { authBasic := Cfg.Section("auth.basic") BasicAuthEnabled = authBasic.Key("enabled").MustBool(true) + // global plugin settings + PluginAppsSkipVerifyTLS = Cfg.Section("plugins").Key("app_tls_skip_verify_insecure").MustBool(false) + // PhantomJS rendering ImagesDir = filepath.Join(DataPath, "png") PhantomDir = filepath.Join(HomePath, "vendor/phantomjs") diff --git a/pkg/setting/setting_oauth.go b/pkg/setting/setting_oauth.go index bc52d2336c3..ee2e812415b 100644 --- a/pkg/setting/setting_oauth.go +++ b/pkg/setting/setting_oauth.go @@ -13,6 +13,7 @@ type OAuthInfo struct { TlsClientCert string TlsClientKey string TlsClientCa string + TlsSkipVerify bool } type OAuther struct { diff --git a/pkg/social/social.go b/pkg/social/social.go index 9d2a53946c7..d40c0a0c965 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -66,6 +66,7 @@ func NewOAuthService() { TlsClientCert: sec.Key("tls_client_cert").String(), TlsClientKey: sec.Key("tls_client_key").String(), TlsClientCa: sec.Key("tls_client_ca").String(), + TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(), } if !info.Enabled { diff --git a/public/app/core/components/colorpicker/spectrum_picker.ts b/public/app/core/components/colorpicker/spectrum_picker.ts new file mode 100644 index 00000000000..c262eaac326 --- /dev/null +++ b/public/app/core/components/colorpicker/spectrum_picker.ts @@ -0,0 +1,23 @@ +/** + * Wrapper for the new ngReact directive for backward compatibility. + * Allows remaining untouched in outdated plugins. + * Technically, it's just a wrapper for react component with two-way data binding support. + */ +import coreModule from '../../core_module'; + +export function spectrumPicker() { + return { + restrict: 'E', + require: 'ngModel', + scope: true, + replace: true, + template: '', + link: function(scope, element, attrs, ngModel) { + scope.ngModel = ngModel; + scope.onColorChange = (color) => { + ngModel.$setViewValue(color); + }; + } + }; +} +coreModule.directive('spectrumPicker', spectrumPicker); diff --git a/public/app/core/controllers/error_ctrl.js b/public/app/core/controllers/error_ctrl.js index cc711f07a22..fd4081186be 100644 --- a/public/app/core/controllers/error_ctrl.js +++ b/public/app/core/controllers/error_ctrl.js @@ -1,19 +1,21 @@ define([ 'angular', + 'app/core/config', '../core_module', ], -function (angular, coreModule) { +function (angular, config, coreModule) { 'use strict'; coreModule.default.controller('ErrorCtrl', function($scope, contextSrv, navModelSrv) { $scope.navModel = navModelSrv.getNotFoundNav(); + $scope.appSubUrl = config.appSubUrl; var showSideMenu = contextSrv.sidemenu; contextSrv.sidemenu = false; $scope.$on('$destroy', function() { - $scope.contextSrv.sidemenu = showSideMenu; + contextSrv.sidemenu = showSideMenu; }); }); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 3b4fdd68611..e28f8d2d7eb 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -17,6 +17,7 @@ import './components/code_editor/code_editor'; import './utils/outline'; import './components/colorpicker/ColorPicker'; import './components/colorpicker/SeriesColorPicker'; +import './components/colorpicker/spectrum_picker'; import {grafanaAppDirective} from './components/grafana_app'; import {sideMenuDirective} from './components/sidemenu/sidemenu'; diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 37352556819..60dc61ef7f2 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -46,6 +46,7 @@ function (_, $, coreModule) { segment.html = selected.html || selected.value; segment.fake = false; segment.expandable = selected.expandable; + segment.type = selected.type; } else if (segment.custom !== 'false') { segment.value = value; diff --git a/public/app/core/utils/version.ts b/public/app/core/utils/version.ts new file mode 100644 index 00000000000..6ee1400df51 --- /dev/null +++ b/public/app/core/utils/version.ts @@ -0,0 +1,34 @@ +import _ from 'lodash'; + +const versionPattern = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z\.]+))?/; + +export class SemVersion { + major: number; + minor: number; + patch: number; + meta: string; + + constructor(version: string) { + let match = versionPattern.exec(version); + if (match) { + this.major = Number(match[1]); + this.minor = Number(match[2] || 0); + this.patch = Number(match[3] || 0); + this.meta = match[4]; + } + } + + isGtOrEq(version: string): boolean { + let compared = new SemVersion(version); + return !(this.major < compared.major || this.minor < compared.minor || this.patch < compared.patch); + } + + isValid(): boolean { + return _.isNumber(this.major); + } +} + +export function isVersionGtOrEq(a: string, b: string): boolean { + let a_semver = new SemVersion(a); + return a_semver.isGtOrEq(b); +} diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 677eec31060..25c23580ed7 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 "opsgenie": return "fa fa-bell"; case "hipchat": return "fa fa-mail-forward"; case "pushover": return "fa fa-mobile"; + case "kafka": return "fa fa-random"; } 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 9b716e02ce6..b1e72a30fbb 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -40,13 +40,11 @@
-
- +
+
-
-
- -
+
+
diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index c8c95b38392..c950d3edd55 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -66,7 +66,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, tooltip += '
'; if (text) { - tooltip += '
' + sanitizeString(text).replace(/\n/g, '
') + '
'; + tooltip += '
' + sanitizeString(text.replace(/\n/g, '
')) + '
'; } var tags = event.tags; diff --git a/public/app/features/dashboard/unsaved_changes_modal.ts b/public/app/features/dashboard/unsaved_changes_modal.ts index cacfbe0f045..ab3ece1b8a2 100644 --- a/public/app/features/dashboard/unsaved_changes_modal.ts +++ b/public/app/features/dashboard/unsaved_changes_modal.ts @@ -18,7 +18,7 @@ const template = `