mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'graphite-seriesbytag' of https://github.com/alexanderzobnin/grafana into graphite-series-by-tags
This commit is contained in:
commit
af9941e50a
@ -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)
|
||||
|
||||
|
28
PLUGIN_DEV.md
Normal file
28
PLUGIN_DEV.md
Normal file
@ -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 `<spectrum-picker>` is no deprecated (will still work for a version more) but we recommend plugin authors
|
||||
to upgrade to new `<color-picker color="ctrl.color" onChange="ctrl.onSparklineColorChange"></color-picker>`
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
74
docs/sources/guides/whats-new-in-v4-6.md
Normal file
74
docs/sources/guides/whats-new-in-v4-6.md
Normal file
@ -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
|
||||
|
@ -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
|
||||
```
|
||||
|
@ -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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
<!-- adding annoation gif animation -->
|
||||
{{< 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.
|
||||
|
||||
<!-- region image/gif animation -->
|
||||
{{< 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)`.
|
||||
|
||||
<!-- image of built in query -->
|
||||
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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
|
120
pkg/services/alerting/notifiers/kafka.go
Normal file
120
pkg/services/alerting/notifiers/kafka.go
Normal file
@ -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: `
|
||||
<h3 class="page-heading">Kafka settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Kafka REST Proxy</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.kafkaRestProxy" placeholder="http://localhost:8082"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Topic</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.kafkaTopic" placeholder="topic1"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
55
pkg/services/alerting/notifiers/kafka_test.go
Normal file
55
pkg/services/alerting/notifiers/kafka_test.go
Normal file
@ -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")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -13,6 +13,7 @@ type OAuthInfo struct {
|
||||
TlsClientCert string
|
||||
TlsClientKey string
|
||||
TlsClientCa string
|
||||
TlsSkipVerify bool
|
||||
}
|
||||
|
||||
type OAuther struct {
|
||||
|
@ -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 {
|
||||
|
23
public/app/core/components/colorpicker/spectrum_picker.ts
Normal file
23
public/app/core/components/colorpicker/spectrum_picker.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Wrapper for the new ngReact <color-picker> directive for backward compatibility.
|
||||
* Allows remaining <spectrum-picker> 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: '<color-picker color="ngModel.$viewValue" onChange="onColorChange"></color-picker>',
|
||||
link: function(scope, element, attrs, ngModel) {
|
||||
scope.ngModel = ngModel;
|
||||
scope.onColorChange = (color) => {
|
||||
ngModel.$setViewValue(color);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
coreModule.directive('spectrumPicker', spectrumPicker);
|
@ -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;
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
34
public/app/core/utils/version.ts
Normal file
34
public/app/core/utils/version.ts
Normal file
@ -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);
|
||||
}
|
@ -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';
|
||||
}
|
||||
|
@ -40,13 +40,11 @@
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form width-6">
|
||||
<button type="submit" ng-click="ctrl.save()" class="btn btn-success">Save</button>
|
||||
<div class="gf-form width-8">
|
||||
<button type="submit" ng-click="ctrl.save()" class="btn btn-success width-7">Save</button>
|
||||
</div>
|
||||
<div class="gf-form width-20">
|
||||
<div class="gf-form">
|
||||
<button type="submit" ng-click="ctrl.testNotification()" class="btn btn-secondary">Send Test</button>
|
||||
</div>
|
||||
<div class="gf-form width-8">
|
||||
<button type="submit" ng-click="ctrl.testNotification()" class="btn btn-secondary width-7">Send Test</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,7 +66,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
|
||||
tooltip += '<div class="graph-annotation__body">';
|
||||
|
||||
if (text) {
|
||||
tooltip += '<div>' + sanitizeString(text).replace(/\n/g, '<br>') + '</div>';
|
||||
tooltip += '<div>' + sanitizeString(text.replace(/\n/g, '<br>')) + '</div>';
|
||||
}
|
||||
|
||||
var tags = event.tags;
|
||||
|
@ -18,7 +18,7 @@ const template = `
|
||||
<div class="modal-content text-center">
|
||||
|
||||
<div class="confirm-modal-text">
|
||||
Do you want to save you changes?
|
||||
Do you want to save your changes?
|
||||
</div>
|
||||
|
||||
<div class="confirm-modal-buttons">
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
<div class="page-container">
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-header">
|
||||
<h1 ng-show="ctrl.isNew">Add data source</h1>
|
||||
<h1 ng-hide="ctrl.isNew">Edit data source</h1>
|
||||
|
||||
@ -22,11 +22,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.tabIndex === 0" class="tab-content">
|
||||
<div ng-if="ctrl.tabIndex === 0" class="tab-content">
|
||||
|
||||
<form name="ctrl.editForm" ng-if="ctrl.current">
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<form name="ctrl.editForm" ng-if="ctrl.current">
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-7">Name</span>
|
||||
<input class="gf-form-input max-width-23" type="text" ng-model="ctrl.current.name" placeholder="name" required>
|
||||
@ -39,7 +39,7 @@
|
||||
<gf-form-switch class="gf-form" label="Default" checked="ctrl.current.isDefault" switch-class="max-width-6"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-7">Type</span>
|
||||
<div class="gf-form-select-wrapper max-width-23">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.type" ng-options="v.id as v.name for v in ctrl.types" ng-change="ctrl.userChangedType()"></select>
|
||||
@ -61,8 +61,8 @@
|
||||
<h5 ng-show="!ctrl.testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
|
||||
<div class="alert-{{ctrl.testing.status}} alert" ng-show="ctrl.testing.done">
|
||||
<div class="alert-icon">
|
||||
<i class="fa fa-exclamation-triangle" ng-show="ctrl.testing.status === 'error'"></i>
|
||||
<i class="fa fa-check" ng-show="ctrl.testing.status !== 'error'"></i>
|
||||
<i class="fa fa-exclamation-triangle" ng-show="ctrl.testing.status === 'error'"></i>
|
||||
<i class="fa fa-check" ng-show="ctrl.testing.status !== 'error'"></i>
|
||||
</div>
|
||||
<div class="alert-body">
|
||||
<div class="alert-title">{{ctrl.testing.message}}</div>
|
||||
@ -71,9 +71,9 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-show="ctrl.isNew" ng-click="ctrl.saveChanges()">Add</button>
|
||||
<button type="submit" class="btn btn-success" ng-show="!ctrl.isNew" ng-click="ctrl.saveChanges()">Save & Test</button>
|
||||
<button type="submit" class="btn btn-danger" ng-show="!ctrl.isNew" ng-click="ctrl.delete()">
|
||||
<button type="submit" class="btn btn-success width-6" ng-show="ctrl.isNew" ng-click="ctrl.saveChanges()">Add</button>
|
||||
<button type="submit" class="btn btn-success width-8" ng-show="!ctrl.isNew" ng-click="ctrl.saveChanges()">Save & Test</button>
|
||||
<button type="submit" class="btn btn-danger width-6" ng-show="!ctrl.isNew" ng-click="ctrl.delete()">
|
||||
Delete
|
||||
</button>
|
||||
<a class="btn btn-link" href="datasources">Cancel</a>
|
||||
|
@ -1,23 +1,23 @@
|
||||
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h3 class="page-heading">Http settings</h3>
|
||||
<h3 class="page-heading">HTTP settings</h3>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-7">Url</span>
|
||||
<span class="gf-form-label width-7">URL</span>
|
||||
<input class="gf-form-input" type="text"
|
||||
ng-model='current.url' placeholder="{{suggestUrl}}"
|
||||
bs-typeahead="getSuggestUrls" min-length="0"
|
||||
ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
|
||||
<info-popover mode="right-absolute">
|
||||
<p>Specify a complete HTTP url (for example http://your_server:8080)</p>
|
||||
<p>Specify a complete HTTP URL (for example http://your_server:8080)</p>
|
||||
<span ng-show="current.access === 'direct'">
|
||||
Your access method is <em>Direct</em>, this means the url
|
||||
Your access method is <em>Direct</em>, this means the URL
|
||||
needs to be accessible from the browser.
|
||||
</span>
|
||||
<span ng-show="current.access === 'proxy'">
|
||||
Your access method is currently <em>Proxy</em>, this means the url
|
||||
Your access method is currently <em>Proxy</em>, this means the URL
|
||||
needs to be accessible from the grafana backend.
|
||||
</span>
|
||||
</info-popover>
|
||||
@ -30,7 +30,7 @@
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24">
|
||||
<select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
|
||||
<info-popover mode="right-absolute">
|
||||
Direct = url is used directly from browser<br>
|
||||
Direct = URL is used directly from browser<br>
|
||||
Proxy = Grafana backend will proxy the request
|
||||
</info-popover>
|
||||
</div>
|
||||
@ -38,27 +38,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="page-heading">Http Auth</h3>
|
||||
<h3 class="page-heading">HTTP Auth</h3>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" label="Basic Auth" checked="current.basicAuth" label-class="width-8" switch-class="max-width-6"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests." checked="current.withCredentials" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="TLS Client Auth" label-class="width-8" checked="current.jsonData.tlsAuth" switch-class="max-width-6"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="With CA Cert" tooltip="Needed for verifing self-signed TLS Certs" checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Basic Auth"
|
||||
checked="current.basicAuth" label-class="width-8" switch-class="max-width-6">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests."
|
||||
checked="current.withCredentials" label-class="width-11" switch-class="max-width-6">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'"
|
||||
label="TLS Client Auth" label-class="width-8"
|
||||
checked="current.jsonData.tlsAuth" switch-class="max-width-6">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'"
|
||||
label="With CA Cert" tooltip="Optional. Needed for self-signed TLS Certs."
|
||||
checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="Skip TLS Verification (Insecure)" label-class="width-16" checked="current.jsonData.tlsSkipVerify" switch-class="max-width-6"></gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -79,7 +73,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="current.jsonData.tlsAuth && current.access=='proxy'">
|
||||
<div class="gf-form-group" ng-if="(current.jsonData.tlsAuth || current.jsonData.tlsAuthWithCACert) && current.access=='proxy'">
|
||||
<div class="gf-form">
|
||||
<h6>TLS Auth Details</h6>
|
||||
<info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover>
|
||||
@ -90,7 +84,7 @@
|
||||
<label class="gf-form-label width-7">CA Cert</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsCACert">
|
||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsCACert" placeholder="Begins with -----BEGIN CERTIFICATE-----. The CA Certificate is necessary if you are using self-signed certificates."></textarea>
|
||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsCACert" placeholder="Begins with -----BEGIN CERTIFICATE-----"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsCACert">
|
||||
@ -100,29 +94,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<label class="gf-form-label width-7">Client Cert</label>
|
||||
<div ng-if="current.jsonData.tlsAuth">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<label class="gf-form-label width-7">Client Cert</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert">
|
||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert">
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert">
|
||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert">
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<label class="gf-form-label width-7">Client Key</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey">
|
||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<label class="gf-form-label width-7">Client Key</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey">
|
||||
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
|
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -42,8 +42,8 @@
|
||||
</div>
|
||||
<div>
|
||||
<h3>Sorry for the inconvenience</h3>
|
||||
<p>Please go back to your <a href="#" class="error-link">home dashboard</a> and try again.</p>
|
||||
<p>If the error persists, seek help on the <a href="#" class="error-link">community site</a>.</p>
|
||||
<p>Please go back to your <a href="{{appSubUrl}}/" class="error-link">home dashboard</a> and try again.</p>
|
||||
<p>If the error persists, seek help on the <a href="https://community.grafana.com" target="_blank" class="error-link">community site</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,12 +2,26 @@
|
||||
|
||||
export class GraphiteConfigCtrl {
|
||||
static templateUrl = 'public/app/plugins/datasource/graphite/partials/config.html';
|
||||
datasourceSrv: any;
|
||||
current: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope) {
|
||||
constructor($scope, datasourceSrv) {
|
||||
this.datasourceSrv = datasourceSrv;
|
||||
this.current.jsonData = this.current.jsonData || {};
|
||||
this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9';
|
||||
|
||||
this.autoDetectGraphiteVersion();
|
||||
}
|
||||
|
||||
autoDetectGraphiteVersion() {
|
||||
this.datasourceSrv.loadDatasource(this.current.name)
|
||||
.then((ds) => {
|
||||
return ds.getVersion();
|
||||
}).then((version) => {
|
||||
this.graphiteVersions.push({name: version, value: version});
|
||||
this.current.jsonData.graphiteVersion = version;
|
||||
});
|
||||
}
|
||||
|
||||
graphiteVersions = [
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import _ from 'lodash';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import {isVersionGtOrEq, SemVersion} from 'app/core/utils/version';
|
||||
|
||||
/** @ngInject */
|
||||
export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
|
||||
@ -9,6 +10,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
this.url = instanceSettings.url;
|
||||
this.name = instanceSettings.name;
|
||||
this.graphiteVersion = instanceSettings.jsonData.graphiteVersion || '0.9';
|
||||
this.supportsTags = supportsTags(this.graphiteVersion);
|
||||
this.cacheTimeout = instanceSettings.cacheTimeout;
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
this.render_method = instanceSettings.render_method || 'POST';
|
||||
@ -245,7 +247,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
this.getTagValues = function(tag, optionalOptions) {
|
||||
let options = optionalOptions || {};
|
||||
|
||||
let httpOptions: any = {
|
||||
let httpOptions: any = {
|
||||
method: 'GET',
|
||||
url: '/tags/' + tag,
|
||||
// for cancellations
|
||||
@ -271,6 +273,72 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
});
|
||||
};
|
||||
|
||||
this.getTagsAutoComplete = (expression, tagPrefix) => {
|
||||
let httpOptions: any = {
|
||||
method: 'GET',
|
||||
url: '/tags/autoComplete/tags',
|
||||
params: {
|
||||
expr: expression
|
||||
}
|
||||
};
|
||||
|
||||
if (tagPrefix) {
|
||||
httpOptions.params.tagPrefix = tagPrefix;
|
||||
}
|
||||
|
||||
return this.doGraphiteRequest(httpOptions).then(results => {
|
||||
if (results.data) {
|
||||
return _.map(results.data, (tag) => {
|
||||
return { text: tag };
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.getTagValuesAutoComplete = (expression, tag, valuePrefix) => {
|
||||
let httpOptions: any = {
|
||||
method: 'GET',
|
||||
url: '/tags/autoComplete/values',
|
||||
params: {
|
||||
expr: expression,
|
||||
tag: tag
|
||||
}
|
||||
};
|
||||
|
||||
if (valuePrefix) {
|
||||
httpOptions.params.valuePrefix = valuePrefix;
|
||||
}
|
||||
|
||||
return this.doGraphiteRequest(httpOptions).then(results => {
|
||||
if (results.data) {
|
||||
return _.map(results.data, (value) => {
|
||||
return { text: value };
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.getVersion = function() {
|
||||
let httpOptions = {
|
||||
method: 'GET',
|
||||
url: '/version/_', // Prevent last / trimming
|
||||
};
|
||||
|
||||
return this.doGraphiteRequest(httpOptions).then(results => {
|
||||
if (results.data) {
|
||||
let semver = new SemVersion(results.data);
|
||||
return semver.isValid() ? results.data : '';
|
||||
}
|
||||
return '';
|
||||
}).catch(() => {
|
||||
return '';
|
||||
});
|
||||
};
|
||||
|
||||
this.testDatasource = function() {
|
||||
return this.metricFindQuery('*').then(function () {
|
||||
return { status: "success", message: "Data source is working"};
|
||||
@ -357,3 +425,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
return clean_options;
|
||||
};
|
||||
}
|
||||
|
||||
function supportsTags(version: string): boolean {
|
||||
return isVersionGtOrEq(version, '1.1');
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
define([
|
||||
'lodash',
|
||||
'jquery'
|
||||
'jquery',
|
||||
'app/core/utils/version'
|
||||
],
|
||||
function (_, $) {
|
||||
function (_, $, version) {
|
||||
'use strict';
|
||||
|
||||
var index = [];
|
||||
@ -944,13 +945,7 @@ function (_, $) {
|
||||
};
|
||||
|
||||
function isVersionRelatedFunction(func, graphiteVersion) {
|
||||
return isVersionGreaterOrEqual(graphiteVersion, func.version) || !func.version;
|
||||
}
|
||||
|
||||
function isVersionGreaterOrEqual(a, b) {
|
||||
var a_num = Number(a);
|
||||
var b_num = Number(b);
|
||||
return a_num >= b_num;
|
||||
return version.isVersionGtOrEq(graphiteVersion, func.version) || !func.version;
|
||||
}
|
||||
|
||||
return {
|
||||
|
283
public/app/plugins/datasource/graphite/graphite_query.ts
Normal file
283
public/app/plugins/datasource/graphite/graphite_query.ts
Normal file
@ -0,0 +1,283 @@
|
||||
import _ from 'lodash';
|
||||
import gfunc from './gfunc';
|
||||
import {Parser} from './parser';
|
||||
|
||||
export default class GraphiteQuery {
|
||||
target: any;
|
||||
functions: any[];
|
||||
segments: any[];
|
||||
tags: any[];
|
||||
error: any;
|
||||
seriesByTagUsed: boolean;
|
||||
checkOtherSegmentsIndex: number;
|
||||
removeTagValue: string;
|
||||
templateSrv: any;
|
||||
scopedVars: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(target, templateSrv?, scopedVars?) {
|
||||
this.target = target;
|
||||
this.parseTarget();
|
||||
|
||||
this.removeTagValue = '-- remove tag --';
|
||||
}
|
||||
|
||||
parseTarget() {
|
||||
this.functions = [];
|
||||
this.segments = [];
|
||||
this.error = null;
|
||||
|
||||
if (this.target.textEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
var parser = new Parser(this.target.target);
|
||||
var astNode = parser.getAst();
|
||||
if (astNode === null) {
|
||||
this.checkOtherSegmentsIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (astNode.type === 'error') {
|
||||
this.error = astNode.message + " at position: " + astNode.pos;
|
||||
this.target.textEditor = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.parseTargetRecursive(astNode, null, 0);
|
||||
} catch (err) {
|
||||
console.log('error parsing target:', err.message);
|
||||
this.error = err.message;
|
||||
this.target.textEditor = true;
|
||||
}
|
||||
|
||||
this.checkOtherSegmentsIndex = this.segments.length - 1;
|
||||
this.checkForSeriesByTag();
|
||||
}
|
||||
|
||||
checkForSeriesByTag() {
|
||||
let seriesByTagFunc = _.find(this.functions, (func) => func.def.name === 'seriesByTag');
|
||||
if (seriesByTagFunc) {
|
||||
this.seriesByTagUsed = true;
|
||||
seriesByTagFunc.hidden = true;
|
||||
let tags = this.splitSeriesByTagParams(seriesByTagFunc);
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
||||
|
||||
getSegmentPathUpTo(index) {
|
||||
var arr = this.segments.slice(0, index);
|
||||
|
||||
return _.reduce(arr, function(result, segment) {
|
||||
return result ? (result + "." + segment.value) : segment.value;
|
||||
}, "");
|
||||
}
|
||||
|
||||
parseTargetRecursive(astNode, func, index) {
|
||||
if (astNode === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (astNode.type) {
|
||||
case 'function':
|
||||
var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
|
||||
_.each(astNode.params, (param, index) => {
|
||||
this.parseTargetRecursive(param, innerFunc, index);
|
||||
});
|
||||
|
||||
innerFunc.updateText();
|
||||
this.functions.push(innerFunc);
|
||||
break;
|
||||
case 'series-ref':
|
||||
this.addFunctionParameter(func, astNode.value, index, this.segments.length > 0);
|
||||
break;
|
||||
case 'bool':
|
||||
case 'string':
|
||||
case 'number':
|
||||
if ((index-1) >= func.def.params.length) {
|
||||
throw { message: 'invalid number of parameters to method ' + func.def.name };
|
||||
}
|
||||
var shiftBack = this.isShiftParamsBack(func);
|
||||
this.addFunctionParameter(func, astNode.value, index, shiftBack);
|
||||
break;
|
||||
case 'metric':
|
||||
if (this.segments.length > 0) {
|
||||
if (astNode.segments.length !== 1) {
|
||||
throw { message: 'Multiple metric params not supported, use text editor.' };
|
||||
}
|
||||
this.addFunctionParameter(func, astNode.segments[0].value, index, true);
|
||||
break;
|
||||
}
|
||||
|
||||
this.segments = astNode.segments;
|
||||
}
|
||||
}
|
||||
|
||||
isShiftParamsBack(func) {
|
||||
return func.def.name !== 'seriesByTag';
|
||||
}
|
||||
|
||||
updateSegmentValue(segment, index) {
|
||||
this.segments[index].value = segment.value;
|
||||
}
|
||||
|
||||
addSelectMetricSegment() {
|
||||
this.segments.push({value: "select metric"});
|
||||
}
|
||||
|
||||
addFunction(newFunc) {
|
||||
this.functions.push(newFunc);
|
||||
this.moveAliasFuncLast();
|
||||
}
|
||||
|
||||
moveAliasFuncLast() {
|
||||
var aliasFunc = _.find(this.functions, function(func) {
|
||||
return func.def.name === 'alias' ||
|
||||
func.def.name === 'aliasByNode' ||
|
||||
func.def.name === 'aliasByMetric';
|
||||
});
|
||||
|
||||
if (aliasFunc) {
|
||||
this.functions = _.without(this.functions, aliasFunc);
|
||||
this.functions.push(aliasFunc);
|
||||
}
|
||||
}
|
||||
|
||||
addFunctionParameter(func, value, index, shiftBack) {
|
||||
if (shiftBack) {
|
||||
index = Math.max(index - 1, 0);
|
||||
}
|
||||
func.params[index] = value;
|
||||
}
|
||||
|
||||
removeFunction(func) {
|
||||
this.functions = _.without(this.functions, func);
|
||||
}
|
||||
|
||||
updateModelTarget(targets) {
|
||||
// render query
|
||||
if (!this.target.textEditor) {
|
||||
var metricPath = this.getSegmentPathUpTo(this.segments.length);
|
||||
this.target.target = _.reduce(this.functions, wrapFunction, metricPath);
|
||||
}
|
||||
|
||||
this.updateRenderedTarget(this.target, targets);
|
||||
|
||||
// loop through other queries and update targetFull as needed
|
||||
for (const target of targets || []) {
|
||||
if (target.refId !== this.target.refId) {
|
||||
this.updateRenderedTarget(target, targets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateRenderedTarget(target, targets) {
|
||||
// render nested query
|
||||
var targetsByRefId = _.keyBy(targets, 'refId');
|
||||
|
||||
// no references to self
|
||||
delete targetsByRefId[target.refId];
|
||||
|
||||
var nestedSeriesRefRegex = /\#([A-Z])/g;
|
||||
var targetWithNestedQueries = target.target;
|
||||
|
||||
// Keep interpolating until there are no query references
|
||||
// The reason for the loop is that the referenced query might contain another reference to another query
|
||||
while (targetWithNestedQueries.match(nestedSeriesRefRegex)) {
|
||||
var updated = targetWithNestedQueries.replace(nestedSeriesRefRegex, (match, g1) => {
|
||||
var t = targetsByRefId[g1];
|
||||
if (!t) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// no circular references
|
||||
delete targetsByRefId[g1];
|
||||
return t.target;
|
||||
});
|
||||
|
||||
if (updated === targetWithNestedQueries) {
|
||||
break;
|
||||
}
|
||||
|
||||
targetWithNestedQueries = updated;
|
||||
}
|
||||
|
||||
delete target.targetFull;
|
||||
if (target.target !== targetWithNestedQueries) {
|
||||
target.targetFull = targetWithNestedQueries;
|
||||
}
|
||||
}
|
||||
|
||||
splitSeriesByTagParams(func) {
|
||||
const tagPattern = /([^\!=~]+)([\!=~]+)([^\!=~]+)/;
|
||||
return _.flatten(_.map(func.params, (param: string) => {
|
||||
let matches = tagPattern.exec(param);
|
||||
if (matches) {
|
||||
let tag = matches.slice(1);
|
||||
if (tag.length === 3) {
|
||||
return {
|
||||
key: tag[0],
|
||||
operator: tag[1],
|
||||
value: tag[2]
|
||||
};
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}));
|
||||
}
|
||||
|
||||
getSeriesByTagFuncIndex() {
|
||||
return _.findIndex(this.functions, (func) => func.def.name === 'seriesByTag');
|
||||
}
|
||||
|
||||
getSeriesByTagFunc() {
|
||||
let seriesByTagFuncIndex = this.getSeriesByTagFuncIndex();
|
||||
if (seriesByTagFuncIndex >= 0) {
|
||||
return this.functions[seriesByTagFuncIndex];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
addTag(tag) {
|
||||
let newTagParam = renderTagString(tag);
|
||||
this.getSeriesByTagFunc().params.push(newTagParam);
|
||||
this.tags.push(tag);
|
||||
}
|
||||
|
||||
removeTag(index) {
|
||||
this.getSeriesByTagFunc().params.splice(index, 1);
|
||||
this.tags.splice(index, 1);
|
||||
}
|
||||
|
||||
updateTag(tag, tagIndex) {
|
||||
this.error = null;
|
||||
|
||||
if (tag.key === this.removeTagValue) {
|
||||
this.removeTag(tagIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
let newTagParam = renderTagString(tag);
|
||||
this.getSeriesByTagFunc().params[tagIndex] = newTagParam;
|
||||
this.tags[tagIndex] = tag;
|
||||
}
|
||||
|
||||
renderTagExpressions(excludeIndex = -1) {
|
||||
return _.compact(_.map(this.tags, (tagExpr, index) => {
|
||||
// Don't render tag that we want to lookup
|
||||
if (index !== excludeIndex) {
|
||||
return tagExpr.key + tagExpr.operator + tagExpr.value;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function wrapFunction(target, func) {
|
||||
return func.render(target);
|
||||
}
|
||||
|
||||
function renderTagString(tag) {
|
||||
return tag.key + tag.operator + tag.value;
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.target.textEditor">
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.target.target" spellcheck="false" ng-blur="ctrl.targetTextChanged()"></input>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.target.textEditor">
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.target.target" spellcheck="false" ng-blur="ctrl.targetTextChanged()"></input>
|
||||
</div>
|
||||
|
||||
<div ng-hide="ctrl.target.textEditor">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form" ng-if="ctrl.seriesByTagUsed">
|
||||
<label class="gf-form-label query-keyword">seriesByTag</label>
|
||||
<div class="gf-form-inline" ng-if="ctrl.queryModel.seriesByTagUsed">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-6 query-keyword">Tags</label>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="tag in ctrl.tags" class="gf-form">
|
||||
<div ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
|
||||
<gf-form-dropdown model="tag.key" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-key"
|
||||
get-options="ctrl.getTags()"
|
||||
get-options="ctrl.getTags($index, $query)"
|
||||
on-change="ctrl.tagChanged(tag, $index)">
|
||||
</gf-form-dropdown>
|
||||
<gf-form-dropdown model="tag.operator" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-operator"
|
||||
@ -20,24 +20,33 @@
|
||||
on-change="ctrl.tagChanged(tag, $index)">
|
||||
</gf-form-dropdown>
|
||||
<gf-form-dropdown model="tag.value" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-value"
|
||||
get-options="ctrl.getTagValues(tag)"
|
||||
get-options="ctrl.getTagValues(tag, $index, $query)"
|
||||
on-change="ctrl.tagChanged(tag, $index)">
|
||||
</gf-form-dropdown>
|
||||
<label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">,</label>
|
||||
</div>
|
||||
<div ng-if="ctrl.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
|
||||
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
|
||||
<metric-segment segment="segment"
|
||||
get-options="ctrl.getTagsAsSegments()"
|
||||
on-change="ctrl.addNewTag(segment)">
|
||||
</metric-segment>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form" ng-if="ctrl.queryModel.seriesByTagUsed">
|
||||
<label class="gf-form-label width-6 query-keyword">Functions</label>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
|
||||
<metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="func in ctrl.functions" class="gf-form">
|
||||
<span graphite-func-editor class="gf-form-label query-part" ng-hide="ctrl.getSeriesByTagFuncIndex() === $index"></span>
|
||||
<div ng-repeat="func in ctrl.queryModel.functions" class="gf-form">
|
||||
<span graphite-func-editor class="gf-form-label query-part" ng-hide="func.hidden"></span>
|
||||
</div>
|
||||
|
||||
<div class="gf-form dropdown">
|
||||
@ -45,8 +54,8 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -3,141 +3,70 @@ import './func_editor';
|
||||
|
||||
import _ from 'lodash';
|
||||
import gfunc from './gfunc';
|
||||
import {Parser} from './parser';
|
||||
import GraphiteQuery from './graphite_query';
|
||||
import {QueryCtrl} from 'app/plugins/sdk';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
const GRAPHITE_TAG_OPERATORS = ['=', '!=', '=~', '!=~'];
|
||||
const TAG_PREFIX = 'tag: ';
|
||||
|
||||
export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
static templateUrl = 'partials/query.editor.html';
|
||||
|
||||
functions: any[];
|
||||
queryModel: GraphiteQuery;
|
||||
segments: any[];
|
||||
addTagSegments: any[];
|
||||
tags: any[];
|
||||
seriesByTagUsed: boolean;
|
||||
removeTagValue: string;
|
||||
supportsTags: boolean;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor($scope, $injector, private uiSegmentSrv, private templateSrv) {
|
||||
super($scope, $injector);
|
||||
this.supportsTags = this.datasource.supportsTags;
|
||||
|
||||
if (this.target) {
|
||||
this.target.target = this.target.target || '';
|
||||
this.parseTarget();
|
||||
this.queryModel = new GraphiteQuery(this.target, templateSrv);
|
||||
this.buildSegments();
|
||||
}
|
||||
|
||||
this.removeTagValue = '-- remove tag --';
|
||||
}
|
||||
|
||||
parseTarget() {
|
||||
this.queryModel.parseTarget();
|
||||
this.buildSegments();
|
||||
}
|
||||
|
||||
toggleEditorMode() {
|
||||
this.target.textEditor = !this.target.textEditor;
|
||||
this.parseTarget();
|
||||
}
|
||||
|
||||
parseTarget() {
|
||||
this.functions = [];
|
||||
this.segments = [];
|
||||
this.error = null;
|
||||
buildSegments() {
|
||||
this.segments = _.map(this.queryModel.segments, segment => {
|
||||
return this.uiSegmentSrv.newSegment(segment);
|
||||
});
|
||||
let checkOtherSegmentsIndex = this.queryModel.checkOtherSegmentsIndex || 0;
|
||||
this.checkOtherSegments(checkOtherSegmentsIndex);
|
||||
|
||||
if (this.target.textEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
var parser = new Parser(this.target.target);
|
||||
var astNode = parser.getAst();
|
||||
if (astNode === null) {
|
||||
this.checkOtherSegments(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (astNode.type === 'error') {
|
||||
this.error = astNode.message + " at position: " + astNode.pos;
|
||||
this.target.textEditor = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.parseTargetRecursive(astNode, null, 0);
|
||||
} catch (err) {
|
||||
console.log('error parsing target:', err.message);
|
||||
this.error = err.message;
|
||||
this.target.textEditor = true;
|
||||
}
|
||||
|
||||
this.checkOtherSegments(this.segments.length - 1);
|
||||
this.checkForSeriesByTag();
|
||||
}
|
||||
|
||||
addFunctionParameter(func, value, index, shiftBack) {
|
||||
if (shiftBack) {
|
||||
index = Math.max(index - 1, 0);
|
||||
}
|
||||
func.params[index] = value;
|
||||
}
|
||||
|
||||
parseTargetRecursive(astNode, func, index) {
|
||||
if (astNode === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (astNode.type) {
|
||||
case 'function':
|
||||
var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
|
||||
_.each(astNode.params, (param, index) => {
|
||||
this.parseTargetRecursive(param, innerFunc, index);
|
||||
});
|
||||
|
||||
innerFunc.updateText();
|
||||
this.functions.push(innerFunc);
|
||||
break;
|
||||
case 'series-ref':
|
||||
this.addFunctionParameter(func, astNode.value, index, this.segments.length > 0);
|
||||
break;
|
||||
case 'bool':
|
||||
case 'string':
|
||||
case 'number':
|
||||
if ((index-1) >= func.def.params.length) {
|
||||
throw { message: 'invalid number of parameters to method ' + func.def.name };
|
||||
}
|
||||
var shiftBack = this.isShiftParamsBack(func);
|
||||
this.addFunctionParameter(func, astNode.value, index, shiftBack);
|
||||
break;
|
||||
case 'metric':
|
||||
if (this.segments.length > 0) {
|
||||
if (astNode.segments.length !== 1) {
|
||||
throw { message: 'Multiple metric params not supported, use text editor.' };
|
||||
}
|
||||
this.addFunctionParameter(func, astNode.segments[0].value, index, true);
|
||||
break;
|
||||
}
|
||||
|
||||
this.segments = _.map(astNode.segments, segment => {
|
||||
return this.uiSegmentSrv.newSegment(segment);
|
||||
});
|
||||
if (this.queryModel.seriesByTagUsed) {
|
||||
this.fixTagSegments();
|
||||
}
|
||||
}
|
||||
|
||||
isShiftParamsBack(func) {
|
||||
return func.def.name !== 'seriesByTag';
|
||||
}
|
||||
|
||||
getSegmentPathUpTo(index) {
|
||||
var arr = this.segments.slice(0, index);
|
||||
|
||||
return _.reduce(arr, function(result, segment) {
|
||||
return result ? (result + "." + segment.value) : segment.value;
|
||||
}, "");
|
||||
addSelectMetricSegment() {
|
||||
this.queryModel.addSelectMetricSegment();
|
||||
this.segments.push(this.uiSegmentSrv.newSelectMetric());
|
||||
}
|
||||
|
||||
checkOtherSegments(fromIndex) {
|
||||
if (fromIndex === 0) {
|
||||
this.segments.push(this.uiSegmentSrv.newSelectMetric());
|
||||
this.addSelectMetricSegment();
|
||||
return;
|
||||
}
|
||||
|
||||
var path = this.getSegmentPathUpTo(fromIndex + 1);
|
||||
var path = this.queryModel.getSegmentPathUpTo(fromIndex + 1);
|
||||
if (path === "") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@ -145,12 +74,13 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
return this.datasource.metricFindQuery(path).then(segments => {
|
||||
if (segments.length === 0) {
|
||||
if (path !== '') {
|
||||
this.queryModel.segments = this.queryModel.segments.splice(0, fromIndex);
|
||||
this.segments = this.segments.splice(0, fromIndex);
|
||||
this.segments.push(this.uiSegmentSrv.newSelectMetric());
|
||||
this.addSelectMetricSegment();
|
||||
}
|
||||
} else if (segments[0].expandable) {
|
||||
if (this.segments.length === fromIndex) {
|
||||
this.segments.push(this.uiSegmentSrv.newSelectMetric());
|
||||
this.addSelectMetricSegment();
|
||||
} else {
|
||||
return this.checkOtherSegments(fromIndex + 1);
|
||||
}
|
||||
@ -166,12 +96,8 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
wrapFunction(target, func) {
|
||||
return func.render(target);
|
||||
}
|
||||
|
||||
getAltSegments(index) {
|
||||
var query = index === 0 ? '*' : this.getSegmentPathUpTo(index) + '.*';
|
||||
var query = index === 0 ? '*' : this.queryModel.getSegmentPathUpTo(index) + '.*';
|
||||
var options = {range: this.panelCtrl.range, requestId: "get-alt-segments"};
|
||||
|
||||
return this.datasource.metricFindQuery(query, options).then(segments => {
|
||||
@ -192,17 +118,44 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
|
||||
// add wildcard option
|
||||
altSegments.unshift(this.uiSegmentSrv.newSegment('*'));
|
||||
return altSegments;
|
||||
|
||||
if (this.supportsTags && index === 0) {
|
||||
this.removeTaggedEntry(altSegments);
|
||||
return this.addAltTagSegments(index, altSegments);
|
||||
} else {
|
||||
return altSegments;
|
||||
}
|
||||
}).catch(err => {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
addAltTagSegments(index, altSegments) {
|
||||
return this.getTagsAsSegments().then((tagSegments) => {
|
||||
tagSegments = _.map(tagSegments, (segment) => {
|
||||
segment.value = TAG_PREFIX + segment.value;
|
||||
return segment;
|
||||
});
|
||||
return altSegments.concat(...tagSegments);
|
||||
});
|
||||
}
|
||||
|
||||
removeTaggedEntry(altSegments) {
|
||||
altSegments = _.remove(altSegments, (s) => s.value === '_tagged');
|
||||
}
|
||||
|
||||
segmentValueChanged(segment, segmentIndex) {
|
||||
this.error = null;
|
||||
this.queryModel.updateSegmentValue(segment, segmentIndex);
|
||||
|
||||
if (this.functions.length > 0 && this.functions[0].def.fake) {
|
||||
this.functions = [];
|
||||
if (this.queryModel.functions.length > 0 && this.queryModel.functions[0].def.fake) {
|
||||
this.queryModel.functions = [];
|
||||
}
|
||||
|
||||
if (segment.type === 'tag') {
|
||||
let tag = removeTagPrefix(segment.value);
|
||||
this.addSeriesByTagFunc(tag);
|
||||
return;
|
||||
}
|
||||
|
||||
if (segment.expandable) {
|
||||
@ -211,81 +164,41 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
this.targetChanged();
|
||||
});
|
||||
} else {
|
||||
this.segments = this.segments.splice(0, segmentIndex + 1);
|
||||
this.spliceSegments(segmentIndex + 1);
|
||||
}
|
||||
|
||||
this.setSegmentFocus(segmentIndex + 1);
|
||||
this.targetChanged();
|
||||
}
|
||||
|
||||
spliceSegments(index) {
|
||||
this.segments = this.segments.splice(0, index);
|
||||
this.queryModel.segments = this.queryModel.segments.splice(0, index);
|
||||
}
|
||||
|
||||
emptySegments() {
|
||||
this.queryModel.segments = [];
|
||||
this.segments = [];
|
||||
}
|
||||
|
||||
targetTextChanged() {
|
||||
this.updateModelTarget();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
updateModelTarget() {
|
||||
// render query
|
||||
if (!this.target.textEditor) {
|
||||
var metricPath = this.getSegmentPathUpTo(this.segments.length);
|
||||
this.target.target = _.reduce(this.functions, this.wrapFunction, metricPath);
|
||||
}
|
||||
|
||||
this.updateRenderedTarget(this.target);
|
||||
|
||||
// loop through other queries and update targetFull as needed
|
||||
for (const target of this.panelCtrl.panel.targets || []) {
|
||||
if (target.refId !== this.target.refId) {
|
||||
this.updateRenderedTarget(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateRenderedTarget(target) {
|
||||
// render nested query
|
||||
var targetsByRefId = _.keyBy(this.panelCtrl.panel.targets, 'refId');
|
||||
|
||||
// no references to self
|
||||
delete targetsByRefId[target.refId];
|
||||
|
||||
var nestedSeriesRefRegex = /\#([A-Z])/g;
|
||||
var targetWithNestedQueries = target.target;
|
||||
|
||||
// Keep interpolating until there are no query references
|
||||
// The reason for the loop is that the referenced query might contain another reference to another query
|
||||
while (targetWithNestedQueries.match(nestedSeriesRefRegex)) {
|
||||
var updated = targetWithNestedQueries.replace(nestedSeriesRefRegex, (match, g1) => {
|
||||
var t = targetsByRefId[g1];
|
||||
if (!t) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// no circular references
|
||||
delete targetsByRefId[g1];
|
||||
return t.target;
|
||||
});
|
||||
|
||||
if (updated === targetWithNestedQueries) {
|
||||
break;
|
||||
}
|
||||
|
||||
targetWithNestedQueries = updated;
|
||||
}
|
||||
|
||||
delete target.targetFull;
|
||||
if (target.target !== targetWithNestedQueries) {
|
||||
target.targetFull = targetWithNestedQueries;
|
||||
}
|
||||
this.queryModel.updateModelTarget(this.panelCtrl.panel.targets);
|
||||
}
|
||||
|
||||
targetChanged() {
|
||||
if (this.error) {
|
||||
if (this.queryModel.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
var oldTarget = this.target.target;
|
||||
var oldTarget = this.queryModel.target.target;
|
||||
this.updateModelTarget();
|
||||
|
||||
if (this.target.target !== oldTarget) {
|
||||
if (this.queryModel.target !== oldTarget) {
|
||||
var lastSegment = this.segments.length > 0 ? this.segments[this.segments.length - 1] : {};
|
||||
if (lastSegment.value !== 'select metric') {
|
||||
this.panelCtrl.refresh();
|
||||
@ -293,21 +206,14 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
removeFunction(func) {
|
||||
this.functions = _.without(this.functions, func);
|
||||
this.targetChanged();
|
||||
}
|
||||
|
||||
addFunction(funcDef) {
|
||||
var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
|
||||
newFunc.added = true;
|
||||
this.functions.push(newFunc);
|
||||
|
||||
this.moveAliasFuncLast();
|
||||
this.queryModel.addFunction(newFunc);
|
||||
this.smartlyHandleNewAliasByNode(newFunc);
|
||||
|
||||
if (this.segments.length === 1 && this.segments[0].fake) {
|
||||
this.segments = [];
|
||||
this.emptySegments();
|
||||
}
|
||||
|
||||
if (!newFunc.params.length && newFunc.added) {
|
||||
@ -319,17 +225,22 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
moveAliasFuncLast() {
|
||||
var aliasFunc = _.find(this.functions, function(func) {
|
||||
return func.def.name === 'alias' ||
|
||||
func.def.name === 'aliasByNode' ||
|
||||
func.def.name === 'aliasByMetric';
|
||||
});
|
||||
removeFunction(func) {
|
||||
this.queryModel.removeFunction(func);
|
||||
this.targetChanged();
|
||||
}
|
||||
|
||||
if (aliasFunc) {
|
||||
this.functions = _.without(this.functions, aliasFunc);
|
||||
this.functions.push(aliasFunc);
|
||||
}
|
||||
addSeriesByTagFunc(tag) {
|
||||
let funcDef = gfunc.getFuncDef('seriesByTag');
|
||||
let newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: false });
|
||||
let tagParam = `${tag}=select tag value`;
|
||||
newFunc.params = [tagParam];
|
||||
this.queryModel.addFunction(newFunc);
|
||||
newFunc.added = true;
|
||||
|
||||
this.emptySegments();
|
||||
this.targetChanged();
|
||||
this.parseTarget();
|
||||
}
|
||||
|
||||
smartlyHandleNewAliasByNode(func) {
|
||||
@ -338,7 +249,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.segments.length; i++) {
|
||||
if (this.segments[i].value.indexOf('*') >= 0) {
|
||||
if (this.segments[i].value.indexOf('*') >= 0) {
|
||||
func.params[0] = i;
|
||||
func.added = false;
|
||||
this.targetChanged();
|
||||
@ -347,39 +258,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// Graphite seriesByTag support //
|
||||
//////////////////////////////////
|
||||
|
||||
checkForSeriesByTag() {
|
||||
let seriesByTagFunc = _.find(this.functions, (func) => func.def.name === 'seriesByTag');
|
||||
if (seriesByTagFunc) {
|
||||
this.seriesByTagUsed = true;
|
||||
let tags = this.splitSeriesByTagParams(seriesByTagFunc);
|
||||
this.tags = tags;
|
||||
this.fixTagSegments();
|
||||
}
|
||||
}
|
||||
|
||||
splitSeriesByTagParams(func) {
|
||||
const tagPattern = /([^\!=~]+)([\!=~]+)([^\!=~]+)/;
|
||||
return _.flatten(_.map(func.params, (param: string) => {
|
||||
let matches = tagPattern.exec(param);
|
||||
if (matches) {
|
||||
let tag = matches.slice(1);
|
||||
if (tag.length === 3) {
|
||||
return {
|
||||
key: tag[0],
|
||||
operator: tag[1],
|
||||
value: tag[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}));
|
||||
}
|
||||
|
||||
getTags() {
|
||||
getAllTags() {
|
||||
return this.datasource.getTags().then((values) => {
|
||||
let altTags = _.map(values, 'text');
|
||||
altTags.splice(0, 0, this.removeTagValue);
|
||||
@ -387,10 +266,22 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
getTags(index, tagPrefix) {
|
||||
let tagExpressions = this.queryModel.renderTagExpressions(index);
|
||||
return this.datasource.getTagsAutoComplete(tagExpressions, tagPrefix)
|
||||
.then((values) => {
|
||||
let altTags = _.map(values, 'text');
|
||||
altTags.splice(0, 0, this.removeTagValue);
|
||||
return mapToDropdownOptions(altTags);
|
||||
});
|
||||
}
|
||||
|
||||
getTagsAsSegments() {
|
||||
return this.datasource.getTags().then((values) => {
|
||||
let tagExpressions = this.queryModel.renderTagExpressions();
|
||||
return this.datasource.getTagsAutoComplete(tagExpressions)
|
||||
.then((values) => {
|
||||
return _.map(values, (val) => {
|
||||
return this.uiSegmentSrv.newSegment(val.text);
|
||||
return this.uiSegmentSrv.newSegment({value: val.text, type: 'tag', expandable: false});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -399,7 +290,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
return mapToDropdownOptions(GRAPHITE_TAG_OPERATORS);
|
||||
}
|
||||
|
||||
getTagValues(tag) {
|
||||
getAllTagValues(tag) {
|
||||
let tagKey = tag.key;
|
||||
return this.datasource.getTagValues(tagKey).then((values) => {
|
||||
let altValues = _.map(values, 'text');
|
||||
@ -407,46 +298,30 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
getTagValues(tag, index, valuePrefix) {
|
||||
let tagExpressions = this.queryModel.renderTagExpressions(index);
|
||||
let tagKey = tag.key;
|
||||
return this.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix).then((values) => {
|
||||
let altValues = _.map(values, 'text');
|
||||
return mapToDropdownOptions(altValues);
|
||||
});
|
||||
}
|
||||
|
||||
tagChanged(tag, tagIndex) {
|
||||
this.error = null;
|
||||
|
||||
if (tag.key === this.removeTagValue) {
|
||||
this.removeTag(tagIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
let newTagParam = renderTagString(tag);
|
||||
this.getSeriesByTagFunc().params[tagIndex] = newTagParam;
|
||||
this.tags[tagIndex] = tag;
|
||||
this.queryModel.updateTag(tag, tagIndex);
|
||||
this.targetChanged();
|
||||
}
|
||||
|
||||
getSeriesByTagFuncIndex() {
|
||||
return _.findIndex(this.functions, (func) => func.def.name === 'seriesByTag');
|
||||
}
|
||||
|
||||
getSeriesByTagFunc() {
|
||||
let seriesByTagFuncIndex = this.getSeriesByTagFuncIndex();
|
||||
if (seriesByTagFuncIndex >= 0) {
|
||||
return this.functions[seriesByTagFuncIndex];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
addNewTag(segment) {
|
||||
let newTagKey = segment.value;
|
||||
let newTag = {key: newTagKey, operator: '=', value: 'select tag value'};
|
||||
let newTagParam = renderTagString(newTag);
|
||||
this.getSeriesByTagFunc().params.push(newTagParam);
|
||||
this.tags.push(newTag);
|
||||
this.queryModel.addTag(newTag);
|
||||
this.targetChanged();
|
||||
this.fixTagSegments();
|
||||
}
|
||||
|
||||
removeTag(index) {
|
||||
this.getSeriesByTagFunc().params.splice(index, 1);
|
||||
this.tags.splice(index, 1);
|
||||
this.queryModel.removeTag(index);
|
||||
this.targetChanged();
|
||||
}
|
||||
|
||||
@ -456,16 +331,16 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
|
||||
showDelimiter(index) {
|
||||
return index !== this.tags.length - 1;
|
||||
return index !== this.queryModel.tags.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTagString(tag) {
|
||||
return tag.key + tag.operator + tag.value;
|
||||
}
|
||||
|
||||
function mapToDropdownOptions(results) {
|
||||
return _.map(results, (value) => {
|
||||
return {text: value, value: value};
|
||||
});
|
||||
}
|
||||
|
||||
function removeTagPrefix(value: string): string {
|
||||
return value.replace(TAG_PREFIX, '');
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ describe('GraphiteQueryCtrl', function() {
|
||||
});
|
||||
|
||||
it('should parse expression and build function model', function() {
|
||||
expect(ctx.ctrl.functions.length).to.be(2);
|
||||
expect(ctx.ctrl.queryModel.functions.length).to.be(2);
|
||||
});
|
||||
});
|
||||
|
||||
@ -61,7 +61,7 @@ describe('GraphiteQueryCtrl', function() {
|
||||
});
|
||||
|
||||
it('should add function with correct node number', function() {
|
||||
expect(ctx.ctrl.functions[0].params[0]).to.be(2);
|
||||
expect(ctx.ctrl.queryModel.functions[0].params[0]).to.be(2);
|
||||
});
|
||||
|
||||
it('should update target', function() {
|
||||
@ -99,7 +99,7 @@ describe('GraphiteQueryCtrl', function() {
|
||||
});
|
||||
|
||||
it('should add both series refs as params', function() {
|
||||
expect(ctx.ctrl.functions[0].params.length).to.be(2);
|
||||
expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(2);
|
||||
});
|
||||
});
|
||||
|
||||
@ -115,7 +115,7 @@ describe('GraphiteQueryCtrl', function() {
|
||||
});
|
||||
|
||||
it('should add function param', function() {
|
||||
expect(ctx.ctrl.functions[0].params.length).to.be(1);
|
||||
expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -131,7 +131,7 @@ describe('GraphiteQueryCtrl', function() {
|
||||
});
|
||||
|
||||
it('should have correct func params', function() {
|
||||
expect(ctx.ctrl.functions[0].params.length).to.be(1);
|
||||
expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -219,11 +219,11 @@ describe('GraphiteQueryCtrl', function() {
|
||||
});
|
||||
|
||||
it('should update functions', function() {
|
||||
expect(ctx.ctrl.getSeriesByTagFuncIndex()).to.be(0);
|
||||
expect(ctx.ctrl.queryModel.getSeriesByTagFuncIndex()).to.be(0);
|
||||
});
|
||||
|
||||
it('should update seriesByTagUsed flag', function() {
|
||||
expect(ctx.ctrl.seriesByTagUsed).to.be(true);
|
||||
expect(ctx.ctrl.queryModel.seriesByTagUsed).to.be(true);
|
||||
});
|
||||
|
||||
it('should update target', function() {
|
||||
@ -247,7 +247,7 @@ describe('GraphiteQueryCtrl', function() {
|
||||
{key: 'tag1', operator: '=', value: 'value1'},
|
||||
{key: 'tag2', operator: '!=~', value: 'value2'}
|
||||
];
|
||||
expect(ctx.ctrl.tags).to.eql(expected);
|
||||
expect(ctx.ctrl.queryModel.tags).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should add plus button', function() {
|
||||
@ -267,7 +267,7 @@ describe('GraphiteQueryCtrl', function() {
|
||||
const expected = [
|
||||
{key: 'tag1', operator: '=', value: 'select tag value'}
|
||||
];
|
||||
expect(ctx.ctrl.tags).to.eql(expected);
|
||||
expect(ctx.ctrl.queryModel.tags).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should update target', function() {
|
||||
@ -289,7 +289,7 @@ describe('GraphiteQueryCtrl', function() {
|
||||
{key: 'tag1', operator: '=', value: 'new_value'},
|
||||
{key: 'tag2', operator: '!=~', value: 'value2'}
|
||||
];
|
||||
expect(ctx.ctrl.tags).to.eql(expected);
|
||||
expect(ctx.ctrl.queryModel.tags).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should update target', function() {
|
||||
@ -310,7 +310,7 @@ describe('GraphiteQueryCtrl', function() {
|
||||
const expected = [
|
||||
{key: 'tag2', operator: '!=~', value: 'value2'}
|
||||
];
|
||||
expect(ctx.ctrl.tags).to.eql(expected);
|
||||
expect(ctx.ctrl.queryModel.tags).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should update target', function() {
|
||||
|
@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-6">Decimals</label>
|
||||
<input type="number" class="gf-form-input max-width-20" placeholder="auto" bs-tooltip="'Override automatic decimal precision for y-axis'" data-placement="right" ng-model="yaxis.decimals" ng-change="ctrl.render()" ng-model-onblur>
|
||||
<input type="number" class="gf-form-input max-width-20" placeholder="auto" empty-to-null bs-tooltip="'Override automatic decimal precision for y-axis'" data-placement="right" ng-model="yaxis.decimals" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
|
@ -497,8 +497,8 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
|
||||
show: panel.yaxes[0].show,
|
||||
index: 1,
|
||||
logBase: panel.yaxes[0].logBase || 1,
|
||||
min: panel.yaxes[0].min ? _.toNumber(panel.yaxes[0].min) : null,
|
||||
max: panel.yaxes[0].max ? _.toNumber(panel.yaxes[0].max) : null,
|
||||
min: parseNumber(panel.yaxes[0].min),
|
||||
max: parseNumber(panel.yaxes[0].max),
|
||||
tickDecimals: panel.yaxes[0].decimals
|
||||
};
|
||||
|
||||
@ -510,9 +510,9 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
|
||||
secondY.show = panel.yaxes[1].show;
|
||||
secondY.logBase = panel.yaxes[1].logBase || 1;
|
||||
secondY.position = 'right';
|
||||
secondY.min = panel.yaxes[1].min ? _.toNumber(panel.yaxes[1].min) : null;
|
||||
secondY.max = panel.yaxes[1].max ? _.toNumber(panel.yaxes[1].max) : null;
|
||||
secondY.tickDecimals = panel.yaxes[1].decimals !== null ? _.toNumber(panel.yaxes[1].decimals): null;
|
||||
secondY.min = parseNumber(panel.yaxes[1].min);
|
||||
secondY.max = parseNumber(panel.yaxes[1].max);
|
||||
secondY.tickDecimals = panel.yaxes[1].decimals;
|
||||
options.yaxes.push(secondY);
|
||||
|
||||
applyLogScale(options.yaxes[1], data);
|
||||
@ -522,6 +522,14 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
|
||||
configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format);
|
||||
}
|
||||
|
||||
function parseNumber(value: any) {
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _.toNumber(value);
|
||||
}
|
||||
|
||||
function applyLogScale(axis, data) {
|
||||
if (axis.logBase === 1) {
|
||||
return;
|
||||
|
@ -43,7 +43,7 @@
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.error-link {color: $yellow;}
|
||||
.error-link {color: $orange;}
|
||||
|
||||
.error-minus {
|
||||
color: #7eb26d;
|
||||
@ -57,4 +57,4 @@
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.graph-text {margin: 0;}
|
||||
.graph-text {margin: 0;}
|
||||
|
55
public/test/core/utils/version_specs.ts
Normal file
55
public/test/core/utils/version_specs.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import {describe, beforeEach, it, expect} from 'test/lib/common';
|
||||
|
||||
import {SemVersion, isVersionGtOrEq} from 'app/core/utils/version';
|
||||
|
||||
describe("SemVersion", () => {
|
||||
let version = '1.0.0-alpha.1';
|
||||
|
||||
describe('parsing', () => {
|
||||
it('should parse version properly', () => {
|
||||
let semver = new SemVersion(version);
|
||||
expect(semver.major).to.be(1);
|
||||
expect(semver.minor).to.be(0);
|
||||
expect(semver.patch).to.be(0);
|
||||
expect(semver.meta).to.be('alpha.1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('comparing', () => {
|
||||
beforeEach(() => {
|
||||
version = '3.4.5';
|
||||
});
|
||||
|
||||
it('should detect greater version properly', () => {
|
||||
let semver = new SemVersion(version);
|
||||
let cases = [
|
||||
{value: '3.4.5', expected: true},
|
||||
{value: '3.4.4', expected: true},
|
||||
{value: '3.4.6', expected: false},
|
||||
{value: '4', expected: false},
|
||||
{value: '3.5', expected: false},
|
||||
];
|
||||
cases.forEach((testCase) => {
|
||||
expect(semver.isGtOrEq(testCase.value)).to.be(testCase.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isVersionGtOrEq', () => {
|
||||
it('should compare versions properly (a >= b)', () => {
|
||||
let cases = [
|
||||
{values: ['3.4.5', '3.4.5'], expected: true},
|
||||
{values: ['3.4.5', '3.4.4'] , expected: true},
|
||||
{values: ['3.4.5', '3.4.6'], expected: false},
|
||||
{values: ['3.4', '3.4.0'], expected: true},
|
||||
{values: ['3', '3.0.0'], expected: true},
|
||||
{values: ['3.1.1-beta1', '3.1'], expected: true},
|
||||
{values: ['3.4.5', '4'], expected: false},
|
||||
{values: ['3.4.5', '3.5'], expected: false},
|
||||
];
|
||||
cases.forEach((testCase) => {
|
||||
expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).to.be(testCase.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -358,6 +358,8 @@
|
||||
// Remove icon clicked
|
||||
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
|
||||
self.remove($(event.target).closest('.tag').data('item'));
|
||||
// Grafana mod, if tags input used in popover the click event will bubble up and hide popover
|
||||
event.stopPropagation();
|
||||
}, self));
|
||||
|
||||
// Only add existing value as tags when using strings as tags
|
||||
|
@ -2,8 +2,8 @@
|
||||
"rules": {
|
||||
"no-string-throw": true,
|
||||
"no-unused-expression": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-unused-variable": true,
|
||||
"no-duplicate-variable": true,
|
||||
"curly": true,
|
||||
"class-name": true,
|
||||
"semicolon": [true, "always", "ignore-bound-class-methods"],
|
||||
|
Loading…
Reference in New Issue
Block a user