Commit 5f2cb8e1 by Torkel Ödegaard

Merge branch 'master' into getting-started-panel

parents 15e369ec d92bb677
......@@ -14,9 +14,10 @@
* **OpenTSDB**: Add support for explicitTags for OpenTSDB>=2.3, closes [#6360](https://github.com/grafana/grafana/pull/6361)
* **OAuth**: Add support for generic oauth, closes [#4718](https://github.com/grafana/grafana/pull/4718)
* **Cloudwatch**: Add support to expand multi select template variable, closes [#5003](https://github.com/grafana/grafana/pull/5003)
* **Graph Panel**: Now supports flexible lower/upper bounds on Y-Max and Y-Min, PR [#5720](https://github.com/grafana/grafana/pull/5720)
* **Background Tasks**: Now support automatic purging of old snapshots, closes [#4087](https://github.com/grafana/grafana/issues/4087)
* **Background Tasks**: Now support automatic purging of old rendered images, closes [#2172](https://github.com/grafana/grafana/issues/2172)
* **Dashboard**: After inactivity hide nav/row actions, fade to nice clean view, can be toggled with `d v`, also added kiosk mode, toggled via `d k` [#6476](https://github.com/grafana/grafana/issues/6476)
* **Dashboard**: Improved dashboard row menu & add panel UX [#6442](https://github.com/grafana/grafana/issues/6442)
### Breaking changes
* **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971)
......@@ -30,6 +31,8 @@
* **Elasticsearch**: Fix for query template variable when looking up terms without query, no longer relies on elasticsearch default field, fixes [#3887](https://github.com/grafana/grafana/pull/3887)
* **Elasticsearch**: Fix for displaying IP address used in terms aggregations, fixes [#4393](https://github.com/grafana/grafana/pull/4393)
* **PNG Rendering**: Fix for server side rendering when using auth proxy, fixes [#5906](https://github.com/grafana/grafana/pull/5906)
* **OpenTSDB**: Fixed multi-value nested templating for opentsdb, fixes [#6455](https://github.com/grafana/grafana/pull/6455)
* **Playlist**: Remove playlist items when dashboard is removed, fixes [#6292](https://github.com/grafana/grafana/issues/6292)
# 3.1.2 (unreleased)
* **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)
......
elasticsearch:
image: elasticsearch:latest
image: elasticsearch:2.4.1
command: elasticsearch -Des.network.host=0.0.0.0
ports:
- "9200:9200"
......
<!--- +++ --->
<!--- title = "Alerting" --->
<!--- description = "Alerting" --->
<!--- keywords = ["grafana", "alerting", "guide"] --->
<!--- type = "docs" --->
<!--- [menu.docs] --->
<!--- name = "Alerting" --->
<!--- identifier = "alerting" --->
<!--- parent = "features" --->
<!--- weight = 6 --->
<!--- +++ --->
<!--- --->
<!--- # Alerting --->
<!--- --->
<!--- > Alerting is still in very early development. Please be aware. --->
<!--- --->
<!--- The roadmap for alerting in Grafana have been changing rapidly during last 2-3 months. So make sure you follow the disucssion in the [alerting issue](https://github.com/grafana/grafana/issues/2209). --->
<!--- --->
<!--- ## Introduction --->
<!--- --->
<!--- > Alerting is turned off by default and have to be enabled in the config file. --->
<!--- --->
<!--- Grafana lets you define alert rules based on metrics queries on dashboards. Every alert is connected to a panel and when ever the query for the panel is updated the alerting rule is also updated. --->
<!--- So far only the graph panel supports alerting. To enable alerting for a panel go to the alerting tab and press 'Create alert' button. --->
<!--- --->
<!--- ## Alert status page --->
<!--- --->
<!--- You can overview all your current alerts on the alert stats page at /alerting --->
<!--- --->
<!--- ## Alert notifications --->
<!--- --->
<!--- When an alert is triggered it goes to the notification handler who takes care of sending emails or push data as webhooks. --->
<!--- The alert notifications can be configured on /alerting/notifications --->
<!--- --->
+++
title = "Alerting"
type = "docs"
[menu.docs]
identifier = "alerting"
parent = "features"
weight = 6
+++
+++
title = "Alerting Notifications"
description = "Alerting Notifications Guide"
keywords = ["Grafana", "alerting", "guide", "notifications"]
type = "docs"
[menu.docs]
name = "Notifications"
parent = "alerting"
weight = 2
+++
# Alert Notifications
{{< imgbox max-width="40%" img="/img/docs/v4/alert_notifications_menu.png" caption="Alerting notifications" >}}
> Alerting is only available in Grafana v4.0 and above.
When an alert changes state it sends out notifications. Each alert rule can have
multiple notifications. But in order to add a notification to an alert rule you first need
to add and configure a `notification` object. This is done from the Alerting/Notifications page.
## Notification Setup
On the notifications list page hit the `New Notification` button to go the the page where you
can configure and setup a new notification.
You specify name and type, and type specific options. You can also test the notification to make
sure it's working and setup correctly.
### Send on all alerts
When checked this option will make this notification used for all alert rules, existing and new.
## Supported notification types
Grafana ships with a set of notification types. More will be added in future releases.
### Email
To enable email notification you have to setup [SMTP settings](/installation/configuration/#smtp)
in the Grafana config. Email notification will upload an image of the alert graph to an
external image destination if available or fallback on attaching the image in the email.
### Slack
{{< imgbox max-width="40%" img="/img/docs/v4/slack_notification.png" caption="Alerting Slack Notification" >}}
To set up slack you need to configure an incoming webhook url at slack. You can follow their guide for how
to do that https://api.slack.com/incoming-webhooks If you want to include screenshots of the firing alerts
in the slack messages you have to configure the [external image destination](#external-image-store) in Grafana.
Setting | Description
---------- | -----------
Recipient | allows you to override the slack recipient.
Mention | make it possible to include a mention in the slack notification sent by Grafana. Ex @here or @channel
### Webhook
The webhook notification is a simple way to send information about an state change over HTTP to a custom endpoint.
Using this notification you could integrated Grafana into any system you choose, by yourself.
Example json body:
```json
{
"title": "My alert",
"ruleId": 1,
"ruleName": "Load peaking!",
"ruleUrl": "http://url.to.grafana/db/dashboard/my_dashboard?panelId=2",
"state": "Alerting",
"imageUrl": "http://s3.image.url",
"evalMatches": [
{
"metric": "requests",
"tags": {},
"value": 122
}
]
}
```
### PagerDuty
To set up PagerDuty, all you have to do is to provide an api key.
> Our pagerduty integration only support trigger events at the moment. You have to resolve them by yourself.
# Enable images in notifications {#external-image-store}
Grafan can render the panel associated with the alert rule and include that in the notification. Some types
of notifications require that this image be publicly accessable (Slack for example). In order to support
images in notifications like Slack Grafana can upload the image to an image store. It currently supports
Amazon S3 for this and Webdav. So to set that up you need to configure the
[external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini
config file.
This is not an optional requirement, you can get slack and email notifications without setting this up.
+++
title = "Alerting Engine & Rules Guide"
description = "Configuring Alert Rules"
keywords = ["grafana", "alerting", "guide", "rules"]
type = "docs"
[menu.docs]
name = "Engine & Rules"
parent = "alerting"
weight = 1
+++
# Alerting Engine & Rules Guide
> Alerting is only available in Grafana v4.0 and above.
## Introduction
{{< imgbox max-width="40%" img="/img/docs/v4/drag_handles_gif.gif" caption="Alerting overview" >}}
Alerting in Grafana allows you to attach rules to your dashboard panels. When you save the dashboard
Grafana will extract the alert rules into a separate alert rule storage and schedule them for evaluation.
In the alert tab of the graph panel you can configure how often the alert rule should be evaluated
and the conditions that need to be met for the alert to change state and trigger its
[notifications]({{< relref "notifications.md" >}}).
## Execution
The alert rules are evaluated in the Grafana backend in a scheduler and query execution engine that is part
of core Grafana. Only some data soures are supported right now. They include `Graphite`, `Prometheus`,
`InfluxDB` and `OpenTSDB`.
### Clustering
We have not implemented clustering yet. So if you run multiple instances of grafana-server
you have to make sure [execute_alerts]({{< relref "/installation/configuration.md#alerting" >}})
is true on only one instance or otherwise you will get duplicated notifications.
<div class="clearfix"></div>
## Rule Config
{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}}
Currently only the graph panel supports alert rules but this will be added to the **Singlestat** and **Table**
panels as well in a future release.
### Name & Evaluation interval
Here you can specify the name of the alert rule and how often the scheduler should evaluate the alert rule.
### Conditions
Currently the only condition type that exists is a `Query` condition that allows you to
specify a query letter, time range and an aggregation function. The letter refers to
a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is
a single value that is then used in the threshold check. The query used in an alert rule cannot
contain any template variables. Currently we only support `AND` operator between conditions.
We plan to add other condition types in the future, like `Other Alert`, where you can include the state
of another alert in your conditions, and `Time Of Day`.
#### Multiple Series
If a query returns multiple series then the aggregation function and threshold check will be evaluated for each series.
What Grafana does not do currently is track alert rule state **per series**. This has implications that is exemplified
in the scenario below.
- Alert condition with query that returns 2 series: **server1** and **server2**
- **server1** series cause the alert rule to fire and switch to state `Alerting`
- Notifications are sent out with message: _load peaking (server1)_
- In a subsequence evaluation of the same alert rule the **server2** series also cause the alert rule to fire
- No new notifications are sent as the alert rule is already in state `Alerting`.
So as you can see from the above scenario Grafana will not send out notifications when other series cause the alert
to fire if the rule already is in state ´Alerting`. To improve support for queries that return multiple series
we plan to track state **per series** in a future release.
### No Data / Null values
Below you condition you can configure how the rule evaluation engine should handle queries that return no data or only null valued
data.
No Data Option | Description
------------ | -------------
NoData | Set alert rule state to `NoData`
Alerting | Set alert rule state to `Alerting`
Keep Last State | Keep the current alert rule state, what ever it is.
### Execution errors or timeouts
The last option is how to handle execution or timeout errors.
Error or timeout option | Description
------------ | -------------
Alerting | Set alert rule state to `Alerting`
Keep Last State | Keep the current alert rule state, what ever it is.
If you an unreliable time series store that where queries sometime timeout or fail randomly you can set this option
t `Keep Last State` to basically ignore them.
## Notifications
In alert tab you can also specify alert rule notifications along with a detailed messsage about the alert rule.
The message can contain anything, information about how you might solve the issue, link to runbook etc.
The actual notifications are configured and shared between multiple alerts. Read the
[Notifications]({{< relref "notifications.md" >}}) guide for how to configure and setup notifications.
## Troubleshooting
{{< imgbox max-width="40%" img="/img/docs/v4/alert_test_rule.png" caption="Test Rule" >}}
First level of troubleshooting you can do is hit the **Test Rule** button. You will get result back that you can expand
to the point where you can see the raw data that was returned form your query.
Further troubleshooting can also be done by inspecting the grafana-server log. If it's not an error or for some reason
the log does not say anything you can enable debug logging for some relevant components. This is done
in Grafana's ini config file.
Example showing loggers that could be relevant when troubleshooting alerting.
```ini
[log]
filters = alerting.scheduler:debug \
alerting.engine:debug \
alerting.resultHandler:debug \
alerting.evalHandler:debug \
alerting.evalContext:debug \
alerting.extractor:debug \
alerting.notifier:debug \
alerting.notifier.slack:debug \
alerting.notifier.pagerduty:debug \
alerting.notifier.email:debug \
alerting.notifier.webhook:debug \
tsdb.graphite:debug \
tsdb.prometheus:debug \
tsdb.opentsdb:debug \
tsdb.influxdb:debug \
```
If you want to log raw query sent to your TSDB and raw response in log you also have to set grafana.ini option `app_mode` to
`development`.
......@@ -16,7 +16,7 @@ Grafana ships with built in support for CloudWatch. You just have to add it as a
be ready to build dashboards for you CloudWatch metrics.
## Adding the data source
![](img/docs/cloudwatch/cloudwatch_add.png)
![](/img/docs/cloudwatch/cloudwatch_add.png)
1. Open the side menu by clicking the the Grafana icon in the top header.
2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
......@@ -25,6 +25,7 @@ be ready to build dashboards for you CloudWatch metrics.
3. Click the `Add new` link in the top header.
4. Select `CloudWatch` from the dropdown.
> NOTE: If at any moment you have issues with getting this datasource to work and grafana is giving you undescriptive errors then dont forget to check your log file (try looking in /var/log/grafana/).
Name | Description
------------ | -------------
......@@ -47,6 +48,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu
### AWS credentials file
Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server.
> NOTE: If you think you have the credentials file in the right place but it is still not working then you might try moving your .aws file to '/usr/share/grafana/' and make sure your credentials file has at most 0644 permissions.
Example content:
......@@ -58,7 +60,7 @@ Example content:
## Metric Query Editor
![](img/docs/cloudwatch/query_editor.png)
![](/img/docs/cloudwatch/query_editor.png)
You need to specify a namespace, metric, at least one stat, and at least one dimension.
......@@ -99,7 +101,7 @@ Example `ec2_instance_attribute()` query
ec2_instance_attribute(us-east-1, InstanceId, { "tag:Environment": [ "production" ] })
![](img/docs/v2/cloudwatch_templating.png)
![](/img/docs/v2/cloudwatch_templating.png)
## Cost
......
......@@ -17,7 +17,7 @@ also annotate your graphs with log events stored in elasticsearch.
## Adding the data source
![](img/docs/v2/add_Graphite.jpg)
![](/img/docs/v2/add_Graphite.jpg)
1. Open the side menu by clicking the the Grafana icon in the top header.
2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
......@@ -47,14 +47,14 @@ Elasticsearch from the browser. You do this by specifying these to options in yo
### Index settings
![](img/docs/elasticsearch/elasticsearch_ds_details.png)
![](/img/docs/elasticsearch/elasticsearch_ds_details.png)
Here you can specify a default for the `time field` and specify the name of your elasticsearch index. You can use
a time pattern for the index name or a wildcard.
## Metric Query editor
![](img/docs/elasticsearch/query_editor.png)
![](/img/docs/elasticsearch/query_editor.png)
The Elasticsearch query editor allows you to select multiple metrics and group by multiple terms or filters. Use the plus and minus icons to the right to add / remove
metrics or group bys. Some metrics and group by have options, click the option text to expand the the row to view and edit metric or group by options.
......@@ -66,7 +66,7 @@ If you have Elasticsearch 2.x and Grafana 2.6 or above then you can use pipeline
to hide metrics from appearing in the graph. This is useful for metrics you only have in the query to be used
in a pipeline metric.
![](img/docs/elasticsearch/pipeline_metrics_editor.png)
![](/img/docs/elasticsearch/pipeline_metrics_editor.png)
## Templating
......
......@@ -17,7 +17,7 @@ change function parameters and much more. The editor can handle all types of gra
queries through the use of query references.
## Adding the data source
![](img/docs/v2/add_Graphite.jpg)
![](/img/docs/v2/add_Graphite.jpg)
1. Open the side menu by clicking the the Grafana icon in the top header.
2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
......@@ -46,7 +46,7 @@ Direct access is still supported because in some cases it may be useful to acces
Click the ``Select metric`` link to start navigating the metric space. One you start you can continue using the mouse
or keyboard arrow keys. You can select a wildcard and still continue.
![](img/docs/animated_gifs/graphite_query1.gif)
![](/img/docs/animated_gifs/graphite_query1.gif)
### Functions
Click the plus icon to the right to add a function. You can search for the function or select it from the menu. Once
......@@ -54,13 +54,13 @@ a function is selected it will be added and your focus will be in the text box o
a parameter just click on it and it will turn into a text box. To delete a function click the function name followed
by the x icon.
![](img/docs/animated_gifs/graphite_query2.gif)
![](/img/docs/animated_gifs/graphite_query2.gif)
### Optional parameters
Some functions like aliasByNode support an optional second argument. To add this parameter specify for example 3,-2 as the first parameter and the function editor will adapt and move the -2 to a second parameter. To remove the second optional parameter just click on it and leave it blank and the editor will remove it.
![](img/docs/animated_gifs/func_editor_optional_params.gif)
![](/img/docs/animated_gifs/func_editor_optional_params.gif)
## Point consolidation
......@@ -80,7 +80,7 @@ values that exists in the wildcard position.
You can also create nested variables that use other variables in their definition. For example
`apps.$app.servers.*` uses the variable `$app` in its query definition.
![](img/docs/v2/templated_variable_parameter.png)
![](/img/docs/v2/templated_variable_parameter.png)
## Query Reference
......
......@@ -14,7 +14,7 @@ weight = 3
Grafana ships with very a feature data source plugin for InfluxDB. Supporting a feature rich query editor, annotation and templating queries.
## Adding the data source
![](img/docs/v2/add_Influx.jpg)
![](/img/docs/v2/add_Influx.jpg)
1. Open the side menu by clicking the the Grafana icon in the top header.
2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
......@@ -41,7 +41,7 @@ Password | Database user's password
## Query Editor
![](assets/img/blog/v2.6/influxdb_editor_v3.gif)
![](/assets/img/blog/v2.6/influxdb_editor_v3.gif)
You find the InfluxDB editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the
panel title, then edit. The editor allows you to select metrics and tags.
......@@ -60,7 +60,7 @@ In the `SELECT` row you can specify what fields and functions you want to use. I
group by time you need an aggregation function. Some functions like derivative require an aggregation function.
The editor tries simplify and unify this part of the query. For example:
![](img/docs/influxdb/select_editor.png)
![](/img/docs/influxdb/select_editor.png)
The above will generate the following InfluxDB `SELECT` clause:
......@@ -93,7 +93,7 @@ You can switch to raw query mode by clicking hamburger icon and then `Switch edi
### Table query / raw data
![](assets/img/blog/v2.6/table_influxdb_logs.png)
![](/assets/img/blog/v2.6/table_influxdb_logs.png)
You can remove the group by time by clicking on the `time` part and then the `x` icon. You can
change the option `Format As` to `Table` if you want to show raw data in the `Table` panel.
......
......@@ -8,7 +8,7 @@ page_keywords: grafana, kairosdb, documentation
Grafana v2.1 brings initial support for KairosDB Datasources. While the process of adding the datasource is similar to adding a Graphite or OpenTSDB datasource type, Kairos DB does have a few different options for building queries.
## Adding the data source to Grafana
![](img/v2/add_KairosDB.jpg)
![](/img/v2/add_KairosDB.jpg)
1. Open the side menu by clicking the the Grafana icon in the top header.
2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
......@@ -30,7 +30,7 @@ Access | Proxy = access via Grafana backend, Direct = access directly from brows
## Query editor
Open a graph in edit mode by click the title.
![](img/v2/kairos_query_editor.jpg)
![](/img/v2/kairos_query_editor.jpg)
For details on KairosDB metric queries checkout the official.
- [Query Metrics - KairosDB 0.9.4 documentation](http://kairosdb.github.io/kairosdocs/restapi/QueryMetrics.html).
......
......@@ -11,7 +11,7 @@ weight = 5
# Using OpenTSDB in Grafana
{{< docs-imagebox img="img/docs/v2/add_OpenTSDB.png" max-width="14rem" >}}
{{< docs-imagebox img="/img/docs/v2/add_OpenTSDB.png" max-width="14rem" >}}
The newest release of Grafana adds additional functionality when using an OpenTSDB Data source.
......@@ -37,7 +37,7 @@ Open a graph in edit mode by click the title. Query editor will differ if the da
> Note: While using Opentsdb 2.2 datasource, make sure you use either Filters or Tags as they are mutually exclusive. If used together, might give you weird results.
![](img/docs/v2/opentsdb_query_editor.png)
![](/img/docs/v2/opentsdb_query_editor.png)
### Auto complete suggestions
As soon as you start typing metric names, tag names and tag values , you should see highlighted auto complete suggestions for them.
......
......@@ -15,7 +15,7 @@ weight = 2
Grafana includes support for Prometheus Datasources. While the process of adding the datasource is similar to adding a Graphite or OpenTSDB datasource type, Prometheus does have a few different options for building queries.
## Adding the data source to Grafana
![](img/v2/add_Prometheus.png)
![](/img/v2/add_Prometheus.png)
1. Open the side menu by clicking the the Grafana icon in the top header.
2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
......@@ -42,7 +42,7 @@ Password | Database user's password
## Query editor
Open a graph in edit mode by click the title.
![](img/v2/prometheus_editor.png)
![](/img/v2/prometheus_editor.png)
For details on Prometheus metric queries check out the Prometheus documentation
- [Query Metrics - Prometheus documentation](http://prometheus.io/docs/querying/basics/).
......@@ -72,4 +72,4 @@ label_values(hostname)
You can also use raw queries & regular expressions to extract anything you might need.
![](img/v2/prometheus_templating.png)
![](/img/v2/prometheus_templating.png)
+++
title = "Keyboard Shortcuts"
type = "docs"
[menu.docs]
parent = "features"
weight = 7
+++
# Keyboard shortcuts
{{< docs-imagebox img="/img/docs/v4/shortcuts.png" max-width="20rem" >}}
Grafana v4 introduces a number of really powerful keyboard shortcuts. You can now focus a panel
by hovering over it with your mouse. With a panel focused you can simple hit `e` to toggle panel
edit mode, or `v` to toggle fullscreen mode. `p` `r` removes the panel. `p` `s` opens share
modal.
Hit `?` on your keyboard to open the shortcuts help modal.
### Global
- `g` `h` Go to Home Dashboard
- `g` `p` Go to Profile
- `s` `o` Open search
- `s` `s` Open search with starred filter
- `s` `t` Open search in tags view
- `esc` Exit edit/setting views
### Dashboard
- `mod+s` Save dashboard
- `mod+h` Hide row controls
- `d` `r` Refresh all panels
- `d` `s` Dashboard settings
- `d` `v` Toggle in-active / view mode
- `d` `k` Toggle kiosk mode (hides top nav)
- `mod+o` Toggle shared graph crosshair
### Focused Panel
- `e` Toggle panel edit view
- `v` Toggle panel fullscreen view
- `p` `s` Open Panel Share Modal
- `p` `r` Remove Panel
### Focused Row
- `r` `c` Collapse Row
- `r` `r` Remove Row
### Time Range
- `t` `z` Zoom out time range
- `t` Move time range back
- `t` Move time range forward
......@@ -22,7 +22,7 @@ Read the [Basic Concepts](/guides/basic_concepts) document to get a crash course
Let's start with creating a new Dashboard. You can find the new Dashboard link at the bottom of the Dashboard picker. You now have a blank Dashboard.
<img class="no-shadow" src="img/docs/v2/v2_top_nav_annotated.png">
<img class="no-shadow" src="/img/docs/v2/v2_top_nav_annotated.png">
The image above shows you the top header for a Dashboard.
......@@ -39,7 +39,7 @@ Dashboards are at the core of what Grafana is all about. Dashboards are composed
## Adding & Editing Graphs and Panels
![](img/docs/v2/graph_metrics_tab_graphite.png)
![](/img/docs/v2/graph_metrics_tab_graphite.png)
1. You add panels via row menu. The row menu is the green icon to the left of each row.
2. To edit the graph you click on the graph title to open the panel menu, then `Edit`.
......@@ -48,7 +48,7 @@ Dashboards are at the core of what Grafana is all about. Dashboards are composed
When you click the `Metrics` tab, you are presented with a Query Editor that is specific to the Panel Data Source. Use the Query Editor to build your queries and Grafana will visualize them in real time.
<img src="img/docs/v2/dashboard_annotated.png" class="no-shadow">
<img src="/img/docs/v2/dashboard_annotated.png" class="no-shadow">
1. Zoom out time range
2. Time picker dropdown. Here you can access relative time range options, auto refresh options and set custom absolute time ranges.
......@@ -61,7 +61,7 @@ When you click the `Metrics` tab, you are presented with a Query Editor that is
You can Drag-and-Drop Panels within and between Rows. Click and hold the Panel title, and drag it to its new location. You can also easily resize panels by clicking the (-) and (+) icons.
![](img/docs/animated_gifs/drag_drop.gif)
![](/img/docs/animated_gifs/drag_drop.gif)
## Tips and shortcuts
......@@ -79,15 +79,3 @@ You can Drag-and-Drop Panels within and between Rows. Click and hold the Panel t
* Ctrl+H Hides all controls (good for tv displays)
* Hit Escape to exit graph when in fullscreen or edit mode
......@@ -23,7 +23,7 @@ A template variable with Multi-Value enabled allows for the selection of multipl
These variables can then be used in any Panel to make them more dynamic, and to give you the perfect view of your data.
Multi-Value variables is also enabling the new `row repeat` and `panel repeat` feature described below.
![Multi-Value Select](img/docs/v2/multi-select.gif "Multi-Value Select")
![Multi-Value Select](/img/docs/v2/multi-select.gif "Multi-Value Select")
<br/><br/>
### Repeating Rows and Panels
......@@ -31,7 +31,7 @@ It’s now possible to create a dashboard that automatically adds (or removes) b
on selected variable values. Any row or any panel can be configured to repeat (duplicate itself) based
on a multi-value template variable.</p>
![Repeating Rows and Panels](img/docs/v2/panel-row-repeat.gif "Repeating Rows and Panels")
![Repeating Rows and Panels](/img/docs/v2/panel-row-repeat.gif "Repeating Rows and Panels")
<br/><br/>
### Dashboard Links & Navigation
......@@ -39,7 +39,7 @@ To support better navigation between dashboards, it's now possible to create cus
panels to appropriate Dashboards. You also have the ability to create flexible top-level links on any
given dashboard thanks to the new dashboard navigation bar feature.
![Dashboard Links](img/docs/v2/dash_links.png "Dashboard Links")
![Dashboard Links](/img/docs/v2/dash_links.png "Dashboard Links")
Dashboard links can be added under dashboard settings. Either defined as static URLs with a custom icon or as dynamic
dashboard links or dropdowns based on custom dashboard search query. These links appear in the same
......@@ -88,7 +88,7 @@ The Viewer role has been modified in Grafana 2.1 so that users assigned this rol
Grafana 2.1 now comes with full support for InfluxDB 0.9. There is a new query editor designed from scratch
for the new features InfluxDB 0.9 enables.
![InfluxDB Editor](img/docs/v2/influx_09_editor_anim.gif "InfluxDB Editor")
![InfluxDB Editor](/img/docs/v2/influx_09_editor_anim.gif "InfluxDB Editor")
<br/>
......@@ -116,15 +116,15 @@ Define series color using regex rule. This is useful when you have templated gra
that change depending selected template variables. Using a regex style override rule you could
for example make all series that contain the word **CPU** `red` and assigned to the second y axis.
![Define series color using regex rule](img/docs/v2/regex_color_override.png "Define series color using regex rule")
![Define series color using regex rule](/img/docs/v2/regex_color_override.png "Define series color using regex rule")
New series style override, negative-y transform and stack groups. Negative y transform is
very useful if you want to plot a series on the negative y scale without affecting the legend values like min or max or
the values shown in the hover tooltip.
![Negative-y Transform](img/docs/v2/negative-y.png "Negative-y Transform")
![Negative-y Transform](/img/docs/v2/negative-y.png "Negative-y Transform")
![Negative-y Transform](img/docs/v2/negative-y-form.png "Negative-y Transform")
![Negative-y Transform](/img/docs/v2/negative-y-form.png "Negative-y Transform")
### Singlestat Panel
Now support string values. Useful for time series database like InfluxDB that supports
......
......@@ -18,7 +18,7 @@ fixes and enhancements to all areas of Grafana, like new Data Sources, a new and
resize handles and improved InfluxDB and OpenTSDB support.
### New time range controls
<img src="img/docs/whatsnew_2_5/timepicker.png" alt="New Time picker">
<img src="/img/docs/whatsnew_2_5/timepicker.png" alt="New Time picker">
A new timepicker with room for more quick ranges as well as new types of relative ranges, like `Today`,
`The day so far` and `This day last week`. Also an improved time & calendar picker that now works
......@@ -26,7 +26,7 @@ correctly in UTC mode.
### Elasticsearch
<img src="img/docs/whatsnew_2_5/elasticsearch_metrics_ex1.png" alt="Elasticsearch example">
<img src="/img/docs/whatsnew_2_5/elasticsearch_metrics_ex1.png" alt="Elasticsearch example">
<br>
This release brings a fully featured query editor for Elasticsearch. You will now be able to visualize
......@@ -46,7 +46,7 @@ Try the new Elasticsearch query editor on the [play.grafana.org](http://play.gra
### CloudWatch
<img src="img/docs/whatsnew_2_5/cloudwatch.png" alt="Cloudwatch editor">
<img src="/img/docs/whatsnew_2_5/cloudwatch.png" alt="Cloudwatch editor">
Grafana 2.5 ships with a new CloudWatch datasource that will allow you to query and visualize CloudWatch
metrics directly from Grafana.
......@@ -57,14 +57,14 @@ metrics directly from Grafana.
### Prometheus
<img src="img/docs/whatsnew_2_5/prometheus_editor.png" alt="Prometheus editor">
<img src="/img/docs/whatsnew_2_5/prometheus_editor.png" alt="Prometheus editor">
Grafana 2.5 ships with a new Prometheus datasource that will allow you to query and visualize data
stored in Prometheus.
### Mix different data sources
<img src="img/docs/whatsnew_2_5/mixed_data.png" alt="Mix data sources in the same dashboard or in the same graph!">
<img src="/img/docs/whatsnew_2_5/mixed_data.png" alt="Mix data sources in the same dashboard or in the same graph!">
In previous releases you have been able to mix different data sources on the same dashboard. In v2.5 you
will be able to mix then on the same graph! You can enable this by selecting the built in `-- Mixed --` data source.
......@@ -73,12 +73,12 @@ to plot metrics from different Graphite servers on the same Graph or plot data f
data from Prometheus. Mixing different data sources on the same graph works for any data source, even custom ones.
### Panel Resize handles
<img src="img/docs/whatsnew_2_5/panel_resize.gif" alt="">
<img src="/img/docs/whatsnew_2_5/panel_resize.gif" alt="">
This release adds resize handles to the the bottom right corners of panels making is easy to resize both width and height.
### User invites
<img src="img/docs/whatsnew_2_5/org_invite.png" alt="">
<img src="/img/docs/whatsnew_2_5/org_invite.png" alt="">
This version also brings some new features for user management.
......
......@@ -17,7 +17,7 @@ The release includes a new Table panel, a new InfluxDB query editor, support for
support for multiple Cloudwatch credentials.
## Table Panel
<img src="assets/img/features/table-panel.png">
<img src="/assets/img/features/table-panel.png">
The new table panel is very flexible, supporting both multiple modes for time series as well as for
table, annotation and raw JSON data. It also provides date formating and value formating and coloring options.
......@@ -27,7 +27,7 @@ table, annotation and raw JSON data. It also provides date formating and value f
In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column.
Where `Metric` is the name of the time series.
<img src="img/docs/v2/table_ts_to_rows.png">
<img src="/img/docs/v2/table_ts_to_rows.png">
### Table Transform
Above you see the options tab for the **Table Panel**. The most important option is the `To Table Transform`.
......@@ -40,7 +40,7 @@ The column styles allow you control how dates and numbers are formatted.
This transform allows you to take multiple time series and group them by time. Which will result in a `Time` column
and a column for each time series.
<img src="img/docs/v2/table_ts_to_columns.png">
<img src="/img/docs/v2/table_ts_to_columns.png">
In the screenshot above you can see how the same time series query as in the previous example can be transformed into
a different table by changing the `To Table Transform` to `Time series to columns`.
......@@ -49,7 +49,7 @@ a different table by changing the `To Table Transform` to `Time series to colum
This transform works very similar to the legend values in the Graph panel. Each series gets its own row. In the Options
tab you can select which aggregations you want using the plus button the Columns section.
<img src="img/docs/v2/table_ts_to_aggregations.png">
<img src="/img/docs/v2/table_ts_to_aggregations.png">
You have to think about how accurate the aggregations will be. It depends on what aggregation is used in the time series query,
how many data points are fetched, etc. The time series aggregations are calculated by Grafana after aggregation is performed
......@@ -59,39 +59,39 @@ by the time series database.
If you want to show documents from Elasticsearch pick `Raw Document` as the first metric.
<img src="img/docs/v2/elastic_raw_doc.png">
<img src="/img/docs/v2/elastic_raw_doc.png">
This in combination with the `JSON Data` table transform will allow you to pick which fields in the document
you want to show in the table.
<img src="img/docs/v2/table_json_data.png">
<img src="/img/docs/v2/table_json_data.png">
### Elasticsearch aggregations
You can also make Elasticsearch aggregation queries without a `Date Histogram`. This allows you to
use Elasticsearch metric aggregations to get accurate aggregations for the selected time range.
<img src="img/docs/v2/elastic_aggregations.png">
<img src="/img/docs/v2/elastic_aggregations.png">
### Annotations
The table can also show any annotations you have enabled in the dashboard.
<img src="img/docs/v2/table_annotations.png">
<img src="/img/docs/v2/table_annotations.png">
## The New InfluxDB Editor
The new InfluxDB editor is a lot more flexible and powerful. It supports nested functions, like `derivative`.
It also uses the same technique as the Graphite query editor in that it presents nested functions as chain of function
transformations. It tries to simplify and unify the complicated nature of InfluxDB's query language.
<img src="assets/img/blog/v2.6/influxdb_editor_v3.gif">
<img src="/assets/img/blog/v2.6/influxdb_editor_v3.gif">
In the `SELECT` row you can specify what fields and functions you want to use. If you have a
group by time you need an aggregation function. Some functions like derivative require an aggregation function.
The editor tries simplify and unify this part of the query. For example:
![](img/docs/influxdb/select_editor.png)
![](/img/docs/influxdb/select_editor.png)
The above will generate the following InfluxDB `SELECT` clause:
......@@ -110,7 +110,7 @@ You can remove the group by by clicking on the `tag` and then click on the x ico
The new editor also allows you to remove group by time and select `raw` table data. Which is very useful
in combination with the new Table panel to show raw log data stored in InfluxDB.
<img src="assets/img/blog/v2.6/table_influxdb_logs.png">
<img src="/assets/img/blog/v2.6/table_influxdb_logs.png">
## Pipeline metrics
......
......@@ -55,7 +55,7 @@ even zoom in). Also they are fast to load as they aren't actually connected to a
They're a great way to communicate about a particular incident with specific people who aren't Users of your Grafana instance. You can also use them to show off your dashboards over the Internet.
![](img/docs/v2/dashboard_snapshot_dialog.png)
![](/img/docs/v2/dashboard_snapshot_dialog.png)
### Publish snapshots
......@@ -67,11 +67,11 @@ Either way, anyone with the link (and access to your Grafana instance for local
In Grafana v2.x you can now override the relative time range for individual panels, causing them to be different than what is selected in the Dashboard time picker in the upper right. You can also add a time shift to individual panels. This allows you to show metrics from different time periods or days at the same time.
![](img/docs/v2/panel_time_override.jpg)
![](/img/docs/v2/panel_time_override.jpg)
You control these overrides in panel editor mode and the new tab `Time Range`.
![](img/docs/v2/time_range_tab.jpg)
![](/img/docs/v2/time_range_tab.jpg)
When you zoom or change the Dashboard time to a custom absolute time range, all panel overrides will be disabled. The panel relative time override is only active when the dashboard time is also relative. The panel timeshift override however is always active, even when the dashboard time is absolute.
......@@ -96,7 +96,7 @@ This feature makes it easy to include interactive visualizations from your Grafa
The top header has gotten a major streamlining in Grafana V2.0.
<img class="no-shadow" src="img/docs/v2/v2_top_nav_annotated.png">
<img class="no-shadow" src="/img/docs/v2/v2_top_nav_annotated.png">
1. `Side menubar toggle` Toggle the side menubar on or off. This allows you to focus on the data presented on the Dashboard. The side menubar provides access to features unrelated to a Dashboard such as Users, Organizations, and Data Sources.
2. `Dashboard dropdown` The main dropdown shows you which Dashboard you are currently viewing, and allows you to easily switch to a new Dashboard. From here you can also create a new Dashboard, Import existing Dashboards, and manage the Playlist.
......@@ -121,7 +121,7 @@ You can easily collapse or re-open the side menubar at any time by clicking the
## New search view & starring dashboards
![](img/docs/v2/dashboard_search.jpg)
![](/img/docs/v2/dashboard_search.jpg)
The dashboard search view has gotten a big overhaul. You can now see and filter by which dashboard you have personally starred.
......@@ -130,11 +130,11 @@ The dashboard search view has gotten a big overhaul. You can now see and filter
The Graph panel now supports 3 logarithmic scales, `log base 10`, `log base 32`, `log base 1024`. Logarithmic y-axis scales are very useful when rendering many series of different order of magnitude on the same scale (eg.
latency, network traffic, and storage)
![](img/docs/v2/graph_logbase10_ms.png)
![](/img/docs/v2/graph_logbase10_ms.png)
## Dashlist panel
![](img/docs/v2/dashlist_starred.png)
![](/img/docs/v2/dashlist_starred.png)
The dashlist is a new panel in Grafana v2.0. It allows you to show your personal starred dashboards, as well as do custom searches based on search strings or tags.
......@@ -153,7 +153,7 @@ In addition, connections to Data Sources can be better controlled and secured, a
A commonly reported problem has been graphs dipping to zero at the the end, because metric data for the last interval has yet to be written to the Data Source. These graphs then "self correct" once the data comes in, but can look deceiving or alarming at times.
You can avoid this problem by adding a `now delay` in `Dashboard Settings` > `Time Picker` tab. This new feature will cause Grafana to ignore the most recent data up to the set delay.
![](img/docs/v2/timepicker_now_delay.jpg)
![](/img/docs/v2/timepicker_now_delay.jpg)
The delay that may be necessary depends on how much latency you have in your collection pipeline.
......@@ -161,7 +161,7 @@ The delay that may be necessary depends on how much latency you have in your col
Grafana v2.0 protects Users from accidentally overwriting each others Dashboard changes. Similar protections are in place if you try to create a new Dashboard with the same name as an existing one.
![](img/docs/v2/overwrite_protection.jpg)
![](/img/docs/v2/overwrite_protection.jpg)
These protections are only the first step; we will be building out additional capabilities around dashboard versioning and management in future versions of Grafana.
......@@ -177,6 +177,6 @@ Grafana now supports server-side PNG rendering. From the Panel share dialog you
> **Note** This requires that your Data Source is accessible from your Grafana instance.
![](img/docs/v2/share_dialog_image_highlight.jpg)
![](/img/docs/v2/share_dialog_image_highlight.jpg)
......@@ -16,21 +16,21 @@ weight = 1
The export feature is now accessed from the share menu.
<img src="img/docs/v31/export_menu.png">
<img src="/img/docs/v31/export_menu.png">
Dashboards exported from Grafana 3.1 are now more portable and easier for others to import than before.
The export process extracts information data source types used by panels and adds these to a new `inputs`
section in the dashboard json. So when you or another person tries to import the dashboard they will be asked to
select data source and optional metrix prefix options.
<img src="img/docs/v31/import_step1.png">
<img src="/img/docs/v31/import_step1.png">
The above screenshot shows the new import modal that gives you 3 options for how to import a dashboard.
One notable new addition here is the ability to import directly from Dashboards shared on [Grafana.net](https://grafana.net).
The next step in the import process:
<img src="img/docs/v31/import_step2.png">
<img src="/img/docs/v31/import_step2.png">
Here you can change the name of the dashboard and also pick what data sources you want the dashboard to use. The above screenshot
shows a CollectD dashboard for Graphite that requires a metric prefix be specified.
......@@ -41,7 +41,7 @@ On [Grafana.net](https://grafana.net) you can now browse & search for dashboards
more are being uploaded every day. To import a dashboard just copy the dashboard url and head back to Grafana,
then Dashboard Search -> Import -> Paste Grafana.net Dashboard URL.
<img src="img/docs/v31/gnet_dashboards_list.png">
<img src="/img/docs/v31/gnet_dashboards_list.png">
## Constant template variables
......
......@@ -43,7 +43,7 @@ entire experience right within Grafana.
## Grafana.net
<img src="img/docs/v3/grafana_net_tour.png">
<img src="/img/docs/v3/grafana_net_tour.png">
[Grafana.net](https://grafana.net) offers a central repository where the community can come together to discover, create and
share plugins (data sources, panels, apps) and dashboards.
......@@ -102,7 +102,7 @@ periodically and remotely.
You can also make Playlists dynamic by using Dashboard **tags** to
define the Playlist.
<img src="img/docs/v3/playlist.png">
<img src="/img/docs/v3/playlist.png">
## Improved UI
......@@ -122,11 +122,11 @@ are literally hundreds of UI improvements and refinements.
Here’s the new side menu in action:
<img src="img/docs/v3/menu.gif">
<img src="/img/docs/v3/menu.gif">
And here's the new look for Dashboard settings:
<img src="img/docs/v3/dashboard_settings.png">
<img src="/img/docs/v3/dashboard_settings.png">
Check out the <a href="http://play.grafana.org" target="_blank">Play
Site</a> to get a feel for some of the UI changes.
......@@ -138,7 +138,7 @@ over the link and click the annotation text. This feature is very
useful for linking to particular commits or tickets where more
detailed information can be presented to the user.
<img src="img/docs/v3/annotation_links.gif">
<img src="/img/docs/v3/annotation_links.gif">
## Data source variables
......@@ -146,11 +146,11 @@ This has been a top requested feature for very long we are exited to finally pro
this feature. You can now add a new `Data source` type variable. That will
automatically be filled with instance names of your data sources.
<img src="img/docs/v3/data_source_variable.png">
<img src="/img/docs/v3/data_source_variable.png">
You can then use this variable as the panel data source:
<img src="img/docs/v3/data_source_variable_use.png">
<img src="/img/docs/v3/data_source_variable_use.png">
This will allow you to quickly change data source server and reuse the
same dashboard for different instances of your metrics backend. For example
......@@ -168,7 +168,7 @@ The Prometheus Data Source now supports annotations.
### InfluxDB
You can now select the InfluxDB policy from the query editor.
<img src="img/docs/v3/influxdb_policy.png">
<img src="/img/docs/v3/influxdb_policy.png">
Grafana 3.0 also comes with support for InfluxDB 0.11 and InfluxDB 0.12.
......@@ -201,23 +201,23 @@ are a couple that I incurage you try!
#### [Clock Panel](https://grafana.net/plugins/grafana-clock-panel)
Support's both current time and count down mode.
<img src="img/docs/v3/clock_panel.png">
<img src="/img/docs/v3/clock_panel.png">
#### [Pie Chart Panel](https://grafana.net/plugins/grafana-piechart-panel)
A simple pie chart panel is now available as an external plugin.
<img src="img/docs/v3/pie_chart_panel.png">
<img src="/img/docs/v3/pie_chart_panel.png">
#### [WorldPing App](https://grafana.net/plugins/raintank-worldping-app)
This is full blown Grafana App that adds new panels, data sources and pages to give
feature rich global performance monitoring directly from your on-prem Grafana.
<img src="img/docs/v3/wP-Screenshot-dash-web.png">
<img src="/img/docs/v3/wP-Screenshot-dash-web.png">
#### [Zabbix App](https://grafana.net/plugins/alexanderzobnin-zabbix-app)
This app contains the already very pouplar Zabbix data source plugin, 2 dashboards and a triggers panel. It is
created and maintained by [Alexander Zobnin](https://github.com/alexanderzobnin/grafana-zabbix).
<img src="img/docs/v3/zabbix_app.png">
<img src="/img/docs/v3/zabbix_app.png">
Checkout the full list of plugins on [Grafana.net](https://grafana.net/plugins)
......
+++
title = "What's New in Grafana v4.0"
description = "Feature & improvement highlights for Grafana v4.0"
keywords = ["grafana", "new", "documentation", "4.0"]
type = "docs"
[menu.docs]
name = "Version 4.0"
identifier = "v4.0"
parent = "whatsnew"
weight = -1
+++
# What's New in Grafana v4.0
As usual this release contains a ton of minor new features, fixes and improved UX. But on top of the usual new goodies
is a core new feature: Alerting! Read on below for a detailed description of what's new in v4.0.
## Alerting
{{< imgbox max-width="40%" img="/img/docs/v4/drag_handles_gif.gif" caption="Alerting overview" >}}
Alerting is a really revolutionary feature for Grafana. It transforms Grafana from a
visualization tool into a truly mission critical monitoring tool. The alert rules are very easy to
configure using your existing graph panels and threshold levels can be set simply by dragging handles to
the right side of the graph. The rules will continually be evaluated by grafana-server and
notifications will be sent out when the rule conditions are met.
This feature has been worked on for over a year with many iterations and rewrites
just to make sure the foundations are really solid. We are really proud to finally release it!
Since the alerting execution is processed in the backend all data source plugins are not supported.
Right now Graphite, Prometheus, InfluxDB and OpenTSDB are supported. Elasticsearch is being worked
on but will be not ready for v4 release.
<div class="clearfix"></div>
### Rules
{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}}
The rule config allows you to specify a name, how often the rule should be evaluated and a series
of conditions that all need to be true for the alert to fire.
Currently the only condition type that exists is a `Query` condition that allows you to
specify a query letter, time range and an aggregation function. The letter refers to
a query you already have added in the **Metrics** tab. The result from the
query and the aggregation function is a single value that is then used in the threshold check.
We plan to add other condition types in the future, like `Other Alert`, where you can include the state
of another alert in your conditions, and `Time Of Day`.
### Notifications
{{< imgbox max-width="40%" img="/img/docs/v4/slack_notification.png" caption="Alerting Slack Notification" >}}
Alerting would not be very useful if there was no way to send notifications when rules trigger and change state. You
can setup notifications of different types. We currently have `Slack`, `PagerDuty`, `Email` and `Webhook` with more in the
pipe that will be added during beta period. The notifications can then be added to your alert rules.
If you have configured an external image store in the grafana.ini config file (s3 and webdav options available)
you can get very rich notifications with an image of the graph and the metric
values all included in the notification.
### Annotations
Alert state changes are recorded in a new annotation store that is built into Grafana. This store
currently only supports storing annotations in Grafana's own internal database (mysql, postgres or sqlite).
The Grafana annotation storage is currently only used for alert state changes but we hope to add the ability for users
to add graph comments in the form of annotations directly from within Grafana in a future release.
### Alert List Panel
{{< imgbox max-width="30%" img="/img/docs/v4/alert_list_panel.png" caption="Alert List Panel" >}}
This new panel allows you to show alert rules or a history of alert rule state changes. You can filter based on states your
interested in. Very useful panel for overview style dashboards.
<div class="clearfix"></div>
## Ad-hoc filter variable
{{< imgbox max-width="30%" img="/img/docs/v4/adhoc_filters.gif" caption="Ad-hoc filters variable" >}}
This is a new and very different type of template variable. It will allow you to create new key/value filters on the fly.
With autocomplete for both key and values. The filter condition will be automatically applied to all
queries that use that data source. This feature opens up more exploratory dashboards. In the gif animation to the right
you have a dashboard for Elasticsearch log data. It uses one query variable that allow you to quickly change how the data
is grouped, and an interval variable for controlling the granularity of the time buckets. What was missing
was a way to dynamically apply filters to the log query. With the `Ad-Hoc Filters` variable you can
dynamically add filters to any log property!
## UX Improvements
We always try to bring some UX/UI refinements & polish in every release.
### TV-mode & Kiosk mode
<div class="row">
<div class="medium-6 columns">
<p>
Grafana is so often used on wall mounted TVs that we figured a clean TV mode would be
really nice. In TV mode the top navbar, row & panel controls will all fade to transparent.
</p>
<p>
This happens automatically after one minute of user inactivity but can also be toggled manually
with the <code>d v</code> sequence shortcut. Any mouse movement or keyboard action will
restore navbar & controls.
</p>
<p>
Another feature is the kiosk mode. This can be enabled with <code>d k</code>
shortcut or by adding <code>&kiosk</code> to the URL when you load a dashboard.
In kiosk mode the navbar is completely hidden/removed from view.
</p>
</div>
<div class="medium-6 columns">
{{< lightboxhelper max-width="100%" img="/img/docs/v4/tvmode.png" caption="TV mode" >}}
<video width="320" height="240" controls>
<source src="/assets/videos/tvmode.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
</div>
### New row menu & add panel experience
{{< imgbox max-width="50%" img="/img/docs/v4/add_panel.gif" caption="Add Panel flow" >}}
We spent a lot of time improving the dashboard building experience. Trying to make it both
more efficient and easier for beginners. After many good but not great experiments
with a `build mode` we eventually decided to just improve the green row menu and
continue work on a `build mode` for a future release.
The new row menu automatically slides out when you mouse over the edge of the row. You no longer need
to hover over the small green icon and the click it to expand the row menu.
There is some minor improvements to drag and drop behaviour. Now when dragging a panel from one row
to another you will insert the panel and Grafana will automatically make room for it.
When you drag a panel within a row you will simply reorder the panels.
If you look at the animation to the right you can see that you can drag and drop a new panel. This is not
required, you can also just click the panel type and it will be inserted at the end of the row
automatically. Dragging a new panel has an advantage in that you can insert a new panel where ever you want
not just at the end of the row.
We plan to further improve dashboard building in the future with a more rich grid & layout system.
### Keyboard shortcuts
{{< imgbox max-width="40%" img="/img/docs/v4/shortcuts.png" caption="Shortcuts" >}}
Grafana v4 introduces a number of really powerful keyboard shortcuts. You can now focus a panel
by hovering over it with your mouse. With a panel focused you can simple hit `e` to toggle panel
edit mode, or `v` to toggle fullscreen mode. `p r` removes the panel. `p s` opens share
modal.
Some nice navigation shortcuts are:
- `g h` for go to home dashboard
- `s s` open search with starred pre-selected
- `s t` open search in tags list view
<div class="clearfix"></div>
## Upgrade & Breaking changes
There are no breaking changes. Old dashboards and features should work the same. Grafana-server will automatically upgrade it's db
schema on restart. It's advisable to do a backup of Grafana's database before updating.
If your are using plugins make sure to update your plugins as some might not work perfectly v4.
You can update plugins using grafana-cli
grafana-cli plugins update-all
## Changelog
Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list
of new features, changes, and bug fixes.
......@@ -25,7 +25,7 @@ curl example:
Open the sidemenu and click the organization dropdown and select the `API Keys` option.
![](img/v2/orgdropdown_api_keys.png)
![](/img/v2/orgdropdown_api_keys.png)
You use the token in all requests in the `Authorization` header, like this:
......
+++
title = "Grafana Installation"
title = "Grafana Documentation Site"
description = "Install guide for Grafana"
keywords = ["grafana", "installation", "documentation"]
type = "docs_root"
type = "docs"
[menu.docs]
name = "Welcome to the Docs"
identifier = "root"
......
......@@ -339,7 +339,7 @@ your Grafana instance. For example
scopes = user:email,read:org
auth_url = https://github.com/login/oauth/authorize
token_url = https://github.com/login/oauth/access_token
allow_sign_up = false
allow_sign_up = true
# space-delimited organization names
allowed_organizations = github google
......@@ -579,3 +579,37 @@ Enabled to automatically remove expired snapshots
### remove snapshots after 90 days
Time to live for snapshots.
## [external_image_storage]
These options control how images should be made public so they can be shared on services like slack.
### provider
You can choose between (s3, webdav). If left empty Grafana will ignore the upload action.
## [external_image_storage.s3]
### bucket_url
bucket url for s3. ex http://grafana.s3.amazonaws.com/
### access_key
access key. ex AAAAAAAAAAAAAAAAAAAA
### secret_key
secret key. ex AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
## [external_image_storage.webdav]
### url
Url to where Grafana will send PUT request with images
### username
basic auth username
### password
basic auth password
## [alerting]
### execute_alerts = true
Makes it possible to turn off alert rule execution.
......@@ -61,7 +61,7 @@ sure your Elasticsearch data source is added. Specify the Elasticsearch
index name where your existing Grafana v1.x dashboards are stored
(the default is `grafana-dash`).
![](img/docs/v2/datasource_edit_elastic.jpg)
![](/img/docs/v2/datasource_edit_elastic.jpg)
### Importing dashboards from InfluxDB
......@@ -74,7 +74,7 @@ your Grafana v1.x dashboards are stored, the default is `grafana`.
Go to the `Dashboards` view and click on the dashboards search drop
down. Click the `Import` button at the bottom of the search drop down.
![](img/docs/v2/dashboard_import.jpg)
![](/img/docs/v2/dashboard_import.jpg)
### Import view
......@@ -82,7 +82,7 @@ In the Import view you find the section `Migrate dashboards`. Pick the
data source you added (from Elasticsearch or InfluxDB), and click the
`Import` button.
![](img/docs/v2/migrate_dashboards.jpg)
![](/img/docs/v2/migrate_dashboards.jpg)
Your dashboards should be automatically imported into the Grafana 2.0
back-end.
......
......@@ -23,7 +23,7 @@ with Grafana being unable to query Graphite, OpenTSDB or InfluxDB. You
might not be able to get metric name completion or the graph might show
an error like this:
![](img/docs/v1/graph_timestore_error.png)
![](/img/docs/v1/graph_timestore_error.png)
For some types of errors, the `View details` link will show you error
details. For many types of HTTP connection errors, however, there is very
......@@ -31,7 +31,7 @@ little information. The best way to troubleshoot these issues is use
the [Chrome developer tools](https://developer.chrome.com/devtools/index).
By pressing `F12` you can bring up the chrome dev tools.
![](img/docs/v1/toubleshooting_chrome_dev_tools.png)
![](/img/docs/v1/toubleshooting_chrome_dev_tools.png)
There are two important tabs in the Chrome developer tools: `Network`
and `Console`. The `Console` tab will show you Javascript errors and
......@@ -49,7 +49,7 @@ chrome console error, request and response information from the
### Inspecting Grafana metric requests
![](img/docs/v1/toubleshooting_chrome_dev_tools_network.png)
![](/img/docs/v1/toubleshooting_chrome_dev_tools_network.png)
After opening the Chrome developer tools for the first time the
`Network` tab is empty. You will need to refresh the page to get
......
......@@ -11,7 +11,7 @@ App plugins is a new kind of grafana plugin that can bundle datasource and panel
Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu.
<img class="no-shadow" src="img/v3/app-in-main-menu.png">
<img class="no-shadow" src="/img/v3/app-in-main-menu.png">
## Enabling app plugins
After installing an app it have to be enabled before it show up as an datasource or panel. You can do that on the app page in the config tab.
......
......@@ -13,7 +13,7 @@ weight = 2
Annotations provide a way to mark points on the graph with rich events. When you hover over an annotation
you can get title, tags, and text information for the event.
![](img/docs/v1/annotated_graph1.png)
![](/img/docs/v1/annotated_graph1.png)
To add an annotation query click dashboard settings icon in top menu and select `Annotations` from the
dropdown. This will open the `Annotations` edit view. Click the `Add` tab to add a new annotation query.
......@@ -28,7 +28,7 @@ Graphite supports two ways to query annotations.
- Graphite events query, use the `Graphite event tags` text input, specify an tag or wildcard (leave empty should also work)
## Elasticsearch annotations
![](img/docs/v2/annotations_es.png)
![](/img/docs/v2/annotations_es.png)
Grafana can query any Elasticsearch index for annotation events. The index name can be the name of an alias or an index wildcard pattern.
You can leave the search query blank or specify a lucene query.
......@@ -39,14 +39,14 @@ as the name for the fields that should be used for the annotation title, tags an
> **Note** The annotation timestamp field in elasticsearch need to be in UTC format.
## InfluxDB Annotations
![](img/docs/v2/annotations_influxdb.png)
![](/img/docs/v2/annotations_influxdb.png)
For InfluxDB you need to enter a query like in the above screenshot. You need to have the ```where $timeFilter``` part.
If you only select one column you will not need to enter anything in the column mapping fields.
## Prometheus Annotations
![](img/docs/v3/annotations_prom.png)
![](/img/docs/v3/annotations_prom.png)
Prometheus supports two ways to query annotations.
......
......@@ -13,7 +13,7 @@ weight = 4
The dashboard list panel allows you to display dynamic links to other dashboards. The list can be configured to use starred dashboards, a search query and/or dashboard tags.
<img class="no-shadow" src="img/docs/v2/dashboard_list_panels.png">
<img class="no-shadow" src="/img/docs/v2/dashboard_list_panels.png">
> On each dashboard load, the dashlist panel will re-query the dashboard list, always providing the most up to date results.
......@@ -21,7 +21,7 @@ The dashboard list panel allows you to display dynamic links to other dashboards
The `starred` dashboard selection displays starred dashboards, up to the number specified in the `Limit Number to` field, in alphabetical order. On dashboard load, the dashlist panel will re-query the favorites to appear in dashboard list panel, always providing the most up to date results.
<img class="no-shadow" src="img/docs/v2/dashboard_list_config_starred.png">
<img class="no-shadow" src="/img/docs/v2/dashboard_list_config_starred.png">
## Mode: Search Dashboards
......@@ -42,11 +42,11 @@ Limit number to | Specify the maximum number of dashboards
### Search by string
To search by a string, enter a search query in the `Search Options: Query` field. Queries are case-insensitive, and partial values are accepted.
<img class="no-shadow" src="img/docs/v2/dashboard_list_config_string.png">
<img class="no-shadow" src="/img/docs/v2/dashboard_list_config_string.png">
### Search by tag
To search by one or more tags, enter your selection in the `Search Options: Tags:` field. Note that existing tags will not appear as you type, and *are* case sensitive. To see a list of existing tags, you can always return to the dashboard, open the Dashboard Picker at the top and click `tags` link in the search bar.
<img class="no-shadow" src="img/docs/v2/dashboard_list_config_tags.png">
<img class="no-shadow" src="/img/docs/v2/dashboard_list_config_tags.png">
> When multiple tags and strings appear, the dashboard list will display those matching ALL conditions.
......
......@@ -17,7 +17,7 @@ Dashboards are exported in Grafana JSON format, and contain everything you need
The export feature is accessed from the share menu.
<img src="img/docs/v31/export_menu.png">
<img src="/img/docs/v31/export_menu.png">
### Making a dashboard portable
......@@ -31,12 +31,12 @@ the dashboard, and will also be added as an required input when the dashboard is
To import a dashboard open dashboard search and then hit the import button.
<img src="img/docs/v31/import_step1.png">
<img src="/img/docs/v31/import_step1.png">
From here you can upload a dashboard json file, paste a [Grafana.net](https://grafana.net) dashboard
url or paste dashboard json text directly into the text area.
<img src="img/docs/v31/import_step2.png">
<img src="/img/docs/v31/import_step2.png">
In step 2 of the import process Grafana will let you change the name of the dashboard, pick what
data source you want the dashboard to use and specify any metric prefixes (if the dashboard use any).
......@@ -45,7 +45,7 @@ data source you want the dashboard to use and specify any metric prefixes (if th
Find dashboads for common server applications at [Grafana.net/dashboards](https://grafana.net/dashboards).
<img src="img/docs/v31/gnet_dashboards_list.png">
<img src="/img/docs/v31/gnet_dashboards_list.png">
## Import & Sharing with Grafana 2.x or 3.0
......
......@@ -12,13 +12,13 @@ weight = 1
The main panel in Grafana is simply named Graph. It provides a very rich set of graphing options.
<img src="img/docs/v1/graph_overview.png" class="no-shadow">
<img src="/img/docs/v1/graph_overview.png" class="no-shadow">
Clicking the title for a panel exposes a menu. The `edit` option opens additional configuration
options for the panel.
## General
![](img/docs/v2/graph_general.png)
![](/img/docs/v2/graph_general.png)
The general tab allows customization of a panel's appearance and menu options.
......@@ -56,7 +56,7 @@ options.
## Axes & Grid
![](img/docs/v2/graph_axes_grid_options.png)
![](/img/docs/v2/graph_axes_grid_options.png)
The Axes & Grid tab controls the display of axes, grids and legend.
......@@ -99,7 +99,7 @@ It is just the sum of all data points received by Grafana.
## Display styles
![](img/docs/v2/graph_display_styles.png)
![](/img/docs/v2/graph_display_styles.png)
Display styles controls properties of the graph.
......@@ -144,4 +144,4 @@ a thicker line width to make it standout.
## Time range
![](img/docs/v2/graph_time_range.png)
![](/img/docs/v2/graph_time_range.png)
......@@ -15,7 +15,7 @@ No mouse? No problem. Grafana has extensive keyboard shortcuts to allow you to n
Press `Shift`+`?` to open the keyboard shortcut dialog from anywhere within the dashboard views.
<img class="no-shadow" src="img/docs/v2/Grafana-Keyboard-Shortcuts.gif" style="width:80%;">
<img class="no-shadow" src="/img/docs/v2/Grafana-Keyboard-Shortcuts.gif" style="width:80%;">
|Shortcut|Action|
......
......@@ -12,7 +12,7 @@ weight = 5
Dashboards can be searched by the dashboard name, filtered by one (or many) tags or filtered by starred status. The dashboard search is accessed through the dashboard picker, available in the dashboard top nav area.
<img class="no-shadow" src="img/docs/v2/dashboard_search.png">
<img class="no-shadow" src="/img/docs/v2/dashboard_search.png">
1. `Dashboard Picker`: The Dashboard Picker is your primary navigation tool to move between dashboards. It is present on all dashboards, and open the Dashboard Search. The dashboard picker also doubles as the title of the current dashboard.
2. `Search Bar`: The search bar allows you to enter any string and search both database and file based dashboards in real-time.
......@@ -23,7 +23,7 @@ When using only a keyboard, you can use your keyboard arrow keys to navigate the
## Find by dashboard name
<img class="no-shadow" src="img/docs/v2/dashboard_search_text.gif">
<img class="no-shadow" src="/img/docs/v2/dashboard_search_text.gif">
To search and load dashboards click the open folder icon in the header or use the shortcut `CTRL`+`F`. Begin typing any part of the desired dashboard names. Search will return results for for any partial string match in real-time, as you type.
......@@ -38,11 +38,11 @@ Tags are a great way to organize your dashboards, especially as the number of da
To filter the dashboard list by tag, click on any tag appearing in the right column. The list may be further filtered by clicking on additional tags:
<img class="no-shadow" src="img/docs/v2/dashboard_search_tag_filtering.gif">
<img class="no-shadow" src="/img/docs/v2/dashboard_search_tag_filtering.gif">
Alternately, to see a list of all available tags, click the tags link in the search bar. All tags will be shown, and when a tag is selected, the dashboard search will be instantly filtered:
<img class="no-shadow" src="img/docs/v2/dashboard_search_tags_all_filtering.gif">
<img class="no-shadow" src="/img/docs/v2/dashboard_search_tags_all_filtering.gif">
When using only a keyboard: `tab` to focus on the *tags* link, `▼` down arrow key to find a tag and select with the `Enter` key.
......@@ -53,6 +53,6 @@ When using only a keyboard: `tab` to focus on the *tags* link, `▼` down arrow
Starring is a great way to organize and find commonly used dashboards. To show only starred dashboards in the list, click the *starred* link in the search bar:
<img class="no-shadow" src="img/docs/v2/dashboard_search_starred_filtering.gif">
<img class="no-shadow" src="/img/docs/v2/dashboard_search_starred_filtering.gif">
When using only a keyboard: `tab` to focus on the *stars* link, `▼` down arrow key to find a tag and select with the `Enter` key.
......@@ -22,7 +22,7 @@ A dashboard snapshot is an instant way to share an interactive dashboard publicl
(metric, template and annotation) and panel links, leaving only the visible metric data and series names embedded into your dashboard. Dashboard
snapshots can be accessed by anyone who has the link and can reach the URL.
![](img/docs/v2/dashboard_snapshot_dialog.png)
![](/img/docs/v2/dashboard_snapshot_dialog.png)
### Publish snapshots
You can publish snapshots to you local instance or to [snapshot.raintank.io](http://snapshot.raintank.io). The later is a free service
......
......@@ -11,7 +11,7 @@ weight = 2
# Singlestat Panel
![](img/docs/v1/singlestat_panel2.png)
![](/img/docs/v1/singlestat_panel2.png)
The Singlestat Panel allows you to show the one main summary stat of a SINGLE series. It reduces the series into a single number (by looking at the max, min, average, or sum of values in the series). Singlestat also provides thresholds to color the stat or the Panel background. It can also translate the single number into a text value, and show a sparkline summary of the series.
......@@ -19,7 +19,7 @@ The Singlestat Panel allows you to show the one main summary stat of a SINGLE se
The singlestat panel has a normal query editor to allow you define your exact metric queries like many other Panels. Through the Options tab, you can access the Singlestat-specific functionality.
<img class="no-shadow" src="img/docs/v1/Singlestat-BaseSettings.png">
<img class="no-shadow" src="/img/docs/v1/Singlestat-BaseSettings.png">
1. `Big Value`: Big Value refers to how we display the main stat for the Singlestat Panel. This is always a single value that is displayed in the Panel in between two strings, `Prefix` and `Suffix`. The single number is calculated by choosing a function (min,max,average,current,total) of your metric query. This functions reduces your query into a single numeric value.
2. `Font Size`: You can use this section to select the font size of the different texts in the Singlestat Panel, i.e. prefix, value and postfix.
......@@ -32,19 +32,19 @@ The singlestat panel has a normal query editor to allow you define your exact me
The coloring options of the Singlestat Panel config allow you to dynamically change the colors based on the Singlestat value.
<img class="no-shadow" src="img/docs/v1/Singlestat-Coloring.png">
<img class="no-shadow" src="/img/docs/v1/Singlestat-Coloring.png">
1. `Background`: This checkbox applies the configured thresholds and colors to the entirety of the Singlestat Panel background.
2. `Value`: This checkbox applies the configured thresholds and colors to the summary stat.
3. `Thresholds`: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90.
4. `Colors`: Select a color and opacity
5. `Invert order`: This link toggles the threshold color order.</br>For example: Green, Orange, Red (<img class="no-shadow" src="img/docs(v1/gyr.png">) will become Red, Orange, Green (<img class="no-shadow" src="img/docs/v1/ryg.png">).
5. `Invert order`: This link toggles the threshold color order.</br>For example: Green, Orange, Red (<img class="no-shadow" src="/img/docs(v1/gyr.png">) will become Red, Orange, Green (<img class="no-shadow" src="/img/docs/v1/ryg.png">).
### Spark Lines
Sparklines are a great way of seeing the historical data related to the summary stat, providing valuable context at a glance. Sparklines act differently than traditional Graph Panels and do not include x or y axis, coordinates, a legend, or ability to interact with the graph.
<img class="no-shadow" src="img/docs/v1/Singlestat-Sparklines.png">
<img class="no-shadow" src="/img/docs/v1/Singlestat-Sparklines.png">
1. `Show`: The show checkbox will toggle whether the spark line is shown in the Panel. When unselected, only the Singlestat value will appear.
2. `Background`: Check if you want the sparklines to take up the full panel width, or uncheck if they should be below the main Singlestat value.
......@@ -57,13 +57,13 @@ Sparklines are a great way of seeing the historical data related to the summary
Value to text mapping allows you to translate the value of the summary stat into explicit text. The text will respect all styling, thresholds and customization defined for the value. This can be useful to translate the number of the main Singlestat value into a context-specific human-readable word or message.
<img class="no-shadow" src="img/docs/v1/Singlestat-ValueMapping.png">
<img class="no-shadow" src="/img/docs/v1/Singlestat-ValueMapping.png">
## Troubleshooting
### Multiple Series Error
<img class="no-shadow" src="img/docs/v2/Singlestat-MultiSeriesError.png">
<img class="no-shadow" src="/img/docs/v2/Singlestat-MultiSeriesError.png">
Grafana 2.5 introduced stricter checking for multiple-series on singlestat panels. In previous versions, the panel logic did not verify that only a single series was used, and instead, displayed the first series encountered. Depending on your data source, this could have lead to inconsistent data being shown and/or a general confusion about which metric was being displayed.
......
......@@ -11,7 +11,7 @@ weight = 2
# Table Panel
<img src="assets/img/features/table-panel.png">
<img src="/assets/img/features/table-panel.png">
The new table panel is very flexible, supporting both multiple modes for time series as well as for
table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options.
......@@ -22,7 +22,7 @@ To view table panels in action and test different configurations with sample dat
The table panel has many ways to manipulate your data for optimal presentation.
<img class="no-shadow" src="img/docs/v2/table-config2.png">
<img class="no-shadow" src="/img/docs/v2/table-config2.png">
1. `Data`: Control how your query is transformed into a table.
2. `Table Display`: Table display options.
......@@ -30,7 +30,7 @@ The table panel has many ways to manipulate your data for optimal presentation.
## Data to Table
<img class="no-shadow" src="img/docs/v2/table-data-options.png">
<img class="no-shadow" src="/img/docs/v2/table-data-options.png">
The data section contains the **To Table Transform (1)**. This is the primary option for how your data/metric
query should be transformed into a table format. The **Columns (2)** option allows you to select what columns
......@@ -38,38 +38,38 @@ you want in the table. Only applicable for some transforms.
### Time series to rows
<img src="img/docs/v2/table_ts_to_rows2.png">
<img src="/img/docs/v2/table_ts_to_rows2.png">
In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series.
### Time series to columns
![](img/docs/v2/table_ts_to_columns2.png)
![](/img/docs/v2/table_ts_to_columns2.png)
This transform allows you to take multiple time series and group them by time. Which will result in the primary column being `Time` and a column for each time series.
### Time series aggregations
![](img/docs/v2/table_ts_to_aggregations2.png)
![](/img/docs/v2/table_ts_to_aggregations2.png)
This table transformation will lay out your table into rows by metric, allowing columns of `Avg`, `Min`, `Max`, `Total`, `Current` and `Count`. More than one column can be added.
### Annotations
![](img/docs/v2/table_annotations.png)
![](/img/docs/v2/table_annotations.png)
If you have annotations enabled in the dashboard you can have the table show them. If you configure this
mode then any queries you have in the metrics tab will be ignored.
### JSON Data
![](img/docs/v2/table_json_data.png)
![](/img/docs/v2/table_json_data.png)
If you have an Elasticsearch **Raw Document** query or an Elasticsearch query without a `date histogram` use this
transform mode and pick the columns using the **Columns** section.
![](img/docs/v2/elastic_raw_doc.png)
![](/img/docs/v2/elastic_raw_doc.png)
## Table Display
<img class="no-shadow" src="img/docs/v2/table-display.png">
<img class="no-shadow" src="/img/docs/v2/table-display.png">
1. `Pagination (Page Size)`: The table display fields allow you to control The `Pagination` (page size) is the threshold at which the table rows will be broken into pages. For example, if your table had 95 records with a pagination value of 10, your table would be split across 9 pages.
2. `Scroll`: The `scroll bar` checkbox toggles the ability to scroll within the panel, when unchecked, the panel height will grow to display all rows.
......@@ -80,7 +80,7 @@ transform mode and pick the columns using the **Columns** section.
The column styles allow you control how dates and numbers are formatted.
<img class="no-shadow" src="img/docs/v2/Column-Options.png">
<img class="no-shadow" src="/img/docs/v2/Column-Options.png">
1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values.
2. `Type`: The three supported types of types are `Number`, `String` and `Date`.
......
......@@ -10,7 +10,7 @@ weight = 1
# Templating
<img class="no-shadow" src="img/docs/v2/templating_var_list.png">
<img class="no-shadow" src="/img/docs/v2/templating_var_list.png">
Dashboard Templating allows you to make your Dashboards more interactive and dynamic.
......@@ -49,7 +49,7 @@ For example, if you were using Templating to list all 20 of your applications, y
> Note: Multi-Select Tagging functionality is currently experimental but is part of Grafana 2.1. To enable this feature click the enable icon when editing Template options for a particular variable.
<img class="no-shadow" src="img/docs/v2/template-tags-config.png">
<img class="no-shadow" src="/img/docs/v2/template-tags-config.png">
Grafana gets the list of tags and the list of values in each tag by performing two queries on your metric namespace.
......@@ -61,13 +61,13 @@ Note: a proof of concept shim that translates the metric query into a SQL call i
Once configured, Multi-Select Tagging provides a convenient way to group and your template variables, and slice your data in the exact way you want. The Tags can be seen on the right side of the template pull-down.
![](img/docs/v2/multi-select.gif)
![](/img/docs/v2/multi-select.gif)
### Interval
Use the `Interval` type to create Template variables around time ranges (eg. `1m`,`1h`, `1d`). There is also a special `auto` option that will change depending on the current time range, you can specify how many times the current time range should be divided to calculate the current `auto` range.
![](img/docs/v2/templated_variable_parameter.png)
![](/img/docs/v2/templated_variable_parameter.png)
### Custom
......
......@@ -13,7 +13,7 @@ weight = 7
Grafana provides numerous ways to manage the time ranges of the data being visualized, both at the Dashboard-level and the Panel-level.
<img class="no-shadow" src="img/docs/whatsnew_2_5/timepicker.png">
<img class="no-shadow" src="/img/docs/whatsnew_2_5/timepicker.png">
In the top right, you have the master Dashboard time picker (it's in between the 'Zoom out' and the 'Refresh' links).
......@@ -43,7 +43,7 @@ Previous Month | `now-1M/M` | `now-1M/M`
There are two settings available from the Dashboard Settings area, allowing customization of the auto-refresh intervals and the definition of `now`.
<img class="no-shadow" src="img/docs/v2/TimePicker-TimeOptions.png">
<img class="no-shadow" src="/img/docs/v2/TimePicker-TimeOptions.png">
### Auto-Refresh Options
......@@ -59,11 +59,11 @@ Users often ask, [when will then be now](https://www.youtube.com/watch?v=VeZ9HhH
You can override the relative time range for individual panels, causing them to be different than what is selected in the Dashboard time picker in the upper right. This allows you to show metrics from different time periods or days at the same time.
<img class="no-shadow" src="img/docs/v2/panel_time_override.jpg">
<img class="no-shadow" src="/img/docs/v2/panel_time_override.jpg">
You control these overrides in panel editor mode and the tab `Time Range`.
<img class="no-shadow" src="img/docs/v2/time_range_tab.jpg">
<img class="no-shadow" src="/img/docs/v2/time_range_tab.jpg">
When you zoom or change the Dashboard time to a custom absolute time range, all panel overrides will be disabled. The panel relative time override is only active when the dashboard time is also relative. The panel timeshift override is always active, even when the dashboard time is absolute.
......
......@@ -22,7 +22,7 @@ take you to the graph.
> is so Hipchat and Slack can show them reliably (they require the image to be publicly available).
<div class="text-center">
<img src="img/tutorials/hubot_grafana.png" class="center"></a>
<img src="/img/tutorials/hubot_grafana.png" class="center"></a>
</div>
## What is Hubot?
......@@ -70,7 +70,7 @@ To verify that this feature works try the `Direct link to rendered image` link i
If you do not get an image when opening this link verify that the required font packages are installed for phantomjs to work.
### Grafana API Key
<img src="img/v2/orgdropdown_api_keys.png" style="width: 150px" class="right"></img>
<img src="/img/v2/orgdropdown_api_keys.png" style="width: 150px" class="right"></img>
You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key.
You can add these from the API Keys page which you find in the Organization dropdown.
......@@ -115,7 +115,7 @@ Now you can add an alias like this:
<div class="text-center">
Using the alias:<br>
<img src="img/tutorials/hubot_grafana2.png" class="center"></a>
<img src="/img/tutorials/hubot_grafana2.png" class="center"></a>
</div>
## Summary
......
......@@ -15,7 +15,7 @@
"es6-promise": "^3.0.2",
"es6-shim": "^0.35.1",
"expect.js": "~0.2.0",
"glob": "~3.2.7",
"glob": "~7.1.1",
"grunt": "~0.4.0",
"grunt-angular-templates": "^0.5.5",
"grunt-cli": "~0.1.13",
......@@ -68,17 +68,20 @@
"license": "Apache-2.0",
"dependencies": {
"eventemitter3": "^1.2.0",
"gaze": "^1.1.2",
"grunt-jscs": "~1.5.x",
"grunt-sass-lint": "^0.2.0",
"grunt-sync": "^0.4.1",
"karma-sinon": "^1.0.3",
"lodash": "^2.4.1",
"mousetrap": "^1.6.0",
"remarkable": "^1.6.2",
"sinon": "1.16.1",
"systemjs-builder": "^0.15.13",
"tether": "^1.2.0",
"tether-drop": "^1.4.2",
"tslint": "^3.4.0",
"typescript": "^1.7.5"
"typescript": "^1.7.5",
"virtual-scroll": "^1.1.1"
}
}
......@@ -175,10 +175,10 @@ func GetAlertNotifications(c *middleware.Context) Response {
return ApiError(500, "Failed to get alert notifications", err)
}
var result []dtos.AlertNotification
result := make([]*dtos.AlertNotification, 0)
for _, notification := range query.Result {
result = append(result, dtos.AlertNotification{
result = append(result, &dtos.AlertNotification{
Id: notification.Id,
Name: notification.Name,
Type: notification.Type,
......
......@@ -140,7 +140,7 @@ func init() {
func handleGetRegions(req *cwRequest, c *middleware.Context) {
regions := []string{
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "cn-north-1",
"eu-central-1", "eu-west-1", "sa-east-1", "us-east-1", "us-west-1", "us-west-2",
"eu-central-1", "eu-west-1", "sa-east-1", "us-east-1", "us-west-1", "us-west-2", "us-gov-west-1",
}
result := []interface{}{}
......
......@@ -127,6 +127,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
"name": panel.Name,
"id": panel.Id,
"info": panel.Info,
"sort": getPanelSort(panel.Id),
}
}
......@@ -150,6 +151,25 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
return jsonObj, nil
}
func getPanelSort(id string) int {
sort := 100
switch id {
case "graph":
sort = 1
case "singlestat":
sort = 2
case "table":
sort = 3
case "text":
sort = 4
case "alertlist":
sort = 5
case "dashlist":
sort = 6
}
return sort
}
func GetFrontendSettings(c *middleware.Context) {
settings, err := getFrontendSettingsMap(c)
if err != nil {
......
......@@ -26,6 +26,18 @@ func ValidateOrgPlaylist(c *middleware.Context) {
c.JsonApiErr(403, "You are not allowed to edit/view playlist", nil)
return
}
items, itemsErr := LoadPlaylistItemDTOs(id)
if itemsErr != nil {
c.JsonApiErr(404, "Playlist items not found", err)
return
}
if len(items) == 0 {
c.JsonApiErr(404, "Playlist is empty", itemsErr)
return
}
}
func SearchPlaylists(c *middleware.Context) Response {
......
......@@ -9,43 +9,44 @@ func init() {
}
var (
M_Instance_Start Counter
M_Page_Status_200 Counter
M_Page_Status_500 Counter
M_Page_Status_404 Counter
M_Page_Status_Unknown Counter
M_Api_Status_200 Counter
M_Api_Status_404 Counter
M_Api_Status_500 Counter
M_Api_Status_Unknown Counter
M_Proxy_Status_200 Counter
M_Proxy_Status_404 Counter
M_Proxy_Status_500 Counter
M_Proxy_Status_Unknown Counter
M_Api_User_SignUpStarted Counter
M_Api_User_SignUpCompleted Counter
M_Api_User_SignUpInvite Counter
M_Api_Dashboard_Save Timer
M_Api_Dashboard_Get Timer
M_Api_Dashboard_Search Timer
M_Api_Admin_User_Create Counter
M_Api_Login_Post Counter
M_Api_Login_OAuth Counter
M_Api_Org_Create Counter
M_Api_Dashboard_Snapshot_Create Counter
M_Api_Dashboard_Snapshot_External Counter
M_Api_Dashboard_Snapshot_Get Counter
M_Models_Dashboard_Insert Counter
M_Alerting_Result_State_Alerting Counter
M_Alerting_Result_State_Ok Counter
M_Alerting_Result_State_Paused Counter
M_Alerting_Result_State_NoData Counter
M_Alerting_Result_State_ExecError Counter
M_Alerting_Result_State_Pending Counter
M_Alerting_Active_Alerts Counter
M_Alerting_Notification_Sent_Slack Counter
M_Alerting_Notification_Sent_Email Counter
M_Alerting_Notification_Sent_Webhook Counter
M_Instance_Start Counter
M_Page_Status_200 Counter
M_Page_Status_500 Counter
M_Page_Status_404 Counter
M_Page_Status_Unknown Counter
M_Api_Status_200 Counter
M_Api_Status_404 Counter
M_Api_Status_500 Counter
M_Api_Status_Unknown Counter
M_Proxy_Status_200 Counter
M_Proxy_Status_404 Counter
M_Proxy_Status_500 Counter
M_Proxy_Status_Unknown Counter
M_Api_User_SignUpStarted Counter
M_Api_User_SignUpCompleted Counter
M_Api_User_SignUpInvite Counter
M_Api_Dashboard_Save Timer
M_Api_Dashboard_Get Timer
M_Api_Dashboard_Search Timer
M_Api_Admin_User_Create Counter
M_Api_Login_Post Counter
M_Api_Login_OAuth Counter
M_Api_Org_Create Counter
M_Api_Dashboard_Snapshot_Create Counter
M_Api_Dashboard_Snapshot_External Counter
M_Api_Dashboard_Snapshot_Get Counter
M_Models_Dashboard_Insert Counter
M_Alerting_Result_State_Alerting Counter
M_Alerting_Result_State_Ok Counter
M_Alerting_Result_State_Paused Counter
M_Alerting_Result_State_NoData Counter
M_Alerting_Result_State_Pending Counter
M_Alerting_Active_Alerts Counter
M_Alerting_Notification_Sent_Slack Counter
M_Alerting_Notification_Sent_Email Counter
M_Alerting_Notification_Sent_Webhook Counter
M_Alerting_Notification_Sent_PagerDuty Counter
// Timers
M_DataSource_ProxyReq_Timer Timer
......@@ -102,13 +103,13 @@ func initMetricVars(settings *MetricSettings) {
M_Alerting_Result_State_Ok = RegCounter("alerting.result", "state", "ok")
M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused")
M_Alerting_Result_State_NoData = RegCounter("alerting.result", "state", "no_data")
M_Alerting_Result_State_ExecError = RegCounter("alerting.result", "state", "exec_error")
M_Alerting_Result_State_Pending = RegCounter("alerting.result", "state", "pending")
M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
M_Alerting_Notification_Sent_Email = RegCounter("alerting.notifications_sent", "type", "email")
M_Alerting_Notification_Sent_Webhook = RegCounter("alerting.notifications_sent", "type", "webhook")
M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty")
// Timers
M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
......
......@@ -9,35 +9,47 @@ import (
type AlertStateType string
type AlertSeverityType string
type NoDataOption string
type ExecutionErrorOption string
const (
AlertStateNoData AlertStateType = "no_data"
AlertStateExecError AlertStateType = "execution_error"
AlertStatePaused AlertStateType = "paused"
AlertStateAlerting AlertStateType = "alerting"
AlertStateOK AlertStateType = "ok"
AlertStatePending AlertStateType = "pending"
AlertStateNoData AlertStateType = "no_data"
AlertStatePaused AlertStateType = "paused"
AlertStateAlerting AlertStateType = "alerting"
AlertStateOK AlertStateType = "ok"
AlertStatePending AlertStateType = "pending"
)
const (
NoDataSetNoData NoDataOption = "no_data"
NoDataSetAlerting NoDataOption = "alerting"
NoDataSetOK NoDataOption = "ok"
NoDataKeepState NoDataOption = "keep_state"
)
const (
ExecutionErrorSetAlerting ExecutionErrorOption = "alerting"
ExecutionErrorKeepState ExecutionErrorOption = "keep_state"
)
func (s AlertStateType) IsValid() bool {
return s == AlertStateOK || s == AlertStateNoData || s == AlertStateExecError || s == AlertStatePaused || s == AlertStatePending
return s == AlertStateOK || s == AlertStateNoData || s == AlertStatePaused || s == AlertStatePending
}
func (s NoDataOption) IsValid() bool {
return s == NoDataSetNoData || s == NoDataSetAlerting || s == NoDataSetOK || s == NoDataKeepState
return s == NoDataSetNoData || s == NoDataSetAlerting || s == NoDataKeepState
}
func (s NoDataOption) ToAlertState() AlertStateType {
return AlertStateType(s)
}
func (s ExecutionErrorOption) IsValid() bool {
return s == ExecutionErrorSetAlerting || s == ExecutionErrorKeepState
}
func (s ExecutionErrorOption) ToAlertState() AlertStateType {
return AlertStateType(s)
}
type Alert struct {
Id int64
Version int64
......
......@@ -16,14 +16,6 @@ type SendEmailCommandSync struct {
SendEmailCommand
}
type SendWebhook struct {
Url string
User string
Password string
Body string
HttpMethod string
}
type SendWebhookSync struct {
Url string
User string
......
......@@ -33,15 +33,17 @@ type AlertQuery struct {
To string
}
func (c *QueryCondition) Eval(context *alerting.EvalContext) {
func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.ConditionResult, error) {
timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
seriesList, err := c.executeQuery(context, timeRange)
if err != nil {
context.Error = err
return
return nil, err
}
emptySerieCount := 0
evalMatchCount := 0
var matches []*alerting.EvalMatch
for _, series := range seriesList {
reducedValue := c.Reducer.Reduce(series)
evalMatch := c.Evaluator.Eval(reducedValue)
......@@ -58,15 +60,20 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
}
if evalMatch {
context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
evalMatchCount++
matches = append(matches, &alerting.EvalMatch{
Metric: series.Name,
Value: reducedValue.Float64,
})
}
}
context.NoDataFound = emptySerieCount == len(seriesList)
context.Firing = len(context.EvalMatches) > 0
return &alerting.ConditionResult{
Firing: evalMatchCount > 0,
NoDataFound: emptySerieCount == len(seriesList),
EvalMatches: matches,
}, nil
}
func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) {
......
......@@ -46,19 +46,19 @@ func TestQueryCondition(t *testing.T) {
Convey("should fire when avg is above 100", func() {
points := tsdb.NewTimeSeriesPointsFromArgs(120, 0)
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.Firing, ShouldBeTrue)
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeTrue)
})
Convey("Should not fire when avg is below 100", func() {
points := tsdb.NewTimeSeriesPointsFromArgs(90, 0)
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.Firing, ShouldBeFalse)
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeFalse)
})
Convey("Should fire if only first serie matches", func() {
......@@ -66,10 +66,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)),
}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.Firing, ShouldBeTrue)
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeTrue)
})
Convey("Empty series", func() {
......@@ -78,10 +78,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()),
}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.NoDataFound, ShouldBeTrue)
So(err, ShouldBeNil)
So(cr.NoDataFound, ShouldBeTrue)
})
Convey("Should set NoDataFound both series contains null", func() {
......@@ -89,10 +89,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.NoDataFound, ShouldBeTrue)
So(err, ShouldBeNil)
So(cr.NoDataFound, ShouldBeTrue)
})
Convey("Should not set NoDataFound if one serie is empty", func() {
......@@ -100,10 +100,10 @@ func TestQueryCondition(t *testing.T) {
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
}
ctx.exec()
cr, err := ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.NoDataFound, ShouldBeFalse)
So(err, ShouldBeNil)
So(cr.NoDataFound, ShouldBeFalse)
})
})
})
......@@ -120,7 +120,7 @@ type queryConditionTestContext struct {
type queryConditionScenarioFunc func(c *queryConditionTestContext)
func (ctx *queryConditionTestContext) exec() {
func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error) {
jsonModel, err := simplejson.NewJson([]byte(`{
"type": "query",
"query": {
......@@ -146,7 +146,7 @@ func (ctx *queryConditionTestContext) exec() {
}, nil
}
condition.Eval(ctx.result)
return condition.Eval(ctx.result)
}
func queryConditionScenario(desc string, fn queryConditionScenarioFunc) {
......
......@@ -62,6 +62,15 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
case "count":
value = float64(len(series.Points))
allNull = false
case "last":
points := series.Points
for i := len(points) - 1; i >= 0; i-- {
if points[i][0].Valid {
value = points[i][0].Float64
allNull = false
break
}
}
}
if allNull {
......
......@@ -35,6 +35,12 @@ func TestSimpleReducer(t *testing.T) {
result := testReducer("count", 1, 2, 3000)
So(result, ShouldEqual, float64(3))
})
Convey("last", func() {
result := testReducer("last", 1, 2, 3000)
So(result, ShouldEqual, float64(3000))
})
})
}
......
......@@ -26,10 +26,23 @@ type EvalContext struct {
ImagePublicUrl string
ImageOnDiskPath string
NoDataFound bool
PrevAlertState m.AlertStateType
Ctx context.Context
}
func NewEvalContext(alertCtx context.Context, rule *Rule) *EvalContext {
return &EvalContext{
Ctx: alertCtx,
StartTime: time.Now(),
Rule: rule,
Logs: make([]*ResultLogEntry, 0),
EvalMatches: make([]*EvalMatch, 0),
log: log.New("alerting.evalContext"),
PrevAlertState: rule.State,
}
}
type StateDescription struct {
Color string
Text string
......@@ -48,11 +61,6 @@ func (c *EvalContext) GetStateModel() *StateDescription {
Color: "#888888",
Text: "No Data",
}
case m.AlertStateExecError:
return &StateDescription{
Color: "#000",
Text: "Execution Error",
}
case m.AlertStateAlerting:
return &StateDescription{
Color: "#D63232",
......@@ -63,6 +71,18 @@ func (c *EvalContext) GetStateModel() *StateDescription {
}
}
func (c *EvalContext) ShouldUpdateAlertState() bool {
return c.Rule.State != c.PrevAlertState
}
func (c *EvalContext) ShouldSendNotification() bool {
if (c.PrevAlertState == m.AlertStatePending) && (c.Rule.State == m.AlertStateOK) {
return false
}
return true
}
func (a *EvalContext) GetDurationMs() float64 {
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
}
......@@ -97,14 +117,3 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
return ruleUrl, nil
}
}
func NewEvalContext(alertCtx context.Context, rule *Rule) *EvalContext {
return &EvalContext{
Ctx: alertCtx,
StartTime: time.Now(),
Rule: rule,
Logs: make([]*ResultLogEntry, 0),
EvalMatches: make([]*EvalMatch, 0),
log: log.New("alerting.evalContext"),
}
}
package alerting
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertingEvalContext(t *testing.T) {
Convey("Eval context", t, func() {
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
Convey("Should update alert state", func() {
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.State = models.AlertStateAlerting
So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
})
Convey("ok -> ok", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.State = models.AlertStateOK
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
})
})
Convey("Should send notifications", func() {
Convey("pending -> ok", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Rule.State = models.AlertStateOK
So(ctx.ShouldSendNotification(), ShouldBeFalse)
})
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.State = models.AlertStateAlerting
So(ctx.ShouldSendNotification(), ShouldBeTrue)
})
})
})
}
......@@ -20,8 +20,12 @@ func NewEvalHandler() *DefaultEvalHandler {
}
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
firing := true
for _, condition := range context.Rule.Conditions {
condition.Eval(context)
cr, err := condition.Eval(context)
if err != nil {
context.Error = err
}
// break if condition could not be evaluated
if context.Error != nil {
......@@ -29,11 +33,15 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
}
// break if result has not triggered yet
if context.Firing == false {
if cr.Firing == false {
firing = false
break
}
context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
}
context.Firing = firing
context.EndTime = time.Now()
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
metrics.M_Alerting_Exeuction_Time.Update(elapsedTime)
......
......@@ -8,11 +8,12 @@ import (
)
type conditionStub struct {
firing bool
firing bool
matches []*EvalMatch
}
func (c *conditionStub) Eval(context *EvalContext) {
context.Firing = c.firing
func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil
}
func TestAlertingExecutor(t *testing.T) {
......@@ -30,10 +31,10 @@ func TestAlertingExecutor(t *testing.T) {
So(context.Firing, ShouldEqual, true)
})
Convey("Show return false with not passing condition", func() {
Convey("Show return false with not passing asdf", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true},
&conditionStub{firing: true, matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
&conditionStub{firing: false},
},
})
......
......@@ -3,6 +3,8 @@ package alerting
import (
"errors"
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
......@@ -104,7 +106,8 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
panelQuery := findPanelQueryByRefId(panel, queryRefId)
if panelQuery == nil {
return nil, ValidationError{Reason: "Alert refes to query that cannot be found"}
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
return nil, ValidationError{Reason: reason}
}
dsName := ""
......
......@@ -21,6 +21,12 @@ type Notifier interface {
GetIsDefault() bool
}
type ConditionResult struct {
Firing bool
NoDataFound bool
EvalMatches []*EvalMatch
}
type Condition interface {
Eval(result *EvalContext)
Eval(result *EvalContext) (*ConditionResult, error)
}
......@@ -14,9 +14,9 @@ type ResultLogEntry struct {
}
type EvalMatch struct {
Value float64
Metric string
Tags map[string]string
Value float64 `json:"value"`
Metric string `json:"metric"`
Tags map[string]string `json:"tags"`
}
type Level struct {
......
......@@ -49,7 +49,7 @@ func (n *RootNotifier) Notify(context *EvalContext) error {
return err
}
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "Amount to send", len(notifiers))
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers))
if len(notifiers) == 0 {
return nil
......
package notifiers
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
func init() {
alerting.RegisterNotifier("pagerduty", NewPagerdutyNotifier)
}
var (
pagerdutyEventApiUrl string = "https://events.pagerduty.com/generic/2010-04-15/create_event.json"
)
func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
key := model.Settings.Get("integrationKey").MustString()
if key == "" {
return nil, alerting.ValidationError{Reason: "Could not find integration key property in settings"}
}
return &PagerdutyNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Key: key,
log: log.New("alerting.notifier.pagerduty"),
}, nil
}
type PagerdutyNotifier struct {
NotifierBase
Key string
log log.Logger
}
func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Notifying Pagerduty")
metrics.M_Alerting_Notification_Sent_PagerDuty.Inc(1)
if evalContext.Rule.State == m.AlertStateAlerting {
bodyJSON := simplejson.New()
bodyJSON.Set("service_key", this.Key)
bodyJSON.Set("description", evalContext.Rule.Name+" - "+evalContext.Rule.Message)
bodyJSON.Set("client", "Grafana")
bodyJSON.Set("event_type", "trigger")
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 != "" {
var contexts []interface{}
imageJSON := simplejson.New()
imageJSON.Set("type", "image")
imageJSON.Set("src", evalContext.ImagePublicUrl)
contexts[0] = imageJSON
bodyJSON.Set("contexts", contexts)
}
body, _ := bodyJSON.MarshalJSON()
cmd := &m.SendWebhookSync{
Url: pagerdutyEventApiUrl,
Body: string(body),
HttpMethod: "POST",
}
if err := bus.Dispatch(cmd); err != nil {
this.log.Error("Failed to send notification to Pagerduty", "error", err, "body", string(body))
}
} else {
this.log.Info("Not sending a trigger to Pagerduty", "state", evalContext.Rule.State)
}
return nil
}
package notifiers
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestPagerdutyNotifier(t *testing.T) {
Convey("Pagerduty 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: "pageduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
_, err := NewPagerdutyNotifier(model)
So(err, ShouldNotBeNil)
})
Convey("settings should trigger incident", func() {
json := `
{
"integrationKey": "abcdefgh0123456789"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
not, err := NewPagerdutyNotifier(model)
pagerdutyNotifier := not.(*PagerdutyNotifier)
So(err, ShouldBeNil)
So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing")
So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty")
So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789")
})
})
})
}
......@@ -23,20 +23,23 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
recipient := model.Settings.Get("recipient").MustString()
mention := model.Settings.Get("mention").MustString()
return &SlackNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Url: url,
Recipient: recipient,
Recipient: recipient,
Mention: mention,
log: log.New("alerting.notifier.slack"),
}, nil
}
type SlackNotifier struct {
NotifierBase
Url string
Url string
Recipient string
log log.Logger
Mention string
log log.Logger
}
func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
......@@ -70,9 +73,9 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
})
}
message := ""
message := this.Mention
if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
message = evalContext.Rule.Message
message += " " + evalContext.Rule.Message
}
body := map[string]interface{}{
......
package notifiers
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestSlackNotifier(t *testing.T) {
Convey("Slack 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: "ops",
Type: "slack",
Settings: settingsJSON,
}
_, err := NewSlackNotifier(model)
So(err, ShouldNotBeNil)
})
Convey("from settings", func() {
json := `
{
"url": "http://google.com"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "ops",
Type: "slack",
Settings: settingsJSON,
}
not, err := NewSlackNotifier(model)
slackNotifier := not.(*SlackNotifier)
So(err, ShouldBeNil)
So(slackNotifier.Name, ShouldEqual, "ops")
So(slackNotifier.Type, ShouldEqual, "slack")
So(slackNotifier.Url, ShouldEqual, "http://google.com")
So(slackNotifier.Recipient, ShouldEqual, "")
So(slackNotifier.Mention, ShouldEqual, "")
})
Convey("from settings with Recipient and Mention", func() {
json := `
{
"url": "http://google.com",
"recipient": "#ds-opentsdb",
"mention": "@carl"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "ops",
Type: "slack",
Settings: settingsJSON,
}
not, err := NewSlackNotifier(model)
slackNotifier := not.(*SlackNotifier)
So(err, ShouldBeNil)
So(slackNotifier.Name, ShouldEqual, "ops")
So(slackNotifier.Type, ShouldEqual, "slack")
So(slackNotifier.Url, ShouldEqual, "http://google.com")
So(slackNotifier.Recipient, ShouldEqual, "#ds-opentsdb")
So(slackNotifier.Mention, ShouldEqual, "@carl")
})
})
})
}
......@@ -51,11 +51,11 @@ func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
ruleUrl, err := evalContext.GetRuleUrl()
if err == nil {
bodyJSON.Set("rule_url", ruleUrl)
bodyJSON.Set("ruleUrl", ruleUrl)
}
if evalContext.ImagePublicUrl != "" {
bodyJSON.Set("image_url", evalContext.ImagePublicUrl)
bodyJSON.Set("imageUrl", evalContext.ImagePublicUrl)
}
body, _ := bodyJSON.MarshalJSON()
......
......@@ -40,12 +40,12 @@ func TestWebhookNotifier(t *testing.T) {
}
not, err := NewWebHookNotifier(model)
emailNotifier := not.(*WebhookNotifier)
webhookNotifier := not.(*WebhookNotifier)
So(err, ShouldBeNil)
So(emailNotifier.Name, ShouldEqual, "ops")
So(emailNotifier.Type, ShouldEqual, "email")
So(emailNotifier.Url, ShouldEqual, "http://google.com")
So(webhookNotifier.Name, ShouldEqual, "ops")
So(webhookNotifier.Type, ShouldEqual, "email")
So(webhookNotifier.Url, ShouldEqual, "http://google.com")
})
})
})
......
......@@ -27,32 +27,55 @@ func NewResultHandler() *DefaultResultHandler {
}
}
func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
oldState := evalContext.Rule.State
func (handler *DefaultResultHandler) GetStateFromEvaluation(evalContext *EvalContext) m.AlertStateType {
if evalContext.Error != nil {
handler.log.Error("Alert Rule Result Error",
"ruleId", evalContext.Rule.Id,
"name", evalContext.Rule.Name,
"error", evalContext.Error,
"changing state to", evalContext.Rule.ExecutionErrorState.ToAlertState())
if evalContext.Rule.ExecutionErrorState == m.ExecutionErrorKeepState {
return evalContext.PrevAlertState
} else {
return evalContext.Rule.ExecutionErrorState.ToAlertState()
}
} else if evalContext.Firing {
return m.AlertStateAlerting
} else if evalContext.NoDataFound {
handler.log.Info("Alert Rule returned no data",
"ruleId", evalContext.Rule.Id,
"name", evalContext.Rule.Name,
"changing state to", evalContext.Rule.NoDataState.ToAlertState())
if evalContext.Rule.NoDataState == m.NoDataKeepState {
return evalContext.PrevAlertState
} else {
return evalContext.Rule.NoDataState.ToAlertState()
}
}
return m.AlertStateOK
}
func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
executionError := ""
annotationData := simplejson.New()
evalContext.Rule.State = handler.GetStateFromEvaluation(evalContext)
if evalContext.Error != nil {
handler.log.Error("Alert Rule Result Error", "ruleId", evalContext.Rule.Id, "error", evalContext.Error)
evalContext.Rule.State = m.AlertStateExecError
executionError = evalContext.Error.Error()
annotationData.Set("errorMessage", executionError)
} else if evalContext.Firing {
evalContext.Rule.State = m.AlertStateAlerting
}
if evalContext.Firing {
annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
} else {
if evalContext.NoDataFound {
if evalContext.Rule.NoDataState != m.NoDataKeepState {
evalContext.Rule.State = evalContext.Rule.NoDataState.ToAlertState()
}
} else {
evalContext.Rule.State = m.AlertStateOK
}
}
countStateResult(evalContext.Rule.State)
if handler.shouldUpdateAlertState(evalContext, oldState) {
handler.log.Info("New state change", "alertId", evalContext.Rule.Id, "newState", evalContext.Rule.State, "oldState", oldState)
if evalContext.ShouldUpdateAlertState() {
handler.log.Info("New state change", "alertId", evalContext.Rule.Id, "newState", evalContext.Rule.State, "prev state", evalContext.PrevAlertState)
cmd := &m.SetAlertStateCommand{
AlertId: evalContext.Rule.Id,
......@@ -76,7 +99,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
Title: evalContext.Rule.Name,
Text: evalContext.GetStateModel().Text,
NewState: string(evalContext.Rule.State),
PrevState: string(oldState),
PrevState: string(evalContext.PrevAlertState),
Epoch: time.Now().Unix(),
Data: annotationData,
}
......@@ -86,21 +109,14 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
handler.log.Error("Failed to save annotation for new alert state", "error", err)
}
if (oldState == m.AlertStatePending) && (evalContext.Rule.State == m.AlertStateOK) {
handler.log.Info("Notfication not sent", "oldState", oldState, "newState", evalContext.Rule.State)
} else {
if evalContext.ShouldSendNotification() {
handler.notifier.Notify(evalContext)
}
}
return nil
}
func (handler *DefaultResultHandler) shouldUpdateAlertState(evalContext *EvalContext, oldState m.AlertStateType) bool {
return evalContext.Rule.State != oldState
}
func countStateResult(state m.AlertStateType) {
switch state {
case m.AlertStatePending:
......@@ -113,7 +129,5 @@ func countStateResult(state m.AlertStateType) {
metrics.M_Alerting_Result_State_Paused.Inc(1)
case m.AlertStateNoData:
metrics.M_Alerting_Result_State_NoData.Inc(1)
case m.AlertStateExecError:
metrics.M_Alerting_Result_State_ExecError.Inc(1)
}
}
package alerting
// import (
// "context"
// "testing"
//
// "github.com/grafana/grafana/pkg/models"
// . "github.com/smartystreets/goconvey/convey"
// )
//
// func TestAlertResultHandler(t *testing.T) {
// Convey("Test result Handler", t, func() {
//
// handler := NewResultHandler()
// evalContext := NewEvalContext(context.TODO(), &Rule{})
//
// Convey("Should update", func() {
//
// Convey("when no earlier alert state", func() {
// oldState := models.AlertStateOK
//
// evalContext.Rule.State = models.AlertStateAlerting
// evalContext.Rule.NoDataState = models.NoDataKeepState
// evalContext.NoDataFound = true
//
// So(handler.shouldUpdateAlertState(evalContext, oldState), ShouldBeFalse)
// })
// })
// })
// }
import (
"context"
"testing"
"fmt"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertingResultHandler(t *testing.T) {
Convey("Result handler", t, func() {
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
dummieError := fmt.Errorf("dummie")
handler := NewResultHandler()
Convey("Should update alert state", func() {
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Firing = true
So(handler.GetStateFromEvaluation(ctx), ShouldEqual, models.AlertStateAlerting)
So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
})
Convey("ok -> error(alerting)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
})
Convey("ok -> error(keep_last)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
})
Convey("pending -> error(keep_last)", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
})
Convey("ok -> no_data(alerting)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.NoDataState = models.NoDataSetAlerting
ctx.NoDataFound = true
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
})
Convey("ok -> no_data(keep_last)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.NoDataState = models.NoDataKeepState
ctx.NoDataFound = true
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
})
Convey("pending -> no_data(keep_last)", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Rule.NoDataState = models.NoDataKeepState
ctx.NoDataFound = true
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
})
})
})
}
......@@ -11,17 +11,18 @@ import (
)
type Rule struct {
Id int64
OrgId int64
DashboardId int64
PanelId int64
Frequency int64
Name string
Message string
NoDataState m.NoDataOption
State m.AlertStateType
Conditions []Condition
Notifications []int64
Id int64
OrgId int64
DashboardId int64
PanelId int64
Frequency int64
Name string
Message string
NoDataState m.NoDataOption
ExecutionErrorState m.ExecutionErrorOption
State m.AlertStateType
Conditions []Condition
Notifications []int64
}
type ValidationError struct {
......@@ -77,6 +78,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
model.Frequency = ruleDef.Frequency
model.State = ruleDef.State
model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data"))
model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
jsonModel := simplejson.NewFromAny(v)
......
......@@ -10,7 +10,9 @@ import (
type FakeCondition struct{}
func (f *FakeCondition) Eval(context *EvalContext) {}
func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) {
return &ConditionResult{}, nil
}
func TestAlertRuleModel(t *testing.T) {
Convey("Testing alert rule", t, func() {
......
......@@ -99,7 +99,7 @@ func createDialer() (*gomail.Dialer, error) {
tlsconfig.Certificates = []tls.Certificate{cert}
}
d := gomail.NewPlainDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
d.TLSConfig = tlsconfig
return d, nil
}
......
......@@ -31,7 +31,6 @@ func Init() error {
bus.AddCtxHandler("email", sendEmailCommandHandlerSync)
bus.AddHandler("webhook", sendWebhook)
bus.AddCtxHandler("webhook", SendWebhookSync)
bus.AddEventListener(signUpStartedHandler)
......@@ -69,18 +68,6 @@ func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
})
}
func sendWebhook(cmd *m.SendWebhook) error {
addToWebhookQueue(&Webhook{
Url: cmd.Url,
User: cmd.User,
Password: cmd.Password,
Body: cmd.Body,
HttpMethod: cmd.HttpMethod,
})
return nil
}
func subjectTemplateFunc(obj map[string]interface{}, value string) string {
obj["value"] = value
return ""
......
......@@ -233,6 +233,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
"DELETE FROM dashboard_tag WHERE dashboard_id = ? ",
"DELETE FROM star WHERE dashboard_id = ? ",
"DELETE FROM dashboard WHERE id = ?",
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
}
for _, sql := range deletes {
......
......@@ -8,6 +8,7 @@ import (
"net/http"
"net/url"
"path"
"regexp"
"strings"
"golang.org/x/net/context/ctxhttp"
......@@ -49,9 +50,9 @@ func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice,
for _, query := range queries {
if fullTarget, err := query.Model.Get("targetFull").String(); err == nil {
formData["target"] = []string{fullTarget}
formData["target"] = []string{fixIntervalFormat(fullTarget)}
} else {
formData["target"] = []string{query.Model.Get("target").MustString()}
formData["target"] = []string{fixIntervalFormat(query.Model.Get("target").MustString())}
}
}
......@@ -141,3 +142,17 @@ func formatTimeRange(input string) string {
}
return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1)
}
func fixIntervalFormat(target string) string {
rMinute := regexp.MustCompile(`'(\d+)m'`)
rMin := regexp.MustCompile("m")
target = rMinute.ReplaceAllStringFunc(target, func(m string) string {
return rMin.ReplaceAllString(m, "min")
})
rMonth := regexp.MustCompile(`'(\d+)M'`)
rMon := regexp.MustCompile("M")
target = rMonth.ReplaceAllStringFunc(target, func(M string) string {
return rMon.ReplaceAllString(M, "mon")
})
return target
}
package graphite
import (
. "github.com/smartystreets/goconvey/convey"
"testing"
)
func TestGraphiteFunctions(t *testing.T) {
Convey("Testing Graphite Functions", t, func() {
Convey("formatting time range for now", func() {
timeRange := formatTimeRange("now")
So(timeRange, ShouldEqual, "now")
})
Convey("formatting time range for now-1m", func() {
timeRange := formatTimeRange("now-1m")
So(timeRange, ShouldEqual, "now-1min")
})
Convey("formatting time range for now-1M", func() {
timeRange := formatTimeRange("now-1M")
So(timeRange, ShouldEqual, "now-1mon")
})
Convey("fix interval format in query for 1m", func() {
timeRange := fixIntervalFormat("aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1m'), 4)")
So(timeRange, ShouldEqual, "aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1min'), 4)")
})
Convey("fix interval format in query for 1M", func() {
timeRange := fixIntervalFormat("aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1M'), 4)")
So(timeRange, ShouldEqual, "aliasByNode(hitcount(averageSeries(app.grafana.*.dashboards.views.count), '1mon'), 4)")
})
Convey("should not override query for 1M", func() {
timeRange := fixIntervalFormat("app.grafana.*.dashboards.views.1M.count")
So(timeRange, ShouldEqual, "app.grafana.*.dashboards.views.1M.count")
})
Convey("should not override query for 1m", func() {
timeRange := fixIntervalFormat("app.grafana.*.dashboards.views.1m.count")
So(timeRange, ShouldEqual, "app.grafana.*.dashboards.views.1m.count")
})
})
}
......@@ -2,6 +2,7 @@ package influxdb
import (
"fmt"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/tsdb"
......@@ -42,7 +43,18 @@ func (qb *QueryBuilder) renderTags(query *Query) []string {
str += " "
}
res = append(res, fmt.Sprintf(`%s"%s" %s '%s'`, str, tag.Key, tag.Operator, tag.Value))
value := tag.Value
nValue, err := strconv.ParseFloat(tag.Value, 64)
if tag.Operator == "=~" || tag.Operator == "!~" {
value = fmt.Sprintf("%s", value)
} else if err == nil {
value = fmt.Sprintf("%v", nValue)
} else {
value = fmt.Sprintf("'%s'", value)
}
res = append(res, fmt.Sprintf(`%s"%s" %s %s`, str, tag.Key, tag.Operator, value))
}
return res
......
......@@ -3,6 +3,8 @@ package influxdb
import (
"testing"
"strings"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
)
......@@ -83,5 +85,23 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `Raw query`)
})
Convey("can render regex tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: "value", Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ value`)
})
Convey("can render number tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "1", Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 1`)
})
Convey("can render string tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "value", Key: "key"}}}
So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 'value'`)
})
})
}
......@@ -81,6 +81,10 @@ func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlic
func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string {
reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`)
if query.LegendFormat == "" {
return metric.String()
}
result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
labelName := strings.Replace(string(in), "{{", "", 1)
labelName = strings.Replace(labelName, "}}", "", 1)
......@@ -108,10 +112,7 @@ func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*Prom
return nil, err
}
format, err := queryModel.Model.Get("legendFormat").String()
if err != nil {
return nil, err
}
format := queryModel.Model.Get("legendFormat").MustString("")
start, err := queryContext.TimeRange.ParseFrom()
if err != nil {
......@@ -156,9 +157,3 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb
queryResults["A"] = queryRes
return queryResults, nil
}
/*
func resultWithError(result *tsdb.BatchResult, err error) *tsdb.BatchResult {
result.Error = err
return result
}*/
......@@ -22,5 +22,19 @@ func TestPrometheus(t *testing.T) {
So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}")
})
Convey("build full serie name", func() {
metric := map[p.LabelName]p.LabelValue{
p.LabelName(p.MetricNameLabel): p.LabelValue("http_request_total"),
p.LabelName("app"): p.LabelValue("backend"),
p.LabelName("device"): p.LabelValue("mobile"),
}
query := &PrometheusQuery{
LegendFormat: "",
}
So(formatLegend(metric, query), ShouldEqual, `http_request_total{app="backend", device="mobile"}`)
})
})
}
......@@ -107,6 +107,53 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
body.addClass(pageClass);
}
$("#tooltip, .tooltip").remove();
// check for kiosk url param
if (data.params.kiosk) {
appEvents.emit('toggle-kiosk-mode');
}
});
// handle kiosk mode
appEvents.on('toggle-kiosk-mode', () => {
body.toggleClass('page-kiosk-mode');
});
// handle in active view state class
var lastActivity = new Date().getTime();
var activeUser = true;
var inActiveTimeLimit = 60 * 1000;
function checkForInActiveUser() {
if (!activeUser) {
return;
}
// only go to activity low mode on dashboard page
if (!body.hasClass('page-dashboard')) {
return;
}
if ((new Date().getTime() - lastActivity) > inActiveTimeLimit) {
activeUser = false;
body.addClass('user-activity-low');
}
}
function userActivityDetected() {
lastActivity = new Date().getTime();
if (!activeUser) {
activeUser = true;
body.removeClass('user-activity-low');
}
}
body.mousemove(userActivityDetected);
body.keydown(userActivityDetected);
setInterval(checkForInActiveUser, 1000);
appEvents.on('toggle-view-mode', () => {
lastActivity = 0;
checkForInActiveUser();
});
// handle document clicks that should hide things
......@@ -116,6 +163,17 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
return;
}
// for stuff that animates, slides out etc, clicking it needs to
// hide it right away
var clickAutoHide = target.closest('[data-click-hide]');
if (clickAutoHide.length) {
var clickAutoHideParent = clickAutoHide.parent();
clickAutoHide.detach();
setTimeout(function() {
clickAutoHideParent.append(clickAutoHide);
}, 100);
}
if (target.parents('.dash-playlist-actions').length === 0) {
playlistSrv.stop();
}
......
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<i class="fa fa-keyboard"></i>
<span class="p-l-1">Shortcuts</span>
</h2>
<!-- <ul class="gf&#45;tabs"> -->
<!-- <li class="gf&#45;tabs&#45;item" ng&#45;repeat="tab in ['Shortcuts']"> -->
<!-- <a class="gf&#45;tabs&#45;link" ng&#45;click="ctrl.tabindex = $index" ng&#45;class="{active: ctrl.tabIndex === $index}"> -->
<!-- {{::tab}} -->
<!-- </a> -->
<!-- </li> -->
<!-- </ul> -->
<a class="modal-header-close" ng-click="ctrl.dismiss();">
<i class="fa fa-remove"></i>
</a>
</div>
<div class="modal-content help-modal">
<p class="small" style="position: absolute; top: 48px; right: 10px">
<span class="shortcut-table-key">mod</span> =
<span class="muted">CTRL on windows, CMD key on Mac</span>
</p>
<div ng-repeat="(category, shortcuts) in ctrl.shortcuts" class="shortcut-category">
<table class="shortcut-table">
<tbody>
<tr>
<th class="shortcut-table-category-header" colspan="2">{{category}}</th>
</tr>
<tr ng-repeat="shortcut in shortcuts">
<td class="shortcut-table-keys">
<span class="shortcut-table-key" ng-repeat="key in shortcut.keys track by $index" ng-bind-html="key">
</span>
</td>
<td class="shortcut-table-description">{{shortcut.description}}</td>
</tr>
<tbody>
</table>
</div>
<div class="clearfix"></div>
</div>
</div>
///<reference path="../../../headers/common.d.ts" />
import coreModule from '../../core_module';
import appEvents from 'app/core/app_events';
export class HelpCtrl {
tabIndex: any;
shortcuts: any;
/** @ngInject */
constructor(private $scope, $sce) {
this.tabIndex = 0;
this.shortcuts = {
'Global': [
{keys: ['g', 'h'], description: 'Go to Home Dashboard'},
{keys: ['g', 'p'], description: 'Go to Profile'},
{keys: ['s', 'o'], description: 'Open search'},
{keys: ['s', 's'], description: 'Open search with starred filter'},
{keys: ['s', 't'], description: 'Open search in tags view'},
{keys: ['esc'], description: 'Exit edit/setting views'},
],
'Dashboard': [
{keys: ['mod+s'], description: 'Save dashboard'},
{keys: ['mod+h'], description: 'Hide row controls'},
{keys: ['d', 'r'], description: 'Refresh all panels'},
{keys: ['d', 's'], description: 'Dashboard settings'},
{keys: ['d', 'v'], description: 'Toggle in-active / view mode'},
{keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)'},
{keys: ['mod+o'], description: 'Toggle shared graph crosshair'},
],
'Focused Panel': [
{keys: ['e'], description: 'Toggle panel edit view'},
{keys: ['v'], description: 'Toggle panel fullscreen view'},
{keys: ['p', 's'], description: 'Open Panel Share Modal'},
{keys: ['p', 'r'], description: 'Remove Panel'},
],
'Focused Row': [
{keys: ['r', 'c'], description: 'Collapse Row'},
{keys: ['r', 'r'], description: 'Remove Row'},
],
'Time Range': [
{keys: ['t', 'z'], description: 'Zoom out time range'},
{keys: ['t', '<i class="fa fa-long-arrow-left"></i>'], description: 'Move time range back'},
{keys: ['t', '<i class="fa fa-long-arrow-right"></i>'], description: 'Move time range forward'},
],
};
}
dismiss() {
appEvents.emit('hide-modal');
}
}
export function helpModal() {
return {
restrict: 'E',
templateUrl: 'public/app/core/components/help/help.html',
controller: HelpCtrl,
bindToController: true,
transclude: true,
controllerAs: 'ctrl',
scope: {},
};
}
coreModule.directive('helpModal', helpModal);
......@@ -44,8 +44,9 @@ export function infoPopover() {
}
});
scope.$on('$destroy', function() {
var unbind = scope.$on('$destroy', function() {
drop.destroy();
unbind();
});
});
......
// ///<reference path="../../headers/common.d.ts" />
//
// import _ from 'lodash';
//
// var objectAssign = require('object-assign');
// var Emitter = require('tiny-emitter');
// var Lethargy = require('lethargy').Lethargy;
// var support = require('./support');
// var clone = require('./clone');
// var bindAll = require('bindall-standalone');
// var EVT_ID = 'virtualscroll';
//
// var keyCodes = {
// LEFT: 37,
// UP: 38,
// RIGHT: 39,
// DOWN: 40
// };
//
// function VirtualScroll(this: any, options) {
// _.bindAll(this, '_onWheel', '_onMouseWheel', '_onTouchStart', '_onTouchMove', '_onKeyDown');
//
// this.el = window;
// if (options && options.el) {
// this.el = options.el;
// delete options.el;
// }
//
// this.options = _.assign({
// mouseMultiplier: 1,
// touchMultiplier: 2,
// firefoxMultiplier: 15,
// keyStep: 120,
// preventTouch: false,
// unpreventTouchClass: 'vs-touchmove-allowed',
// limitInertia: false
// }, options);
//
// if (this.options.limitInertia) this._lethargy = new Lethargy();
//
// this._emitter = new Emitter();
// this._event = {
// y: 0,
// x: 0,
// deltaX: 0,
// deltaY: 0
// };
//
// this.touchStartX = null;
// this.touchStartY = null;
// this.bodyTouchAction = null;
// }
//
// VirtualScroll.prototype._notify = function(e) {
// var evt = this._event;
// evt.x += evt.deltaX;
// evt.y += evt.deltaY;
//
// this._emitter.emit(EVT_ID, {
// x: evt.x,
// y: evt.y,
// deltaX: evt.deltaX,
// deltaY: evt.deltaY,
// originalEvent: e
// });
// };
//
// VirtualScroll.prototype._onWheel = function(e) {
// var options = this.options;
// if (this._lethargy && this._lethargy.check(e) === false) return;
//
// var evt = this._event;
//
// // In Chrome and in Firefox (at least the new one)
// evt.deltaX = e.wheelDeltaX || e.deltaX * -1;
// evt.deltaY = e.wheelDeltaY || e.deltaY * -1;
//
// // for our purpose deltamode = 1 means user is on a wheel mouse, not touch pad
// // real meaning: https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent#Delta_modes
// if(support.isFirefox && e.deltaMode == 1) {
// evt.deltaX *= options.firefoxMultiplier;
// evt.deltaY *= options.firefoxMultiplier;
// }
//
// evt.deltaX *= options.mouseMultiplier;
// evt.deltaY *= options.mouseMultiplier;
//
// this._notify(e);
// };
//
// VirtualScroll.prototype._onMouseWheel = function(e) {
// if (this.options.limitInertia && this._lethargy.check(e) === false) return;
//
// var evt = this._event;
//
// // In Safari, IE and in Chrome if 'wheel' isn't defined
// evt.deltaX = (e.wheelDeltaX) ? e.wheelDeltaX : 0;
// evt.deltaY = (e.wheelDeltaY) ? e.wheelDeltaY : e.wheelDelta;
//
// this._notify(e);
// };
//
// VirtualScroll.prototype._onTouchStart = function(e) {
// var t = (e.targetTouches) ? e.targetTouches[0] : e;
// this.touchStartX = t.pageX;
// this.touchStartY = t.pageY;
// };
//
// VirtualScroll.prototype._onTouchMove = function(e) {
// var options = this.options;
// if(options.preventTouch
// && !e.target.classList.contains(options.unpreventTouchClass)) {
// e.preventDefault();
// }
//
// var evt = this._event;
//
// var t = (e.targetTouches) ? e.targetTouches[0] : e;
//
// evt.deltaX = (t.pageX - this.touchStartX) * options.touchMultiplier;
// evt.deltaY = (t.pageY - this.touchStartY) * options.touchMultiplier;
//
// this.touchStartX = t.pageX;
// this.touchStartY = t.pageY;
//
// this._notify(e);
// };
//
// VirtualScroll.prototype._onKeyDown = function(e) {
// var evt = this._event;
// evt.deltaX = evt.deltaY = 0;
//
// switch(e.keyCode) {
// case keyCodes.LEFT:
// case keyCodes.UP:
// evt.deltaY = this.options.keyStep;
// break;
//
// case keyCodes.RIGHT:
// case keyCodes.DOWN:
// evt.deltaY = - this.options.keyStep;
// break;
//
// default:
// return;
// }
//
// this._notify(e);
// };
//
// VirtualScroll.prototype._bind = function() {
// if(support.hasWheelEvent) this.el.addEventListener('wheel', this._onWheel);
// if(support.hasMouseWheelEvent) this.el.addEventListener('mousewheel', this._onMouseWheel);
//
// if(support.hasTouch) {
// this.el.addEventListener('touchstart', this._onTouchStart);
// this.el.addEventListener('touchmove', this._onTouchMove);
// }
//
// if(support.hasPointer && support.hasTouchWin) {
// this.bodyTouchAction = document.body.style.msTouchAction;
// document.body.style.msTouchAction = 'none';
// this.el.addEventListener('MSPointerDown', this._onTouchStart, true);
// this.el.addEventListener('MSPointerMove', this._onTouchMove, true);
// }
//
// if(support.hasKeyDown) document.addEventListener('keydown', this._onKeyDown);
// };
//
// VirtualScroll.prototype._unbind = function() {
// if(support.hasWheelEvent) this.el.removeEventListener('wheel', this._onWheel);
// if(support.hasMouseWheelEvent) this.el.removeEventListener('mousewheel', this._onMouseWheel);
//
// if(support.hasTouch) {
// this.el.removeEventListener('touchstart', this._onTouchStart);
// this.el.removeEventListener('touchmove', this._onTouchMove);
// }
//
// if(support.hasPointer && support.hasTouchWin) {
// document.body.style.msTouchAction = this.bodyTouchAction;
// this.el.removeEventListener('MSPointerDown', this._onTouchStart, true);
// this.el.removeEventListener('MSPointerMove', this._onTouchMove, true);
// }
//
// if(support.hasKeyDown) document.removeEventListener('keydown', this._onKeyDown);
// };
//
// VirtualScroll.prototype.on = function(cb, ctx) {
// this._emitter.on(EVT_ID, cb, ctx);
//
// var events = this._emitter.e;
// if (events && events[EVT_ID] && events[EVT_ID].length === 1) this._bind();
// };
//
// VirtualScroll.prototype.off = function(cb, ctx) {
// this._emitter.off(EVT_ID, cb, ctx);
//
// var events = this._emitter.e;
// if (!events[EVT_ID] || events[EVT_ID].length <= 0) this._unbind();
// };
//
// VirtualScroll.prototype.reset = function() {
// var evt = this._event;
// evt.x = 0;
// evt.y = 0;
// };
//
// VirtualScroll.prototype.destroy = function() {
// this._emitter.off();
// this._unbind();
// };
......@@ -27,17 +27,13 @@
</div>
<div class="search-results-container" ng-if="ctrl.tagsMode">
<div class="row">
<div class="span6 offset1">
<div ng-repeat="tag in ctrl.results" class="pointer" style="width: 180px; float: left;"
ng-class="{'selected': $index === ctrl.selectedIndex }"
ng-click="ctrl.filterByTag(tag.term, $event)">
<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
<i class="fa fa-tag"></i>
<span>{{tag.term}} &nbsp;({{tag.count}})</span>
</a>
</div>
</div>
<div ng-repeat="tag in ctrl.results" class="pointer" style="width: 180px; float: left;"
ng-class="{'selected': $index === ctrl.selectedIndex }"
ng-click="ctrl.filterByTag(tag.term, $event)">
<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
<i class="fa fa-tag"></i>
<span>{{tag.term}} &nbsp;({{tag.count}})</span>
</a>
</div>
</div>
......
......@@ -29,7 +29,7 @@ export class SearchCtrl {
this.isOpen = this.ignoreClose;
}
openSearch() {
openSearch(evt, payload) {
if (this.isOpen) {
this.isOpen = false;
return;
......@@ -43,10 +43,21 @@ export class SearchCtrl {
this.currentSearchId = 0;
this.ignoreClose = true;
if (payload && payload.starred) {
this.query.starred = true;
}
if (payload && payload.tagsMode) {
return this.$timeout(() => {
this.ignoreClose = false;
this.giveSearchFocus = this.giveSearchFocus + 1;
this.getTags();
}, 100);
}
this.$timeout(() => {
this.ignoreClose = false;
this.giveSearchFocus = this.giveSearchFocus + 1;
this.query.query = '';
this.search();
}, 100);
}
......
......@@ -42,6 +42,10 @@ import './filters/filters';
import coreModule from './core_module';
import appEvents from './app_events';
import colors from './utils/colors';
import {assignModelProperties} from './utils/model_utils';
import {contextSrv} from './services/context_srv';
import {KeybindingSrv} from './services/keybindingSrv';
import {helpModal} from './components/help/help';
export {
......@@ -62,4 +66,8 @@ export {
queryPartEditorDirective,
WizardFlow,
colors,
assignModelProperties,
contextSrv,
KeybindingSrv,
helpModal,
};
......@@ -10,8 +10,6 @@ function (_, $, coreModule) {
return {
link: function($scope, elem) {
var lastHideControlsVal;
$scope.onAppEvent('panel-fullscreen-enter', function() {
elem.toggleClass('panel-in-fullscreen', true);
});
......@@ -20,13 +18,13 @@ function (_, $, coreModule) {
elem.toggleClass('panel-in-fullscreen', false);
});
var lastHideControlsVal;
$scope.$watch('dashboard.hideControls', function() {
if (!$scope.dashboard) {
return;
}
var hideControls = $scope.dashboard.hideControls || $scope.playlist_active;
var hideControls = $scope.dashboard.hideControls;
if (lastHideControlsVal !== hideControls) {
elem.toggleClass('hide-controls', hideControls);
lastHideControlsVal = hideControls;
......
......@@ -58,7 +58,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
var componentInfo: any = {
name: 'panel-plugin-' + scope.panel.type,
bindings: {dashboard: "=", panel: "=", row: "="},
attrs: {dashboard: "dashboard", panel: "panel", row: "row"},
attrs: {dashboard: "ctrl.dashboard", panel: "panel", row: "ctrl.row"},
};
var panelElemName = 'panel-' + scope.panel.type;
......
......@@ -60,8 +60,8 @@ coreModule.filter('noXml', function() {
coreModule.filter('interpolateTemplateVars', function (templateSrv) {
var filterFunc: any = function(text, scope) {
var scopedVars;
if (scope.ctrl && scope.ctrl.panel) {
scopedVars = scope.ctrl.panel.scopedVars;
if (scope.ctrl) {
scopedVars = (scope.ctrl.panel || scope.ctrl.row).scopedVars;
} else {
scopedVars = scope.row.scopedVars;
}
......
......@@ -28,10 +28,17 @@ function (coreModule) {
coreModule.default.controller('NewDashboardCtrl', function($scope) {
$scope.initDashboard({
meta: { canStar: false, canShare: false },
meta: { canStar: false, canShare: false, isNew: true },
dashboard: {
title: "New dashboard",
rows: [{ height: '250px', panels:[] }]
rows: [
{
title: 'Dashboard Row',
height: '250px',
panels:[],
isNew: true,
}
]
},
}, $scope);
});
......
///<reference path="../../headers/common.d.ts" />
import $ from 'jquery';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import Mousetrap from 'mousetrap';
export class KeybindingSrv {
helpModal: boolean;
/** @ngInject */
constructor(
private $rootScope,
private $modal,
private $location,
private contextSrv,
private $timeout) {
// clear out all shortcuts on route change
$rootScope.$on('$routeChangeSuccess', () => {
Mousetrap.reset();
// rebind global shortcuts
this.setupGlobal();
});
this.setupGlobal();
}
setupGlobal() {
this.bind(['?', 'h'], this.showHelpModal);
this.bind("g h", this.goToHome);
this.bind("g a", this.openAlerting);
this.bind("g p", this.goToProfile);
this.bind("s s", this.openSearchStarred);
this.bind('s o', this.openSearch);
this.bind('s t', this.openSearchTags);
this.bind('f', this.openSearch);
}
openSearchStarred() {
this.$rootScope.appEvent('show-dash-search', {starred: true});
}
openSearchTags() {
this.$rootScope.appEvent('show-dash-search', {tagsMode: true});
}
openSearch() {
this.$rootScope.appEvent('show-dash-search');
}
openAlerting() {
this.$location.url("/alerting");
}
goToHome() {
this.$location.url("/");
}
goToProfile() {
this.$location.url("/profile");
}
showHelpModal() {
appEvents.emit('show-modal', {templateHtml: '<help-modal></help-modal>'});
}
bind(keyArg, fn) {
Mousetrap.bind(keyArg, evt => {
evt.preventDefault();
evt.stopPropagation();
evt.returnValue = false;
return this.$rootScope.$apply(fn.bind(this));
});
}
showDashEditView(view) {
var search = _.extend(this.$location.search(), {editview: view});
this.$location.search(search);
}
setupDashboardBindings(scope, dashboard) {
// this.bind('b', () => {
// dashboard.toggleEditMode();
// });
this.bind('mod+o', () => {
dashboard.sharedCrosshair = !dashboard.sharedCrosshair;
scope.broadcastRefresh();
});
this.bind('mod+h', () => {
dashboard.hideControls = !dashboard.hideControls;
});
this.bind('mod+s', e => {
scope.appEvent('save-dashboard');
});
this.bind('t z', () => {
scope.appEvent('zoom-out');
});
this.bind('t left', () => {
scope.appEvent('shift-time-backward');
});
this.bind('t right', () => {
scope.appEvent('shift-time-forward');
});
this.bind('mod+i', () => {
scope.appEvent('quick-snapshot');
});
// edit panel
this.bind('e', () => {
if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) {
this.$rootScope.appEvent('panel-change-view', {
fullscreen: true,
edit: true,
panelId: dashboard.meta.focusPanelId,
toggle: true
});
}
});
// view panel
this.bind('v', () => {
if (dashboard.meta.focusPanelId) {
this.$rootScope.appEvent('panel-change-view', {
fullscreen: true,
edit: null,
panelId: dashboard.meta.focusPanelId,
toggle: true,
});
}
});
// delete panel
this.bind('p r', () => {
if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) {
var panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId);
panelInfo.row.removePanel(panelInfo.panel);
dashboard.meta.focusPanelId = 0;
}
});
// share panel
this.bind('p s', () => {
if (dashboard.meta.focusPanelId) {
var shareScope = scope.$new();
var panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId);
shareScope.panel = panelInfo.panel;
shareScope.dashboard = dashboard;
appEvents.emit('show-modal', {
src: 'public/app/features/dashboard/partials/shareModal.html',
scope: shareScope
});
}
});
// delete row
this.bind('r r', () => {
if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) {
var panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId);
dashboard.removeRow(panelInfo.row);
dashboard.meta.focusPanelId = 0;
}
});
// collapse row
this.bind('r c', () => {
if (dashboard.meta.focusPanelId) {
var panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId);
panelInfo.row.toggleCollapse();
dashboard.meta.focusPanelId = 0;
}
});
this.bind('d r', () => {
scope.broadcastRefresh();
});
this.bind('d s', () => {
this.showDashEditView('settings');
});
this.bind('d k', () => {
appEvents.emit('toggle-kiosk-mode');
});
this.bind('d v', () => {
appEvents.emit('toggle-view-mode');
});
this.bind('esc', () => {
var popups = $('.popover.in');
if (popups.length > 0) {
return;
}
// close modals
var modalData = $(".modal").data();
if (modalData && modalData.$scope && modalData.$scope.dismiss) {
modalData.$scope.dismiss();
}
scope.appEvent('hide-dash-editor');
scope.exitFullscreen();
});
}
}
coreModule.service('keybindingSrv', KeybindingSrv);
......@@ -8,6 +8,7 @@ import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
export class UtilSrv {
modalScope: any;
/** @ngInject */
constructor(private $rootScope, private $modal) {
......@@ -15,12 +16,27 @@ export class UtilSrv {
init() {
appEvents.on('show-modal', this.showModal.bind(this), this.$rootScope);
appEvents.on('hide-modal', this.hideModal.bind(this), this.$rootScope);
}
hideModal() {
if (this.modalScope && this.modalScope.dismiss) {
this.modalScope.dismiss();
}
}
showModal(options) {
if (this.modalScope && this.modalScope.dismiss) {
this.modalScope.dismiss();
}
this.modalScope = options.scope;
if (options.model) {
options.scope = this.$rootScope.$new();
options.scope.model = options.model;
this.modalScope = this.$rootScope.$new();
this.modalScope.model = options.model;
} else if (!this.modalScope) {
this.modalScope = this.$rootScope.$new();
}
var modal = this.$modal({
......@@ -29,7 +45,7 @@ export class UtilSrv {
templateHtml: options.templateHtml,
persist: false,
show: false,
scope: options.scope,
scope: this.modalScope,
keyboard: false,
backdrop: options.backdrop
});
......
......@@ -23,12 +23,17 @@ export class Emitter {
this.emitter.on(name, handler);
if (scope) {
scope.$on('$destroy', () => {
var unbind = scope.$on('$destroy', () => {
this.emitter.off(name, handler);
unbind();
});
}
}
removeAllListeners(evt?) {
this.emitter.removeAllListeners(evt);
}
off(name, handler) {
this.emitter.off(name, handler);
}
......
export function assignModelProperties(target, source, defaults) {
for (var key in defaults) {
if (!defaults.hasOwnProperty(key)) {
continue;
}
target[key] = source[key] === undefined ? defaults[key] : source[key];
}
}
......@@ -7,7 +7,7 @@
</div>
<div class="grafana-info-box span8" style="margin: 20px 0 25px 0">
These system settings are defined in grafana.ini or grafana.custom.ini (or overriden in ENV variables).
These system settings are defined in grafana.ini or custom.ini (or overriden in ENV variables).
To change these you currently need to restart grafana.
</div>
......
......@@ -34,13 +34,18 @@ var reducerTypes = [
{text: 'max()', value: 'max'},
{text: 'sum()' , value: 'sum'},
{text: 'count()', value: 'count'},
{text: 'last()', value: 'last'},
];
var noDataModes = [
{text: 'OK', value: 'ok'},
{text: 'Alerting', value: 'alerting'},
{text: 'No Data', value: 'no_data'},
{text: 'Keep Last', value: 'keep_last'},
{text: 'Keep Last State', value: 'keep_state'},
];
var executionErrorModes = [
{text: 'Alerting', value: 'alerting'},
{text: 'Keep Last State', value: 'keep_state'},
];
function createReducerPart(model) {
......@@ -48,7 +53,6 @@ function createReducerPart(model) {
return new QueryPart(model, def);
}
function getStateDisplayModel(state) {
switch (state) {
case 'ok': {
......@@ -113,6 +117,7 @@ export default {
conditionTypes: conditionTypes,
evalFunctions: evalFunctions,
noDataModes: noDataModes,
executionErrorModes: executionErrorModes,
reducerTypes: reducerTypes,
createReducerPart: createReducerPart,
joinEvalMatches: joinEvalMatches,
......
......@@ -15,7 +15,6 @@ export class AlertListCtrl {
{text: 'OK', value: 'ok'},
{text: 'Alerting', value: 'alerting'},
{text: 'No Data', value: 'no_data'},
{text: 'Execution Error', value: 'execution_error'},
];
filters = {
......
......@@ -19,6 +19,7 @@ export class AlertTabCtrl {
conditionModels: any;
evalFunctions: any;
noDataModes: any;
executionErrorModes: any;
addNotificationSegment;
notifications;
alertNotifications;
......@@ -42,6 +43,7 @@ export class AlertTabCtrl {
this.evalFunctions = alertDef.evalFunctions;
this.conditionTypes = alertDef.conditionTypes;
this.noDataModes = alertDef.noDataModes;
this.executionErrorModes = alertDef.executionErrorModes;
this.appSubUrl = config.appSubUrl;
}
......@@ -52,7 +54,7 @@ export class AlertTabCtrl {
var thresholdChangedEventHandler = this.graphThresholdChanged.bind(this);
this.panelCtrl.events.on('threshold-changed', thresholdChangedEventHandler);
// set panel alert edit mode
// set panel alert edit mode
this.$scope.$on("$destroy", () => {
this.panelCtrl.events.off("threshold-changed", thresholdChangedEventHandler);
this.panelCtrl.editingThresholds = false;
......@@ -88,6 +90,7 @@ export class AlertTabCtrl {
case "email": return "fa fa-envelope";
case "slack": return "fa fa-slack";
case "webhook": return "fa fa-cubes";
case "pagerduty": return "fa fa-bullhorn";
}
}
......@@ -140,6 +143,7 @@ export class AlertTabCtrl {
}
alert.noDataState = alert.noDataState || 'no_data';
alert.executionErrorState = alert.executionErrorState || 'alerting';
alert.frequency = alert.frequency || '60s';
alert.handler = alert.handler || 1;
alert.notifications = alert.notifications || [];
......
......@@ -8,6 +8,11 @@
<i class="fa fa-info-circle"></i>
How to add an alert
</a>
<a class="btn btn-inverse" href="alerting/notifications" >
<i class="fa fa-cog"></i>
Configure notifications
</a>
</div>
<div class="gf-form-group">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment