mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/develop' into gauge-value-options
This commit is contained in:
commit
f77c354341
@ -1,7 +1,7 @@
|
||||
[run]
|
||||
init_cmds = [
|
||||
["go", "run", "build.go", "-dev", "build-server"],
|
||||
["./bin/grafana-server", "cfg:app_mode=development"]
|
||||
["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"]
|
||||
]
|
||||
watch_all = true
|
||||
follow_symlinks = true
|
||||
@ -14,5 +14,5 @@ watch_exts = [".go", ".ini", ".toml", ".template.html"]
|
||||
build_delay = 1500
|
||||
cmds = [
|
||||
["go", "run", "build.go", "-dev", "build-server"],
|
||||
["./bin/grafana-server", "cfg:app_mode=development"]
|
||||
["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"]
|
||||
]
|
||||
|
@ -127,7 +127,7 @@ jobs:
|
||||
|
||||
build-all:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.0
|
||||
- image: grafana/build-container:1.2.1
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -175,7 +175,7 @@ jobs:
|
||||
|
||||
build:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.0
|
||||
- image: grafana/build-container:1.2.1
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -241,7 +241,7 @@ jobs:
|
||||
|
||||
build-enterprise:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.0
|
||||
- image: grafana/build-container:1.2.1
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -273,7 +273,7 @@ jobs:
|
||||
|
||||
build-all-enterprise:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.0
|
||||
- image: grafana/build-container:1.2.1
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -359,6 +359,9 @@ jobs:
|
||||
- run:
|
||||
name: deploy to gcp
|
||||
command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/release'
|
||||
- run:
|
||||
name: Deploy to Grafana.com
|
||||
command: './scripts/build/publish.sh --enterprise'
|
||||
|
||||
deploy-master:
|
||||
docker:
|
||||
@ -507,6 +510,7 @@ workflows:
|
||||
- grafana-docker-release:
|
||||
requires:
|
||||
- build-all
|
||||
- build-all-enterprise
|
||||
- test-backend
|
||||
- test-frontend
|
||||
- codespell
|
||||
|
27
CHANGELOG.md
27
CHANGELOG.md
@ -1,7 +1,12 @@
|
||||
# 5.4.0 (unreleased)
|
||||
|
||||
* **Cloudwatch**: Fix invalid time range causes segmentation fault [#14150](https://github.com/grafana/grafana/issues/14150)
|
||||
|
||||
# 5.4.0-beta1 (2018-11-20)
|
||||
|
||||
### New Features
|
||||
|
||||
* **Alerting**: Introduce alert debouncing with the `FOR` setting. [#7886](https://github.com/grafana/grafana/issues/7886) & [#6202](https://github.com/grafana/grafana/issues/6202)
|
||||
* **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat)
|
||||
* **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
|
||||
* **MySQL**: Graphical query builder [#13762](https://github.com/grafana/grafana/issues/13762), thx [svenklemm](https://github.com/svenklemm)
|
||||
@ -9,29 +14,41 @@
|
||||
* **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
|
||||
* **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
|
||||
* **Teams**: Team preferences (theme, home dashboard, timezone) support [#12550](https://github.com/grafana/grafana/issues/12550)
|
||||
* **Graph**: Time regions support enabling highlight of weekdays and/or certain timespans [#5930](https://github.com/grafana/grafana/issues/5930)
|
||||
* **OAuth**: Automatic redirect to sign-in with OAuth [#11893](https://github.com/grafana/grafana/issues/11893), thx [@Nick-Triller](https://github.com/Nick-Triller)
|
||||
* **Stackdriver**: Template query editor [#13561](https://github.com/grafana/grafana/issues/13561)
|
||||
|
||||
### Minor
|
||||
|
||||
* **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043)
|
||||
* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy)
|
||||
* **Cloudwatch**: CloudHSM metrics and dimensions [#14129](https://github.com/grafana/grafana/pull/14129), thx [@daktari](https://github.com/daktari)
|
||||
* **Cloudwatch**: Enable using variables in the stats field [#13810](https://github.com/grafana/grafana/issues/13810), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
|
||||
* **Elasticsearch**: Fix switching to/from es raw document metric query [#6367](https://github.com/grafana/grafana/issues/6367)
|
||||
* **Elasticsearch**: Fix deprecation warning about terms aggregation order key in Elasticsearch 6.x [#11977](https://github.com/grafana/grafana/issues/11977)
|
||||
* **Graph**: Render dots when no connecting line can be made [#13605](https://github.com/grafana/grafana/issues/13605), thx [@jsferrei](https://github.com/jsferrei)
|
||||
* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme)
|
||||
* **Singlestat**: Fix XSS in prefix/postfix [#13946](https://github.com/grafana/grafana/issues/13946), thx [@cinaglia](https://github.com/cinaglia)
|
||||
* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
|
||||
* **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
|
||||
* **Alerting**: More options for the Slack Alert notifier [#13993](https://github.com/grafana/grafana/issues/13993), thx [@andreykaipov](https://github.com/andreykaipov)
|
||||
* **Alerting**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
|
||||
* **Alerting**: Increase Telegram captions length limit [#13876](https://github.com/grafana/grafana/pull/13876), thx [@skgsergio](https://github.com/skgsergio)
|
||||
* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
|
||||
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
|
||||
* **OAuth**: Fix Google OAuth relies on email, not google account id [#13924](https://github.com/grafana/grafana/issues/13924), thx [@vinicyusmacedo](https://github.com/vinicyusmacedo)
|
||||
* **Dashboard**: Toggle legend using keyboard shortcut [#13655](https://github.com/grafana/grafana/issues/13655), thx [@davewat](https://github.com/davewat)
|
||||
* **Dashboard**: Fix render dashboard row drag handle only in edit mode [#13555](https://github.com/grafana/grafana/issues/13555), thx [@praveensastry](https://github.com/praveensastry)
|
||||
* **Teams**: Fix cannot select team if not included in initial search [#13425](https://github.com/grafana/grafana/issues/13425)
|
||||
* **Render**: Support full height screenshots using phantomjs render script [#13352](https://github.com/grafana/grafana/pull/13352), thx [@amuraru](https://github.com/amuraru)
|
||||
* **HTTP API**: Support retrieving teams by user [#14120](https://github.com/grafana/grafana/pull/14120), thx [@supercharlesliu](https://github.com/supercharlesliu)
|
||||
* **Metrics**: Add basic authentication to metrics endpoint [#13577](https://github.com/grafana/grafana/issues/13577), thx [@bobmshannon](https://github.com/bobmshannon)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
|
||||
|
||||
# 5.3.5 (unreleased)
|
||||
|
||||
* **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043)
|
||||
* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited).
|
||||
|
||||
# 5.3.4 (2018-11-13)
|
||||
|
||||
|
2
Makefile
2
Makefile
@ -25,7 +25,7 @@ build: build-go build-js
|
||||
|
||||
build-docker-dev:
|
||||
@echo "\033[92mInfo:\033[0m the frontend code is expected to be built already."
|
||||
go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest
|
||||
go run build.go -goos linux -pkg-arch amd64 ${OPT} build pkg-archive latest
|
||||
cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
||||
cd packaging/docker && docker build --tag grafana/grafana:dev .
|
||||
|
||||
|
2
build.go
2
build.go
@ -128,6 +128,8 @@ func main() {
|
||||
if goos == linux {
|
||||
createLinuxPackages()
|
||||
}
|
||||
case "pkg-archive":
|
||||
grunt(gruntBuildArg("package")...)
|
||||
|
||||
case "pkg-rpm":
|
||||
grunt(gruntBuildArg("release")...)
|
||||
|
@ -490,6 +490,10 @@ enabled = false
|
||||
enabled = true
|
||||
interval_seconds = 10
|
||||
|
||||
#If both are set, basic auth will be required for the metrics endpoint.
|
||||
basic_auth_username =
|
||||
basic_auth_password =
|
||||
|
||||
# Send internal Grafana metrics to graphite
|
||||
[metrics.graphite]
|
||||
# Enable by setting the address setting (ex localhost:2003)
|
||||
|
511
devenv/dev-dashboards/panel_tests_graph_time_regions.json
Normal file
511
devenv/dev-dashboards/panel_tests_graph_time_regions.json
Normal file
@ -0,0 +1,511 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"fill": 2,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [
|
||||
{
|
||||
"colorMode": "gray",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(255, 255, 255, 0.03)",
|
||||
"from": "08:30",
|
||||
"fromDayOfWeek": 1,
|
||||
"line": false,
|
||||
"lineColor": "rgba(255, 255, 255, 0.2)",
|
||||
"op": "time",
|
||||
"to": "16:45",
|
||||
"toDayOfWeek": 5
|
||||
}
|
||||
],
|
||||
"timeShift": null,
|
||||
"title": "Business Hours",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"fill": 2,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 4,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [
|
||||
{
|
||||
"colorMode": "red",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(255, 255, 255, 0.03)",
|
||||
"from": "20:00",
|
||||
"fromDayOfWeek": 7,
|
||||
"line": false,
|
||||
"lineColor": "rgba(255, 255, 255, 0.2)",
|
||||
"op": "time",
|
||||
"to": "23:00",
|
||||
"toDayOfWeek": 7
|
||||
}
|
||||
],
|
||||
"timeShift": null,
|
||||
"title": "Sunday's 20-23",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {
|
||||
"A-series": "#d683ce"
|
||||
},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"fill": 2,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"id": 3,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 0.5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(255, 0, 0, 0.22)",
|
||||
"from": "",
|
||||
"fromDayOfWeek": 1,
|
||||
"line": true,
|
||||
"lineColor": "rgba(255, 0, 0, 0.32)",
|
||||
"op": "time",
|
||||
"to": "",
|
||||
"toDayOfWeek": 1
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(255, 127, 0, 0.22)",
|
||||
"fromDayOfWeek": 2,
|
||||
"line": true,
|
||||
"lineColor": "rgba(255, 127, 0, 0.32)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 2
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(255, 255, 0, 0.22)",
|
||||
"fromDayOfWeek": 3,
|
||||
"line": true,
|
||||
"lineColor": "rgba(255, 255, 0, 0.22)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 3
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(0, 255, 0, 0.22)",
|
||||
"fromDayOfWeek": 4,
|
||||
"line": true,
|
||||
"lineColor": "rgba(0, 255, 0, 0.32)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 4
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(0, 0, 255, 0.22)",
|
||||
"fromDayOfWeek": 5,
|
||||
"line": true,
|
||||
"lineColor": "rgba(0, 0, 255, 0.32)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 5
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(75, 0, 130, 0.22)",
|
||||
"fromDayOfWeek": 6,
|
||||
"line": true,
|
||||
"lineColor": "rgba(75, 0, 130, 0.32)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 6
|
||||
},
|
||||
{
|
||||
"colorMode": "custom",
|
||||
"fill": true,
|
||||
"fillColor": "rgba(148, 0, 211, 0.22)",
|
||||
"fromDayOfWeek": 7,
|
||||
"line": true,
|
||||
"lineColor": "rgba(148, 0, 211, 0.32)",
|
||||
"op": "time",
|
||||
"toDayOfWeek": 7
|
||||
}
|
||||
],
|
||||
"timeShift": null,
|
||||
"title": "Each day of week",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"fill": 2,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 24
|
||||
},
|
||||
"id": 5,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [
|
||||
{
|
||||
"colorMode": "red",
|
||||
"fill": false,
|
||||
"from": "05:00",
|
||||
"line": true,
|
||||
"op": "time"
|
||||
}
|
||||
],
|
||||
"timeShift": null,
|
||||
"title": "05:00",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-30d",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "Panel Tests - Graph (Time Regions)",
|
||||
"uid": "XMjIZPmik",
|
||||
"version": 1
|
||||
}
|
@ -1,250 +1,681 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
60
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always OK",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 60
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Always OK",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": "125",
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
177
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"executionErrorState": "alerting",
|
||||
"for": "0m",
|
||||
"frequency": "60s",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always Alerting",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 4,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "200,445,100,150,200,220,190",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 177
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Always Alerting",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
1
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"operator": {
|
||||
"type": "and"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"15m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"executionErrorState": "alerting",
|
||||
"for": "5m",
|
||||
"frequency": "1m",
|
||||
"handler": 1,
|
||||
"name": "TestData - No data",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 5,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "no_data_points",
|
||||
"stringInput": "",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "No data",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
177
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"operator": {
|
||||
"type": "and"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"15m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"executionErrorState": "alerting",
|
||||
"for": "1m",
|
||||
"frequency": "1m",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always Pending",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 7
|
||||
},
|
||||
"id": 6,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "200,445,100,150,200,220,190",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 177
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Always Alerting with For",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
100
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"operator": {
|
||||
"type": "and"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"executionErrorState": "alerting",
|
||||
"for": "900000h",
|
||||
"frequency": "1m",
|
||||
"handler": 1,
|
||||
"name": "Always Pending",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 14
|
||||
},
|
||||
"id": 7,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "200,445,100,150,200,220,190",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 100
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Always Alerting with For",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"revision": 2,
|
||||
"title": "Alerting with TestData",
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"grafana-test"
|
||||
],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"hideControls": false,
|
||||
"sharedCrosshair": false,
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": 255.625,
|
||||
"panels": [
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
60
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always OK",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 3,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 6,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"value": 60,
|
||||
"op": "gt",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"colorMode": "critical"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Always OK",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": "125",
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
177
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"handler": 1,
|
||||
"name": "TestData - Always Alerting",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"id": 4,
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 6,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenario": "random_walk",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "200,445,100,150,200,220,190",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 177
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Always Alerting",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": "",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "New row"
|
||||
}
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
@ -274,14 +705,8 @@
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"schemaVersion": 13,
|
||||
"version": 4,
|
||||
"links": [],
|
||||
"gnetId": null
|
||||
}
|
||||
"timezone": "browser",
|
||||
"title": "Alerting with TestData",
|
||||
"uid": "7MeksYbmk",
|
||||
"version": 7
|
||||
}
|
@ -9,7 +9,7 @@ services:
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
|
||||
db:
|
||||
image: mysql
|
||||
image: mysql:5.6
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_DATABASE: grafana
|
||||
|
@ -39,6 +39,7 @@ local alertDashboardTemplate = {
|
||||
"executionErrorState": "alerting",
|
||||
"frequency": "10s",
|
||||
"handler": 1,
|
||||
"for": "1m",
|
||||
"name": "bulk alerting",
|
||||
"noDataState": "no_data",
|
||||
"notifications": [
|
||||
|
@ -39,7 +39,7 @@ Currently alerting supports a limited form of high availability. Since v4.2.0 of
|
||||
|
||||
## 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.
|
||||
@ -48,6 +48,19 @@ panels as well in a future release.
|
||||
|
||||
Here you can specify the name of the alert rule and how often the scheduler should evaluate the alert rule.
|
||||
|
||||
### For
|
||||
|
||||
> This setting is available in Grafana 5.4 and above.
|
||||
|
||||
If an alert rule has a configured `For` and the query violates the configured threshold it will first go from `OK` to `Pending`. Going from `OK` to `Pending` Grafana will not send any notifications. Once the alert rule has been firing for more than `For` duration, it will change to `Alerting` and send alert notifications.
|
||||
|
||||
Typically, it's always a good idea to use this setting since it's often worse to get false positive than wait a few minutes before the alert notification triggers. Looking at the `Alert list` or `Alert list panels` you will be able to see alerts in pending state.
|
||||
|
||||
Below you can see an example timeline of an alert using the `For` setting. At ~16:04 the alert state changes to `Pending` and after 4 minutes it changes to `Alerting` which is when alert notifications are sent. Once the series falls back to normal the alert rule goes back to `OK`.
|
||||
{{< imgbox img="/img/docs/v54/alerting-for-dark-theme.png" caption="Alerting For" >}}
|
||||
|
||||
{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}}
|
||||
|
||||
### Conditions
|
||||
|
||||
Currently the only condition type that exists is a `Query` condition that allows you to
|
||||
@ -57,11 +70,11 @@ specify a query letter, time range and an aggregation function.
|
||||
### Query condition example
|
||||
|
||||
```sql
|
||||
avg() OF query(A, 5m, now) IS BELOW 14
|
||||
avg() OF query(A, 15m, now) IS BELOW 14
|
||||
```
|
||||
|
||||
- `avg()` Controls how the values for **each** series should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function.
|
||||
- `query(A, 5m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `5m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data.
|
||||
- `query(A, 15m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `15m, now` means 15 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data.
|
||||
- `IS BELOW 14` Defines the type of threshold and the threshold value. You can click on `IS BELOW` to change the type of threshold.
|
||||
|
||||
The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
|
||||
|
@ -73,7 +73,18 @@ You can hide the Grafana login form using the below configuration settings.
|
||||
|
||||
```bash
|
||||
[auth]
|
||||
disable_login_form ⁼ true
|
||||
disable_login_form = true
|
||||
```
|
||||
|
||||
### Automatic OAuth login
|
||||
|
||||
Set to true to attempt login with OAuth automatically, skipping the login screen.
|
||||
This setting is ignored if multiple OAuth providers are configured.
|
||||
Defaults to `false`.
|
||||
|
||||
```bash
|
||||
[auth]
|
||||
oauth_auto_login = true
|
||||
```
|
||||
|
||||
### Hide sign-out menu
|
||||
@ -84,3 +95,12 @@ Set to the option detailed below to true to hide sign-out menu link. Useful if y
|
||||
[auth]
|
||||
disable_signout_menu = true
|
||||
```
|
||||
|
||||
### URL redirect after signing out
|
||||
|
||||
URL to redirect the user to after signing out from Grafana. This can for example be used to enable signout from oauth provider.
|
||||
|
||||
```bash
|
||||
[auth]
|
||||
signout_redirect_url =
|
||||
```
|
||||
|
@ -158,9 +158,9 @@ Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod`
|
||||
|
||||
It is also possible to resolve the name of the Monitored Resource Type.
|
||||
|
||||
| Alias Pattern Format | Description | Example Result |
|
||||
| ------------------------ | ------------------------------------------------| ---------------- |
|
||||
| `{{resource.type}}` | returns the name of the monitored resource type | `gce_instance` |
|
||||
| Alias Pattern Format | Description | Example Result |
|
||||
| -------------------- | ----------------------------------------------- | -------------- |
|
||||
| `{{resource.type}}` | returns the name of the monitored resource type | `gce_instance` |
|
||||
|
||||
Example Alias By: `{{resource.type}} - {{metric.type}}`
|
||||
|
||||
@ -177,7 +177,17 @@ types of template variables.
|
||||
|
||||
### Query Variable
|
||||
|
||||
Writing variable queries is not supported yet.
|
||||
Variable of the type *Query* allows you to query Stackdriver for various types of data. The Stackdriver data source plugin provides the following `Query Types`.
|
||||
|
||||
| Name | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| *Metric Types* | Returns a list of metric type names that are available for the specified service. |
|
||||
| *Labels Keys* | Returns a list of keys for `metric label` and `resource label` in the specified metric. |
|
||||
| *Labels Values* | Returns a list of values for the label in the specified metric. |
|
||||
| *Resource Types* | Returns a list of resource types for the the specified metric. |
|
||||
| *Aggregations* | Returns a list of aggregations (cross series reducers) for the the specified metric. |
|
||||
| *Aligners* | Returns a list of aligners (per series aligners) for the the specified metric. |
|
||||
| *Alignment periods* | Returns a list of all alignment periods that are available in Stackdriver query editor in Grafana |
|
||||
|
||||
### Using variables in queries
|
||||
|
||||
|
@ -186,6 +186,14 @@ There is an option under Series overrides to draw lines as dashes. Set Dashes to
|
||||
Thresholds allow you to add arbitrary lines or sections to the graph to make it easier to see when
|
||||
the graph crosses a particular threshold.
|
||||
|
||||
### Time Regions
|
||||
|
||||
> Only available in Grafana v5.4 and above.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v54/graph_time_regions.png" max-width= "800px" >}}
|
||||
|
||||
Time regions allow you to highlight certain time regions of the graph to make it easier to see for example weekends, business hours and/or off work hours.
|
||||
|
||||
## Time Range
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v51/graph-time-range.png" max-width= "900px" >}}
|
||||
|
18
docs/sources/guides/whats-new-in-v5-4.md
Normal file
18
docs/sources/guides/whats-new-in-v5-4.md
Normal file
@ -0,0 +1,18 @@
|
||||
+++
|
||||
title = "What's New in Grafana v5.4"
|
||||
description = "Feature & improvement highlights for Grafana v5.4"
|
||||
keywords = ["grafana", "new", "documentation", "5.4"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Version 5.4"
|
||||
identifier = "v5.4"
|
||||
parent = "whatsnew"
|
||||
weight = -10
|
||||
+++
|
||||
|
||||
# What's New in Grafana v5.4
|
||||
|
||||
## 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.
|
@ -226,6 +226,40 @@ Content-Type: application/json
|
||||
]
|
||||
```
|
||||
|
||||
## Get Teams for user
|
||||
|
||||
`GET /api/users/:id/teams`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/users/1/teams HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"id":1,
|
||||
"orgId":1,
|
||||
"name":"team1",
|
||||
"email":"",
|
||||
"avatarUrl":"/avatar/3fcfe295eae3bcb67a49349377428a66",
|
||||
"memberCount":1
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## User
|
||||
|
||||
## Actual User
|
||||
|
@ -454,6 +454,12 @@ Ex `filters = sqlstore:debug`
|
||||
### enabled
|
||||
Enable metrics reporting. defaults true. Available via HTTP API `/metrics`.
|
||||
|
||||
### basic_auth_username
|
||||
If set configures the username to use for basic authentication on the metrics endpoint.
|
||||
|
||||
### basic_auth_password
|
||||
If set configures the password to use for basic authentication on the metrics endpoint.
|
||||
|
||||
### interval_seconds
|
||||
|
||||
Flush/Write interval when sending metrics to external TSDB. Defaults to 10s.
|
||||
|
@ -56,7 +56,7 @@ if [ -f "$DEFAULT" ]; then
|
||||
. "$DEFAULT"
|
||||
fi
|
||||
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=deb cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
|
||||
function checkUser() {
|
||||
if [ `id -u` -ne 0 ]; then
|
||||
|
@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||
--packaging=deb \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR} \
|
||||
|
@ -1,9 +1,17 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
_grafana_tag=$1
|
||||
_raw_grafana_tag=$1
|
||||
_docker_repo=${2:-grafana/grafana-enterprise}
|
||||
|
||||
if echo "$_raw_grafana_tag" | grep -q "^v"; then
|
||||
_grafana_tag=$(echo "${_raw_grafana_tag}" | cut -d "v" -f 2)
|
||||
else
|
||||
_grafana_tag="${_raw_grafana_tag}"
|
||||
fi
|
||||
|
||||
echo "Building and deploying ${_docker_repo}:${_grafana_tag}"
|
||||
|
||||
docker build \
|
||||
--tag "${_docker_repo}:${_grafana_tag}"\
|
||||
--no-cache=true \
|
||||
|
@ -80,6 +80,7 @@ fi
|
||||
exec grafana-server \
|
||||
--homepath="$GF_PATHS_HOME" \
|
||||
--config="$GF_PATHS_CONFIG" \
|
||||
--packaging=docker \
|
||||
"$@" \
|
||||
cfg:default.log.mode="console" \
|
||||
cfg:default.paths.data="$GF_PATHS_DATA" \
|
||||
|
@ -60,7 +60,7 @@ fi
|
||||
# overwrite settings from default file
|
||||
[ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME
|
||||
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=rpm cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||
|
||||
function isRunning() {
|
||||
status -p $PID_FILE $NAME > /dev/null 2>&1
|
||||
|
@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||
--packaging=rpm \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR} \
|
||||
|
@ -295,7 +295,7 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
|
||||
return Error(500, "", err)
|
||||
}
|
||||
|
||||
var response m.AlertStateType = m.AlertStatePending
|
||||
var response m.AlertStateType = m.AlertStateUnknown
|
||||
pausedState := "un-paused"
|
||||
if cmd.Paused {
|
||||
response = m.AlertStatePaused
|
||||
|
@ -140,6 +140,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
usersRoute.Get("/", Wrap(SearchUsers))
|
||||
usersRoute.Get("/search", Wrap(SearchUsersWithPaging))
|
||||
usersRoute.Get("/:id", Wrap(GetUserByID))
|
||||
usersRoute.Get("/:id/teams", Wrap(GetUserTeams))
|
||||
usersRoute.Get("/:id/orgs", Wrap(GetUserOrgList))
|
||||
// query parameters /users/lookup?loginOrEmail=admin@example.com
|
||||
usersRoute.Get("/lookup", Wrap(GetUserByLoginOrEmail))
|
||||
|
19
pkg/api/basic_auth.go
Normal file
19
pkg/api/basic_auth.go
Normal file
@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
// BasicAuthenticatedRequest parses the provided HTTP request for basic authentication credentials
|
||||
// and returns true if the provided credentials match the expected username and password.
|
||||
// Returns false if the request is unauthenticated.
|
||||
// Uses constant-time comparison in order to mitigate timing attacks.
|
||||
func BasicAuthenticatedRequest(req macaron.Request, expectedUser, expectedPass string) bool {
|
||||
user, pass, ok := req.BasicAuth()
|
||||
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(expectedUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass)) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
45
pkg/api/basic_auth_test.go
Normal file
45
pkg/api/basic_auth_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func TestBasicAuthenticatedRequest(t *testing.T) {
|
||||
expectedUser := "prometheus"
|
||||
expectedPass := "password"
|
||||
|
||||
Convey("Given a valid set of basic auth credentials", t, func() {
|
||||
httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
|
||||
So(err, ShouldBeNil)
|
||||
req := macaron.Request{
|
||||
Request: httpReq,
|
||||
}
|
||||
encodedCreds := encodeBasicAuthCredentials(expectedUser, expectedPass)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds))
|
||||
authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass)
|
||||
So(authenticated, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Given an invalid set of basic auth credentials", t, func() {
|
||||
httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
|
||||
So(err, ShouldBeNil)
|
||||
req := macaron.Request{
|
||||
Request: httpReq,
|
||||
}
|
||||
encodedCreds := encodeBasicAuthCredentials("invaliduser", "invalidpass")
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds))
|
||||
authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass)
|
||||
So(authenticated, ShouldBeFalse)
|
||||
})
|
||||
}
|
||||
|
||||
func encodeBasicAuthCredentials(user, pass string) string {
|
||||
creds := fmt.Sprintf("%s:%s", user, pass)
|
||||
return base64.StdEncoding.EncodeToString([]byte(creds))
|
||||
}
|
@ -245,6 +245,11 @@ func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if hs.metricsEndpointBasicAuthEnabled() && !BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) {
|
||||
ctx.Resp.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).
|
||||
ServeHTTP(ctx.Resp, ctx.Req.Request)
|
||||
}
|
||||
@ -299,3 +304,7 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) metricsEndpointBasicAuthEnabled() bool {
|
||||
return hs.Cfg.MetricsEndpointBasicAuthUsername != "" && hs.Cfg.MetricsEndpointBasicAuthPassword != ""
|
||||
}
|
||||
|
30
pkg/api/http_server_test.go
Normal file
30
pkg/api/http_server_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestHTTPServer(t *testing.T) {
|
||||
Convey("Given a HTTPServer", t, func() {
|
||||
ts := &HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
|
||||
Convey("Given that basic auth on the metrics endpoint is enabled", func() {
|
||||
ts.Cfg.MetricsEndpointBasicAuthUsername = "foo"
|
||||
ts.Cfg.MetricsEndpointBasicAuthPassword = "bar"
|
||||
|
||||
So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Given that basic auth on the metrics endpoint is disabled", func() {
|
||||
ts.Cfg.MetricsEndpointBasicAuthUsername = ""
|
||||
ts.Cfg.MetricsEndpointBasicAuthPassword = ""
|
||||
|
||||
So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
}
|
@ -39,6 +39,10 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
|
||||
viewData.Settings["loginError"] = loginError
|
||||
}
|
||||
|
||||
if tryOAuthAutoLogin(c) {
|
||||
return
|
||||
}
|
||||
|
||||
if !tryLoginUsingRememberCookie(c) {
|
||||
c.HTML(200, ViewIndex, viewData)
|
||||
return
|
||||
@ -53,6 +57,24 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
|
||||
c.Redirect(setting.AppSubUrl + "/")
|
||||
}
|
||||
|
||||
func tryOAuthAutoLogin(c *m.ReqContext) bool {
|
||||
if !setting.OAuthAutoLogin {
|
||||
return false
|
||||
}
|
||||
oauthInfos := setting.OAuthService.OAuthInfos
|
||||
if len(oauthInfos) != 1 {
|
||||
log.Warn("Skipping OAuth auto login because multiple OAuth providers are configured.")
|
||||
return false
|
||||
}
|
||||
for key := range setting.OAuthService.OAuthInfos {
|
||||
redirectUrl := setting.AppSubUrl + "/login/" + key
|
||||
log.Info("OAuth auto login enabled. Redirecting to " + redirectUrl)
|
||||
c.Redirect(redirectUrl, 307)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
|
||||
// Check auto-login.
|
||||
uname := c.GetCookie(setting.CookieUserName)
|
||||
|
@ -113,7 +113,16 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response {
|
||||
|
||||
// GET /api/user/teams
|
||||
func GetSignedInUserTeamList(c *m.ReqContext) Response {
|
||||
query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId}
|
||||
return getUserTeamList(c.OrgId, c.UserId)
|
||||
}
|
||||
|
||||
// GET /api/users/:id/teams
|
||||
func GetUserTeams(c *m.ReqContext) Response {
|
||||
return getUserTeamList(c.OrgId, c.ParamsInt64(":id"))
|
||||
}
|
||||
|
||||
func getUserTeamList(userID int64, orgID int64) Response {
|
||||
query := m.GetTeamsByUserQuery{OrgId: orgID, UserId: userID}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return Error(500, "Failed to get user teams", err)
|
||||
@ -122,11 +131,10 @@ func GetSignedInUserTeamList(c *m.ReqContext) Response {
|
||||
for _, team := range query.Result {
|
||||
team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
|
||||
}
|
||||
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
// GET /api/user/:id/orgs
|
||||
// GET /api/users/:id/orgs
|
||||
func GetUserOrgList(c *m.ReqContext) Response {
|
||||
return getUserOrgList(c.ParamsInt64(":id"))
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
extensions "github.com/grafana/grafana/pkg/extensions"
|
||||
"github.com/grafana/grafana/pkg/extensions"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
||||
@ -39,6 +39,7 @@ var buildstamp string
|
||||
var configFile = flag.String("config", "", "path to config file")
|
||||
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
|
||||
var pidFile = flag.String("pidfile", "", "path to pid file")
|
||||
var packaging = flag.String("packaging", "unknown", "describes the way Grafana was installed")
|
||||
|
||||
func main() {
|
||||
v := flag.Bool("v", false, "prints current version and exits")
|
||||
@ -53,7 +54,10 @@ func main() {
|
||||
if *profile {
|
||||
runtime.SetBlockProfileRate(1)
|
||||
go func() {
|
||||
http.ListenAndServe(fmt.Sprintf("localhost:%d", *profilePort), nil)
|
||||
err := http.ListenAndServe(fmt.Sprintf("localhost:%d", *profilePort), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
f, err := os.Create("trace.out")
|
||||
@ -79,6 +83,7 @@ func main() {
|
||||
setting.BuildStamp = buildstampInt64
|
||||
setting.BuildBranch = buildBranch
|
||||
setting.IsEnterprise = extensions.IsEnterprise
|
||||
setting.Packaging = validPackaging(*packaging)
|
||||
|
||||
metrics.SetBuildInformation(version, commit, buildBranch)
|
||||
|
||||
@ -95,6 +100,16 @@ func main() {
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func validPackaging(packaging string) string {
|
||||
validTypes := []string{"dev", "deb", "rpm", "docker", "brew", "hosted", "unknown"}
|
||||
for _, vt := range validTypes {
|
||||
if packaging == vt {
|
||||
return packaging
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func listenToSystemSignals(server *GrafanaServerImpl) {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
sighupChan := make(chan os.Signal, 1)
|
||||
|
@ -67,6 +67,7 @@ type GrafanaServerImpl struct {
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Run() error {
|
||||
var err error
|
||||
g.loadConfiguration()
|
||||
g.writePIDFile()
|
||||
|
||||
@ -74,20 +75,38 @@ func (g *GrafanaServerImpl) Run() error {
|
||||
social.NewOAuthService()
|
||||
|
||||
serviceGraph := inject.Graph{}
|
||||
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
|
||||
serviceGraph.Provide(&inject.Object{Value: g.cfg})
|
||||
serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
|
||||
serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
|
||||
err = serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to provide object to the graph: %v", err)
|
||||
}
|
||||
err = serviceGraph.Provide(&inject.Object{Value: g.cfg})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to provide object to the graph: %v", err)
|
||||
}
|
||||
err = serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to provide object to the graph: %v", err)
|
||||
}
|
||||
err = serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to provide object to the graph: %v", err)
|
||||
}
|
||||
|
||||
// self registered services
|
||||
services := registry.GetServices()
|
||||
|
||||
// Add all services to dependency graph
|
||||
for _, service := range services {
|
||||
serviceGraph.Provide(&inject.Object{Value: service.Instance})
|
||||
err = serviceGraph.Provide(&inject.Object{Value: service.Instance})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to provide object to the graph: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
serviceGraph.Provide(&inject.Object{Value: g})
|
||||
err = serviceGraph.Provide(&inject.Object{Value: g})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to provide object to the graph: %v", err)
|
||||
}
|
||||
|
||||
// Inject dependencies to services
|
||||
if err := serviceGraph.Populate(); err != nil {
|
||||
@ -144,6 +163,7 @@ func (g *GrafanaServerImpl) Run() error {
|
||||
}
|
||||
|
||||
sendSystemdNotification("READY=1")
|
||||
|
||||
return g.childRoutines.Wait()
|
||||
}
|
||||
|
||||
|
@ -313,7 +313,7 @@ func init() {
|
||||
|
||||
// SetBuildInformation sets the build information for this binary
|
||||
func SetBuildInformation(version, revision, branch string) {
|
||||
// We export this info twice for backwards compability.
|
||||
// We export this info twice for backwards compatibility.
|
||||
// Once this have been released for some time we should be able to remote `M_Grafana_Version`
|
||||
// The reason we added a new one is that its common practice in the prometheus community
|
||||
// to name this metric `*_build_info` so its easy to do aggregation on all programs.
|
||||
@ -397,11 +397,12 @@ func sendUsageStats(oauthProviders map[string]bool) {
|
||||
|
||||
metrics := map[string]interface{}{}
|
||||
report := map[string]interface{}{
|
||||
"version": version,
|
||||
"metrics": metrics,
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"edition": getEdition(),
|
||||
"version": version,
|
||||
"metrics": metrics,
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"edition": getEdition(),
|
||||
"packaging": setting.Packaging,
|
||||
}
|
||||
|
||||
statsQuery := models.GetSystemStatsQuery{}
|
||||
@ -447,6 +448,8 @@ func sendUsageStats(oauthProviders map[string]bool) {
|
||||
}
|
||||
metrics["stats.ds.other.count"] = dsOtherCount
|
||||
|
||||
metrics["stats.packaging."+setting.Packaging+".count"] = 1
|
||||
|
||||
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
|
||||
if err := bus.Dispatch(&dsAccessStats); err != nil {
|
||||
metricsLogger.Error("Failed to get datasource access stats", "error", err)
|
||||
|
@ -176,6 +176,7 @@ func TestMetrics(t *testing.T) {
|
||||
setting.BasicAuthEnabled = true
|
||||
setting.LdapEnabled = true
|
||||
setting.AuthProxyEnabled = true
|
||||
setting.Packaging = "deb"
|
||||
|
||||
wg.Add(1)
|
||||
sendUsageStats(oauthProviders)
|
||||
@ -243,6 +244,8 @@ func TestMetrics(t *testing.T) {
|
||||
So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1)
|
||||
So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1)
|
||||
So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
|
||||
|
||||
So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -115,6 +115,7 @@ func Recovery() macaron.Handler {
|
||||
|
||||
c.Data["Title"] = "Server Error"
|
||||
c.Data["AppSubUrl"] = setting.AppSubUrl
|
||||
c.Data["Theme"] = setting.DefaultTheme
|
||||
|
||||
if setting.Env == setting.DEV {
|
||||
if theErr, ok := err.(error); ok {
|
||||
|
@ -19,6 +19,7 @@ const (
|
||||
AlertStateAlerting AlertStateType = "alerting"
|
||||
AlertStateOK AlertStateType = "ok"
|
||||
AlertStatePending AlertStateType = "pending"
|
||||
AlertStateUnknown AlertStateType = "unknown"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -39,7 +40,12 @@ var (
|
||||
)
|
||||
|
||||
func (s AlertStateType) IsValid() bool {
|
||||
return s == AlertStateOK || s == AlertStateNoData || s == AlertStatePaused || s == AlertStatePending
|
||||
return s == AlertStateOK ||
|
||||
s == AlertStateNoData ||
|
||||
s == AlertStatePaused ||
|
||||
s == AlertStatePending ||
|
||||
s == AlertStateAlerting ||
|
||||
s == AlertStateUnknown
|
||||
}
|
||||
|
||||
func (s NoDataOption) IsValid() bool {
|
||||
@ -66,12 +72,13 @@ type Alert struct {
|
||||
PanelId int64
|
||||
Name string
|
||||
Message string
|
||||
Severity string
|
||||
Severity string //Unused
|
||||
State AlertStateType
|
||||
Handler int64
|
||||
Handler int64 //Unused
|
||||
Silenced bool
|
||||
ExecutionError string
|
||||
Frequency int64
|
||||
For time.Duration
|
||||
|
||||
EvalData *simplejson.Json
|
||||
NewStateDate time.Time
|
||||
|
@ -24,6 +24,7 @@ type DataSourcePlugin struct {
|
||||
Metrics bool `json:"metrics"`
|
||||
Alerting bool `json:"alerting"`
|
||||
Explore bool `json:"explore"`
|
||||
Table bool `json:"tables"`
|
||||
Logs bool `json:"logs"`
|
||||
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
|
||||
BuiltIn bool `json:"builtIn,omitempty"`
|
||||
|
@ -68,8 +68,13 @@ func (c *EvalContext) GetStateModel() *StateDescription {
|
||||
Color: "#D63232",
|
||||
Text: "Alerting",
|
||||
}
|
||||
case m.AlertStateUnknown:
|
||||
return &StateDescription{
|
||||
Color: "#888888",
|
||||
Text: "Unknown",
|
||||
}
|
||||
default:
|
||||
panic("Unknown rule state " + c.Rule.State)
|
||||
panic("Unknown rule state for alert " + c.Rule.State)
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,7 +118,26 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
|
||||
}
|
||||
|
||||
// GetNewState returns the new state from the alert rule evaluation
|
||||
func (c *EvalContext) GetNewState() m.AlertStateType {
|
||||
ns := getNewStateInternal(c)
|
||||
if ns != m.AlertStateAlerting || c.Rule.For == 0 {
|
||||
return ns
|
||||
}
|
||||
|
||||
since := time.Since(c.Rule.LastStateChange)
|
||||
if c.PrevAlertState == m.AlertStatePending && since > c.Rule.For {
|
||||
return m.AlertStateAlerting
|
||||
}
|
||||
|
||||
if c.PrevAlertState == m.AlertStateAlerting {
|
||||
return m.AlertStateAlerting
|
||||
}
|
||||
|
||||
return m.AlertStatePending
|
||||
}
|
||||
|
||||
func getNewStateInternal(c *EvalContext) m.AlertStateType {
|
||||
if c.Error != nil {
|
||||
c.log.Error("Alert Rule Result Error",
|
||||
"ruleId", c.Rule.Id,
|
||||
@ -125,11 +149,13 @@ func (c *EvalContext) GetNewState() m.AlertStateType {
|
||||
return c.PrevAlertState
|
||||
}
|
||||
return c.Rule.ExecutionErrorState.ToAlertState()
|
||||
}
|
||||
|
||||
} else if c.Firing {
|
||||
if c.Firing {
|
||||
return m.AlertStateAlerting
|
||||
}
|
||||
|
||||
} else if c.NoDataFound {
|
||||
if c.NoDataFound {
|
||||
c.log.Info("Alert Rule returned no data",
|
||||
"ruleId", c.Rule.Id,
|
||||
"name", c.Rule.Name,
|
||||
|
@ -2,11 +2,11 @@ package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestStateIsUpdatedWhenNeeded(t *testing.T) {
|
||||
@ -31,71 +31,176 @@ func TestStateIsUpdatedWhenNeeded(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlertingEvalContext(t *testing.T) {
|
||||
Convey("Should compute and replace properly new rule state", t, func() {
|
||||
func TestGetStateFromEvalContext(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
expected models.AlertStateType
|
||||
applyFn func(ec *EvalContext)
|
||||
}{
|
||||
{
|
||||
name: "ok -> alerting",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.Firing = true
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> error(alerting)",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Error = errors.New("test error")
|
||||
ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> pending. since its been firing for less than FOR",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Firing = true
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
|
||||
ec.Rule.For = time.Minute * 5
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> pending. since it has to be pending longer than FOR and prev state is ok",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Firing = true
|
||||
ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
|
||||
ec.Rule.For = time.Minute * 2
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> alerting. since its been firing for more than FOR and prev state is pending",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Firing = true
|
||||
ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
|
||||
ec.Rule.For = time.Minute * 2
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "alerting -> alerting. should not update regardless of FOR",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateAlerting
|
||||
ec.Firing = true
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
|
||||
ec.Rule.For = time.Minute * 2
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> ok. should not update regardless of FOR",
|
||||
expected: models.AlertStateOK,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
|
||||
ec.Rule.For = time.Minute * 2
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> error(keep_last)",
|
||||
expected: models.AlertStateOK,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Error = errors.New("test error")
|
||||
ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> error(keep_last)",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Error = errors.New("test error")
|
||||
ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> no_data(alerting)",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ec.NoDataFound = true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok -> no_data(keep_last)",
|
||||
expected: models.AlertStateOK,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStateOK
|
||||
ec.Rule.NoDataState = models.NoDataKeepState
|
||||
ec.NoDataFound = true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> no_data(keep_last)",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Rule.NoDataState = models.NoDataKeepState
|
||||
ec.NoDataFound = true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> no_data(alerting) with for duration have not passed",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ec.NoDataFound = true
|
||||
ec.Rule.For = time.Minute * 5
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> no_data(alerting) should set alerting since time passed FOR",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ec.NoDataFound = true
|
||||
ec.Rule.For = time.Minute * 2
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> error(alerting) with for duration have not passed ",
|
||||
expected: models.AlertStatePending,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
ec.Error = errors.New("test error")
|
||||
ec.Rule.For = time.Minute * 5
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pending -> error(alerting) should set alerting since time passed FOR",
|
||||
expected: models.AlertStateAlerting,
|
||||
applyFn: func(ec *EvalContext) {
|
||||
ec.PrevAlertState = models.AlertStatePending
|
||||
ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
ec.Error = errors.New("test error")
|
||||
ec.Rule.For = time.Minute * 2
|
||||
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
|
||||
dummieError := fmt.Errorf("dummie error")
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Firing = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> error(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
})
|
||||
|
||||
Convey("pending -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
})
|
||||
|
||||
Convey("pending -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
})
|
||||
})
|
||||
tc.applyFn(ctx)
|
||||
have := ctx.GetNewState()
|
||||
if have != tc.expected {
|
||||
t.Errorf("failed: %s \n expected '%s' have '%s'\n", tc.name, tc.expected, string(have))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ package alerting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@ -115,6 +115,15 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
||||
return nil, ValidationError{Reason: "Could not parse frequency"}
|
||||
}
|
||||
|
||||
rawFor := jsonAlert.Get("for").MustString()
|
||||
var forValue time.Duration
|
||||
if rawFor != "" {
|
||||
forValue, err = time.ParseDuration(rawFor)
|
||||
if err != nil {
|
||||
return nil, ValidationError{Reason: "Could not parse for"}
|
||||
}
|
||||
}
|
||||
|
||||
alert := &m.Alert{
|
||||
DashboardId: e.Dash.Id,
|
||||
OrgId: e.OrgID,
|
||||
@ -124,6 +133,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
||||
Handler: jsonAlert.Get("handler").MustInt64(),
|
||||
Message: jsonAlert.Get("message").MustString(),
|
||||
Frequency: frequency,
|
||||
For: forValue,
|
||||
}
|
||||
|
||||
for _, condition := range jsonAlert.Get("conditions").MustArray() {
|
||||
|
@ -3,6 +3,7 @@ package alerting
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@ -46,7 +47,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
json, err := ioutil.ReadFile("./test-data/graphite-alert.json")
|
||||
json, err := ioutil.ReadFile("./testdata/graphite-alert.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Extractor should not modify the original json", func() {
|
||||
@ -118,6 +119,11 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
So(alerts[1].PanelId, ShouldEqual, 4)
|
||||
})
|
||||
|
||||
Convey("should extract for param", func() {
|
||||
So(alerts[0].For, ShouldEqual, time.Minute*2)
|
||||
So(alerts[1].For, ShouldEqual, time.Duration(0))
|
||||
})
|
||||
|
||||
Convey("should extract name and desc", func() {
|
||||
So(alerts[0].Name, ShouldEqual, "name1")
|
||||
So(alerts[0].Message, ShouldEqual, "desc1")
|
||||
@ -140,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Panels missing id should return error", func() {
|
||||
panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json")
|
||||
panelWithoutId, err := ioutil.ReadFile("./testdata/panels-missing-id.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(panelWithoutId)
|
||||
@ -156,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Panel with id set to zero should return error", func() {
|
||||
panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json")
|
||||
panelWithIdZero, err := ioutil.ReadFile("./testdata/panel-with-id-0.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(panelWithIdZero)
|
||||
@ -172,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Parse alerts from dashboard without rows", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
|
||||
json, err := ioutil.ReadFile("./testdata/v5-dashboard.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(json)
|
||||
@ -192,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Parse and validate dashboard containing influxdb alert", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/influxdb-alert.json")
|
||||
json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(json)
|
||||
@ -221,7 +227,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should be able to extract collapsed panels", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/collapsed-panels.json")
|
||||
json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(json)
|
||||
@ -242,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Parse and validate dashboard without id and containing an alert", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/dash-without-id.json")
|
||||
json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJSON, err := simplejson.NewJson(json)
|
||||
|
@ -1,13 +1,60 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestWhenAlertManagerShouldNotify(t *testing.T) {
|
||||
tcs := []struct {
|
||||
prevState m.AlertStateType
|
||||
newState m.AlertStateType
|
||||
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
prevState: m.AlertStatePending,
|
||||
newState: m.AlertStateOK,
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
prevState: m.AlertStateAlerting,
|
||||
newState: m.AlertStateOK,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
prevState: m.AlertStateOK,
|
||||
newState: m.AlertStatePending,
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
prevState: m.AlertStateUnknown,
|
||||
newState: m.AlertStatePending,
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
am := &AlertmanagerNotifier{log: log.New("test.logger")}
|
||||
evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
||||
State: tc.prevState,
|
||||
})
|
||||
|
||||
evalContext.Rule.State = tc.newState
|
||||
|
||||
res := am.ShouldNotify(context.TODO(), evalContext, &m.AlertNotificationState{})
|
||||
if res != tc.expect {
|
||||
t.Errorf("got %v expected %v", res, tc.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertmanagerNotifier(t *testing.T) {
|
||||
Convey("Alertmanager notifier tests", t, func() {
|
||||
|
||||
|
@ -67,6 +67,16 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC
|
||||
}
|
||||
|
||||
// Do not notify when we become OK for the first time.
|
||||
if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStateOK {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notify when we become OK for the first time.
|
||||
if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStatePending {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notify when we become OK from pending
|
||||
if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK {
|
||||
return false
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStatePending,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
@ -38,7 +37,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStateAlerting,
|
||||
prevState: m.AlertStateOK,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
@ -47,7 +45,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStatePending,
|
||||
prevState: m.AlertStateOK,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
@ -56,7 +53,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateOK,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
@ -65,7 +61,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateOK,
|
||||
sendReminder: true,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
@ -74,7 +69,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateAlerting,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
@ -94,7 +88,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
prevState: m.AlertStateAlerting,
|
||||
frequency: time.Minute * 10,
|
||||
sendReminder: true,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
@ -132,6 +125,27 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
prevState: m.AlertStateOK,
|
||||
state: &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-2 * time.Minute).Unix()},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "unknown -> ok",
|
||||
prevState: m.AlertStateUnknown,
|
||||
newState: m.AlertStateOK,
|
||||
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "unknown -> pending",
|
||||
prevState: m.AlertStateUnknown,
|
||||
newState: m.AlertStatePending,
|
||||
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "unknown -> alerting",
|
||||
prevState: m.AlertStateUnknown,
|
||||
newState: m.AlertStateAlerting,
|
||||
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
@ -141,6 +155,10 @@ func TestShouldSendAlertNotification(t *testing.T) {
|
||||
State: tc.prevState,
|
||||
})
|
||||
|
||||
if tc.state == nil {
|
||||
tc.state = &m.AlertNotificationState{}
|
||||
}
|
||||
|
||||
evalContext.Rule.State = tc.newState
|
||||
nb := &NotifierBase{SendReminder: tc.sendReminder, Frequency: tc.frequency}
|
||||
|
||||
|
@ -73,6 +73,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
// when two servers are raising. This makes sure that the server
|
||||
// with the last state change always sends a notification.
|
||||
evalContext.Rule.StateChanges = cmd.Result.StateChanges
|
||||
|
||||
// Update the last state change of the alert rule in memory
|
||||
evalContext.Rule.LastStateChange = time.Now()
|
||||
}
|
||||
|
||||
// save annotation
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
|
||||
@ -18,6 +19,8 @@ type Rule struct {
|
||||
Frequency int64
|
||||
Name string
|
||||
Message string
|
||||
LastStateChange time.Time
|
||||
For time.Duration
|
||||
NoDataState m.NoDataOption
|
||||
ExecutionErrorState m.ExecutionErrorOption
|
||||
State m.AlertStateType
|
||||
@ -100,6 +103,8 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
||||
model.Message = ruleDef.Message
|
||||
model.Frequency = ruleDef.Frequency
|
||||
model.State = ruleDef.State
|
||||
model.LastStateChange = ruleDef.NewStateDate
|
||||
model.For = ruleDef.For
|
||||
model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data"))
|
||||
model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
|
||||
model.StateChanges = ruleDef.StateChanges
|
||||
|
@ -23,6 +23,7 @@
|
||||
"message": "desc1",
|
||||
"handler": 1,
|
||||
"frequency": "60s",
|
||||
"for": "2m",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
@ -193,7 +193,8 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
|
||||
if alertToUpdate.ContainsUpdates(alert) {
|
||||
alert.Updated = timeNow()
|
||||
alert.State = alertToUpdate.State
|
||||
sess.MustCols("message")
|
||||
sess.MustCols("message", "for")
|
||||
|
||||
_, err := sess.ID(alert.Id).Update(alert)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -204,7 +205,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
|
||||
} else {
|
||||
alert.Updated = timeNow()
|
||||
alert.Created = timeNow()
|
||||
alert.State = m.AlertStatePending
|
||||
alert.State = m.AlertStateUnknown
|
||||
alert.NewStateDate = timeNow()
|
||||
|
||||
_, err := sess.Insert(alert)
|
||||
@ -299,7 +300,7 @@ func PauseAlert(cmd *m.PauseAlertCommand) error {
|
||||
params = append(params, string(m.AlertStatePaused))
|
||||
params = append(params, timeNow())
|
||||
} else {
|
||||
params = append(params, string(m.AlertStatePending))
|
||||
params = append(params, string(m.AlertStateUnknown))
|
||||
params = append(params, timeNow())
|
||||
}
|
||||
|
||||
@ -323,7 +324,7 @@ func PauseAllAlerts(cmd *m.PauseAllAlertCommand) error {
|
||||
if cmd.Paused {
|
||||
newState = string(m.AlertStatePaused)
|
||||
} else {
|
||||
newState = string(m.AlertStatePending)
|
||||
newState = string(m.AlertStateUnknown)
|
||||
}
|
||||
|
||||
res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow())
|
||||
|
@ -109,7 +109,7 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
So(alert.DashboardId, ShouldEqual, testDash.Id)
|
||||
So(alert.PanelId, ShouldEqual, 1)
|
||||
So(alert.Name, ShouldEqual, "Alerting title")
|
||||
So(alert.State, ShouldEqual, "pending")
|
||||
So(alert.State, ShouldEqual, m.AlertStateUnknown)
|
||||
So(alert.NewStateDate, ShouldNotBeNil)
|
||||
So(alert.EvalData, ShouldNotBeNil)
|
||||
So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
|
||||
@ -154,7 +154,7 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
So(query.Result[0].Name, ShouldEqual, "Name")
|
||||
|
||||
Convey("Alert state should not be updated", func() {
|
||||
So(query.Result[0].State, ShouldEqual, "pending")
|
||||
So(query.Result[0].State, ShouldEqual, m.AlertStateUnknown)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -133,4 +133,8 @@ func addAlertMigrations(mg *Migrator) {
|
||||
mg.AddMigration("create alert_notification_state table v1", NewAddTableMigration(alert_notification_state))
|
||||
mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id",
|
||||
NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0]))
|
||||
|
||||
mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{
|
||||
Name: "for", Type: DB_BigInt, Nullable: true,
|
||||
}))
|
||||
}
|
||||
|
@ -57,6 +57,9 @@ var (
|
||||
IsEnterprise bool
|
||||
ApplicationName string
|
||||
|
||||
// packaging
|
||||
Packaging = "unknown"
|
||||
|
||||
// Paths
|
||||
HomePath string
|
||||
PluginsPath string
|
||||
@ -112,6 +115,7 @@ var (
|
||||
ExternalUserMngLinkUrl string
|
||||
ExternalUserMngLinkName string
|
||||
ExternalUserMngInfo string
|
||||
OAuthAutoLogin bool
|
||||
ViewersCanEdit bool
|
||||
|
||||
// Http auth
|
||||
@ -215,6 +219,8 @@ type Cfg struct {
|
||||
DisableBruteForceLoginProtection bool
|
||||
TempDataLifetime time.Duration
|
||||
MetricsEndpointEnabled bool
|
||||
MetricsEndpointBasicAuthUsername string
|
||||
MetricsEndpointBasicAuthPassword string
|
||||
EnableAlphaPanels bool
|
||||
EnterpriseLicensePath string
|
||||
}
|
||||
@ -626,6 +632,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
auth := iniFile.Section("auth")
|
||||
DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
|
||||
DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)
|
||||
OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false)
|
||||
SignoutRedirectUrl = auth.Key("signout_redirect_url").String()
|
||||
|
||||
// anonymous access
|
||||
@ -676,6 +683,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
|
||||
cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
|
||||
cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true)
|
||||
cfg.MetricsEndpointBasicAuthUsername = iniFile.Section("metrics").Key("basic_auth_username").String()
|
||||
cfg.MetricsEndpointBasicAuthPassword = iniFile.Section("metrics").Key("basic_auth_password").String()
|
||||
|
||||
analytics := iniFile.Section("analytics")
|
||||
ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)
|
||||
|
@ -32,6 +32,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool {
|
||||
|
||||
func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
var data struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
@ -47,6 +48,7 @@ func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
|
||||
}
|
||||
|
||||
return &BasicUserInfo{
|
||||
Id: data.Id,
|
||||
Name: data.Name,
|
||||
Email: data.Email,
|
||||
Login: data.Email,
|
||||
|
@ -126,6 +126,18 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
plog.Error("Execute Query Panic", "error", err, "stack", log.Stack(1))
|
||||
if theErr, ok := err.(error); ok {
|
||||
resultChan <- &tsdb.QueryResult{
|
||||
RefId: query.RefId,
|
||||
Error: theErr,
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
queryRes, err := e.executeQuery(ectx, query, queryContext)
|
||||
if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
|
||||
return err
|
||||
@ -146,6 +158,17 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
for region, getMetricDataQuery := range getMetricDataQueries {
|
||||
q := getMetricDataQuery
|
||||
eg.Go(func() error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
plog.Error("Execute Get Metric Data Query Panic", "error", err, "stack", log.Stack(1))
|
||||
if theErr, ok := err.(error); ok {
|
||||
resultChan <- &tsdb.QueryResult{
|
||||
Error: theErr,
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext)
|
||||
if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
|
||||
return err
|
||||
@ -188,8 +211,8 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatch
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if endTime.Before(startTime) {
|
||||
return nil, fmt.Errorf("Invalid time range: End time can't be before start time")
|
||||
if !startTime.Before(endTime) {
|
||||
return nil, fmt.Errorf("Invalid time range: Start time must be before end time")
|
||||
}
|
||||
|
||||
params := &cloudwatch.GetMetricStatisticsInput{
|
||||
|
@ -1,9 +1,13 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
@ -14,6 +18,24 @@ import (
|
||||
func TestCloudWatch(t *testing.T) {
|
||||
Convey("CloudWatch", t, func() {
|
||||
|
||||
Convey("executeQuery", func() {
|
||||
e := &CloudWatchExecutor{
|
||||
DataSource: &models.DataSource{
|
||||
JsonData: simplejson.New(),
|
||||
},
|
||||
}
|
||||
|
||||
Convey("End time before start time should result in error", func() {
|
||||
_, err := e.executeQuery(context.Background(), &CloudWatchQuery{}, &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("now-1h", "now-2h")})
|
||||
So(err.Error(), ShouldEqual, "Invalid time range: Start time must be before end time")
|
||||
})
|
||||
|
||||
Convey("End time equals start time should result in error", func() {
|
||||
_, err := e.executeQuery(context.Background(), &CloudWatchQuery{}, &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("now-1h", "now-1h")})
|
||||
So(err.Error(), ShouldEqual, "Invalid time range: Start time must be before end time")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("can parse cloudwatch json model", func() {
|
||||
json := `
|
||||
{
|
||||
|
@ -46,6 +46,7 @@ func init() {
|
||||
"AWS/Billing": {"EstimatedCharges"},
|
||||
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
|
||||
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
|
||||
"AWS/CloudHSM": {"HsmUnhealthy", "HsmTemperature", "HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSslCtxsOccupied", "HsmSessionCount", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"},
|
||||
"AWS/Connect": {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
|
||||
"AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
|
||||
"AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
|
||||
@ -121,6 +122,7 @@ func init() {
|
||||
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
|
||||
"AWS/CloudFront": {"DistributionId", "Region"},
|
||||
"AWS/CloudSearch": {},
|
||||
"AWS/CloudHSM": {"Region", "ClusterId", "HsmId"},
|
||||
"AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
|
||||
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
|
||||
"AWS/DX": {"ConnectionId"},
|
||||
|
@ -0,0 +1,67 @@
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
interface Props {
|
||||
text: () => string;
|
||||
elType?: string;
|
||||
onSuccess?: (evt: any) => void;
|
||||
onError?: (evt: any) => void;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export class CopyToClipboard extends PureComponent<Props> {
|
||||
clipboardjs: any;
|
||||
myRef: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.myRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { text, onSuccess, onError } = this.props;
|
||||
|
||||
this.clipboardjs = new ClipboardJS(this.myRef.current, {
|
||||
text: text,
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
this.clipboardjs.on('success', evt => {
|
||||
evt.clearSelection();
|
||||
onSuccess(evt);
|
||||
});
|
||||
}
|
||||
|
||||
if (onError) {
|
||||
this.clipboardjs.on('error', evt => {
|
||||
console.error('Action:', evt.action);
|
||||
console.error('Trigger:', evt.trigger);
|
||||
onError(evt);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.clipboardjs) {
|
||||
this.clipboardjs.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
getElementType = () => {
|
||||
return this.props.elType || 'button';
|
||||
};
|
||||
|
||||
render() {
|
||||
const { elType, text, children, onError, onSuccess, ...restProps } = this.props;
|
||||
|
||||
return React.createElement(
|
||||
this.getElementType(),
|
||||
{
|
||||
ref: this.myRef,
|
||||
...restProps,
|
||||
},
|
||||
this.props.children
|
||||
);
|
||||
}
|
||||
}
|
43
public/app/core/components/Form/Element.tsx
Normal file
43
public/app/core/components/Form/Element.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { PureComponent, ReactNode, ReactElement } from 'react';
|
||||
import { Label } from './Label';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
interface Props {
|
||||
label?: ReactNode;
|
||||
labelClassName?: string;
|
||||
id?: string;
|
||||
children: ReactElement<any>;
|
||||
}
|
||||
|
||||
export class Element extends PureComponent<Props> {
|
||||
elementId: string = this.props.id || uniqueId('form-element-');
|
||||
|
||||
get elementLabel() {
|
||||
const { label, labelClassName } = this.props;
|
||||
|
||||
if (label) {
|
||||
return (
|
||||
<Label htmlFor={this.elementId} className={labelClassName}>
|
||||
{label}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get children() {
|
||||
const { children } = this.props;
|
||||
|
||||
return React.cloneElement(children, { id: this.elementId });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="our-custom-wrapper-class">
|
||||
{this.elementLabel}
|
||||
{this.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
53
public/app/core/components/Form/Input.test.tsx
Normal file
53
public/app/core/components/Form/Input.test.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Input, EventsWithValidation } from './Input';
|
||||
import { ValidationEvents } from 'app/types';
|
||||
|
||||
const TEST_ERROR_MESSAGE = 'Value must be empty or less than 3 chars';
|
||||
const testBlurValidation: ValidationEvents = {
|
||||
[EventsWithValidation.onBlur]: [
|
||||
{
|
||||
rule: (value: string) => {
|
||||
if (!value || value.length < 3) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
errorMessage: TEST_ERROR_MESSAGE,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Input', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer.create(<Input />).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should validate with error onBlur', () => {
|
||||
const wrapper = shallow(<Input validationEvents={testBlurValidation} />);
|
||||
const evt = {
|
||||
persist: jest.fn,
|
||||
target: {
|
||||
value: 'I can not be more than 2 chars',
|
||||
},
|
||||
};
|
||||
|
||||
wrapper.find('input').simulate('blur', evt);
|
||||
expect(wrapper.state('error')).toBe(TEST_ERROR_MESSAGE);
|
||||
});
|
||||
|
||||
it('should validate without error onBlur', () => {
|
||||
const wrapper = shallow(<Input validationEvents={testBlurValidation} />);
|
||||
const evt = {
|
||||
persist: jest.fn,
|
||||
target: {
|
||||
value: 'Hi',
|
||||
},
|
||||
};
|
||||
|
||||
wrapper.find('input').simulate('blur', evt);
|
||||
expect(wrapper.state('error')).toBe(null);
|
||||
});
|
||||
});
|
89
public/app/core/components/Form/Input.tsx
Normal file
89
public/app/core/components/Form/Input.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { ValidationEvents, ValidationRule } from 'app/types';
|
||||
import { validate, hasValidationEvent } from 'app/core/utils/validate';
|
||||
|
||||
export enum InputStatus {
|
||||
Invalid = 'invalid',
|
||||
Valid = 'valid',
|
||||
}
|
||||
|
||||
export enum InputTypes {
|
||||
Text = 'text',
|
||||
Number = 'number',
|
||||
Password = 'password',
|
||||
Email = 'email',
|
||||
}
|
||||
|
||||
export enum EventsWithValidation {
|
||||
onBlur = 'onBlur',
|
||||
onFocus = 'onFocus',
|
||||
onChange = 'onChange',
|
||||
}
|
||||
|
||||
interface Props extends React.HTMLProps<HTMLInputElement> {
|
||||
validationEvents?: ValidationEvents;
|
||||
hideErrorMessage?: boolean;
|
||||
|
||||
// Override event props and append status as argument
|
||||
onBlur?: (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => void;
|
||||
onFocus?: (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => void;
|
||||
onChange?: (event: React.FormEvent<HTMLInputElement>, status?: InputStatus) => void;
|
||||
}
|
||||
|
||||
export class Input extends PureComponent<Props> {
|
||||
state = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
get status() {
|
||||
return this.state.error ? InputStatus.Invalid : InputStatus.Valid;
|
||||
}
|
||||
|
||||
get isInvalid() {
|
||||
return this.status === InputStatus.Invalid;
|
||||
}
|
||||
|
||||
validatorAsync = (validationRules: ValidationRule[]) => {
|
||||
return evt => {
|
||||
const errors = validate(evt.target.value, validationRules);
|
||||
this.setState(prevState => {
|
||||
return {
|
||||
...prevState,
|
||||
error: errors ? errors[0] : null,
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
populateEventPropsWithStatus = (restProps, validationEvents: ValidationEvents) => {
|
||||
const inputElementProps = { ...restProps };
|
||||
Object.keys(EventsWithValidation).forEach((eventName: EventsWithValidation) => {
|
||||
if (hasValidationEvent(eventName, validationEvents) || restProps[eventName]) {
|
||||
inputElementProps[eventName] = async evt => {
|
||||
evt.persist(); // Needed for async. https://reactjs.org/docs/events.html#event-pooling
|
||||
if (hasValidationEvent(eventName, validationEvents)) {
|
||||
await this.validatorAsync(validationEvents[eventName]).apply(this, [evt]);
|
||||
}
|
||||
if (restProps[eventName]) {
|
||||
restProps[eventName].apply(null, [evt, this.status]);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
return inputElementProps;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { validationEvents, className, hideErrorMessage, ...restProps } = this.props;
|
||||
const { error } = this.state;
|
||||
const inputClassName = 'gf-form-input' + (this.isInvalid ? ' invalid' : '');
|
||||
const inputElementProps = this.populateEventPropsWithStatus(restProps, validationEvents);
|
||||
|
||||
return (
|
||||
<div className="our-custom-wrapper-class">
|
||||
<input {...inputElementProps} className={inputClassName} />
|
||||
{error && !hideErrorMessage && <span>{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
19
public/app/core/components/Form/Label.tsx
Normal file
19
public/app/core/components/Form/Label.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
htmlFor?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export class Label extends PureComponent<Props> {
|
||||
render() {
|
||||
const { children, htmlFor, className } = this.props;
|
||||
|
||||
return (
|
||||
<label className={`custom-label-class ${className || ''}`} htmlFor={htmlFor}>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Input renders correctly 1`] = `
|
||||
<div
|
||||
className="our-custom-wrapper-class"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
/>
|
||||
</div>
|
||||
`;
|
3
public/app/core/components/Form/index.ts
Normal file
3
public/app/core/components/Form/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { Element } from './Element';
|
||||
export { Input } from './Input';
|
||||
export { Label } from './Label';
|
51
public/app/core/components/JSONFormatter/JSONFormatter.tsx
Normal file
51
public/app/core/components/JSONFormatter/JSONFormatter.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React, { PureComponent, createRef } from 'react';
|
||||
// import JSONFormatterJS, { JSONFormatterConfiguration } from 'json-formatter-js';
|
||||
import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
json: {};
|
||||
config?: any;
|
||||
open?: number;
|
||||
onDidRender?: (formattedJson: any) => void;
|
||||
}
|
||||
|
||||
export class JSONFormatter extends PureComponent<Props> {
|
||||
private wrapperRef = createRef<HTMLDivElement>();
|
||||
|
||||
static defaultProps = {
|
||||
open: 3,
|
||||
config: {
|
||||
animateOpen: true,
|
||||
},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.renderJson();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.renderJson();
|
||||
}
|
||||
|
||||
renderJson = () => {
|
||||
const { json, config, open, onDidRender } = this.props;
|
||||
const wrapperEl = this.wrapperRef.current;
|
||||
const formatter = new JsonExplorer(json, open, config);
|
||||
const hasChildren: boolean = wrapperEl.hasChildNodes();
|
||||
if (hasChildren) {
|
||||
wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild);
|
||||
} else {
|
||||
wrapperEl.appendChild(formatter.render());
|
||||
}
|
||||
|
||||
if (onDidRender) {
|
||||
onDidRender(formatter.json);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
return <div className={className} ref={this.wrapperRef} />;
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@ export default class UnitPicker extends PureComponent<Props> {
|
||||
const styles = {
|
||||
...ResetStyles,
|
||||
menu: () => ({
|
||||
maxHeight: '500px',
|
||||
maxHeight: '75%',
|
||||
overflow: 'scroll',
|
||||
}),
|
||||
menuList: () =>
|
||||
@ -66,6 +66,7 @@ export default class UnitPicker extends PureComponent<Props> {
|
||||
className={`width-${width} gf-form-input--form-dropdown`}
|
||||
defaultValue={value}
|
||||
isSearchable={true}
|
||||
menuShouldScrollIntoView={false}
|
||||
options={groupOptions}
|
||||
placeholder="Choose"
|
||||
onChange={onSelected}
|
||||
|
@ -14,3 +14,4 @@ exports[`PickerOption renders correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -36,17 +36,13 @@ export class Switch extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="gf-form">
|
||||
{label && (
|
||||
<label htmlFor={labelId} className={labelClassName}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<label htmlFor={labelId} className="gf-form-switch-container">
|
||||
{label && <label className={labelClassName}>{label}</label>}
|
||||
<div className={switchClassName}>
|
||||
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
|
||||
<label htmlFor={labelId} />
|
||||
<span className="gf-form-switch__slider" />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -64,10 +64,10 @@
|
||||
|
||||
<div class="search-results" ng-show="ctrl.sections.length > 0">
|
||||
<div class="search-results-filter-row">
|
||||
<gf-form-switch
|
||||
<gf-form-checkbox
|
||||
on-change="ctrl.onSelectAllChanged()"
|
||||
checked="ctrl.selectAllChecked"
|
||||
switch-class="gf-form-switch--transparent gf-form-switch--search-result-filter-row__checkbox"
|
||||
switch-class="gf-form-checkbox--transparent"
|
||||
/>
|
||||
<div class="search-results-filter-row__filters">
|
||||
<div class="gf-form-select-wrapper" ng-show="!(ctrl.canMove || ctrl.canDelete)">
|
||||
|
@ -1,12 +1,12 @@
|
||||
<div ng-repeat="section in ctrl.results" class="search-section">
|
||||
<div class="search-section__header pointer" ng-hide="section.hideHeader" ng-class="{'selected': section.selected}" ng-click="ctrl.toggleFolderExpand(section)">
|
||||
<div ng-click="ctrl.toggleSelection(section, $event)">
|
||||
<gf-form-switch
|
||||
<div ng-click="ctrl.toggleSelection(section, $event)" class="center-vh">
|
||||
<gf-form-checkbox
|
||||
ng-show="ctrl.editable"
|
||||
on-change="ctrl.selectionChanged($event)"
|
||||
checked="section.checked"
|
||||
switch-class="gf-form-switch--transparent gf-form-switch--search-result__section">
|
||||
</gf-form-switch>
|
||||
switch-class="gf-form-checkbox--transparent">
|
||||
</gf-form-checkbox>
|
||||
</div>
|
||||
<i class="search-section__header__icon" ng-class="section.icon"></i>
|
||||
<span class="search-section__header__text">{{::section.title}}</span>
|
||||
@ -21,13 +21,13 @@
|
||||
|
||||
<div ng-if="section.expanded">
|
||||
<a ng-repeat="item in section.items" class="search-item search-item--indent" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
|
||||
<div ng-click="ctrl.toggleSelection(item, $event)">
|
||||
<gf-form-switch
|
||||
<div ng-click="ctrl.toggleSelection(item, $event)" class="center-vh">
|
||||
<gf-form-checkbox
|
||||
ng-show="ctrl.editable"
|
||||
on-change="ctrl.selectionChanged()"
|
||||
checked="item.checked"
|
||||
switch-class="gf-form-switch--transparent gf-form-switch--search-result__item">
|
||||
</gf-form-switch>
|
||||
switch-class="gf-form-checkbox--transparent">
|
||||
</gf-form-checkbox>
|
||||
</div>
|
||||
<span class="search-item__icon">
|
||||
<i class="gicon mini gicon-dashboard-list"></i>
|
||||
|
@ -1,16 +1,33 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
const template = `
|
||||
<label for="check-{{ctrl.id}}" class="gf-form-label {{ctrl.labelClass}} pointer" ng-show="ctrl.label">
|
||||
{{ctrl.label}}
|
||||
<info-popover mode="right-normal" ng-if="ctrl.tooltip" position="top center">
|
||||
{{ctrl.tooltip}}
|
||||
</info-popover>
|
||||
<label for="check-{{ctrl.id}}" class="gf-form-switch-container">
|
||||
<div class="gf-form-label {{ctrl.labelClass}}" ng-show="ctrl.label">
|
||||
{{ctrl.label}}
|
||||
<info-popover mode="right-normal" ng-if="ctrl.tooltip" position="top center">
|
||||
{{ctrl.tooltip}}
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form-switch {{ctrl.switchClass}}" ng-if="ctrl.show">
|
||||
<input id="check-{{ctrl.id}}" type="checkbox" ng-model="ctrl.checked" ng-change="ctrl.internalOnChange()">
|
||||
<span class="gf-form-switch__slider"></span>
|
||||
</div>
|
||||
</label>
|
||||
`;
|
||||
|
||||
const checkboxTemplate = `
|
||||
<label for="check-{{ctrl.id}}" class="gf-form-switch-container">
|
||||
<div class="gf-form-label {{ctrl.labelClass}}" ng-show="ctrl.label">
|
||||
{{ctrl.label}}
|
||||
<info-popover mode="right-normal" ng-if="ctrl.tooltip" position="top center">
|
||||
{{ctrl.tooltip}}
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form-checkbox {{ctrl.switchClass}}" ng-if="ctrl.show">
|
||||
<input id="check-{{ctrl.id}}" type="checkbox" ng-model="ctrl.checked" ng-change="ctrl.internalOnChange()">
|
||||
<span class="gf-form-switch__checkbox"></span>
|
||||
</div>
|
||||
</label>
|
||||
<div class="gf-form-switch {{ctrl.switchClass}}" ng-if="ctrl.show">
|
||||
<input id="check-{{ctrl.id}}" type="checkbox" ng-model="ctrl.checked" ng-change="ctrl.internalOnChange()">
|
||||
<label for="check-{{ctrl.id}}" data-on="Yes" data-off="No"></label>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export class SwitchCtrl {
|
||||
@ -51,4 +68,23 @@ export function switchDirective() {
|
||||
};
|
||||
}
|
||||
|
||||
export function checkboxDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: SwitchCtrl,
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
scope: {
|
||||
checked: '=',
|
||||
label: '@',
|
||||
labelClass: '@',
|
||||
tooltip: '@',
|
||||
switchClass: '@',
|
||||
onChange: '&',
|
||||
},
|
||||
template: checkboxTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('gfFormSwitch', switchDirective);
|
||||
coreModule.directive('gfFormCheckbox', checkboxDirective);
|
||||
|
@ -14,3 +14,4 @@ export const DASHBOARD_TOP_PADDING = 20;
|
||||
|
||||
export const PANEL_HEADER_HEIGHT = 27;
|
||||
export const PANEL_BORDER = 2;
|
||||
export const PANEL_OPTIONS_KEY_PREFIX = 'options-';
|
||||
|
@ -31,6 +31,7 @@ export interface LogSearchMatch {
|
||||
}
|
||||
|
||||
export interface LogRow {
|
||||
duplicates?: number;
|
||||
entry: string;
|
||||
key: string; // timestamp + labels
|
||||
labels: string;
|
||||
@ -71,6 +72,53 @@ export interface LogsStreamLabels {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export enum LogsDedupStrategy {
|
||||
none = 'none',
|
||||
exact = 'exact',
|
||||
numbers = 'numbers',
|
||||
signature = 'signature',
|
||||
}
|
||||
|
||||
const isoDateRegexp = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d[,\.]\d+([+-][0-2]\d:[0-5]\d|Z)/g;
|
||||
function isDuplicateRow(row: LogRow, other: LogRow, strategy: LogsDedupStrategy): boolean {
|
||||
switch (strategy) {
|
||||
case LogsDedupStrategy.exact:
|
||||
// Exact still strips dates
|
||||
return row.entry.replace(isoDateRegexp, '') === other.entry.replace(isoDateRegexp, '');
|
||||
|
||||
case LogsDedupStrategy.numbers:
|
||||
return row.entry.replace(/\d/g, '') === other.entry.replace(/\d/g, '');
|
||||
|
||||
case LogsDedupStrategy.signature:
|
||||
return row.entry.replace(/\w/g, '') === other.entry.replace(/\w/g, '');
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): LogsModel {
|
||||
if (strategy === LogsDedupStrategy.none) {
|
||||
return logs;
|
||||
}
|
||||
|
||||
const dedupedRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => {
|
||||
const previous = result[result.length - 1];
|
||||
if (index > 0 && isDuplicateRow(row, previous, strategy)) {
|
||||
previous.duplicates++;
|
||||
} else {
|
||||
row.duplicates = 0;
|
||||
result.push(row);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...logs,
|
||||
rows: dedupedRows,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] {
|
||||
// Graph time series by log level
|
||||
const seriesByLevel = {};
|
||||
|
@ -5,6 +5,7 @@ import _ from 'lodash';
|
||||
export interface AngularComponent {
|
||||
destroy();
|
||||
digest();
|
||||
getScope();
|
||||
}
|
||||
|
||||
export class AngularLoader {
|
||||
@ -28,6 +29,9 @@ export class AngularLoader {
|
||||
digest: () => {
|
||||
scope.$digest();
|
||||
},
|
||||
getScope: () => {
|
||||
return scope;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ class DynamicDirectiveSrv {
|
||||
}
|
||||
|
||||
if (!directiveInfo.fn.registered) {
|
||||
console.log('register panel tab');
|
||||
coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
|
||||
directiveInfo.fn.registered = true;
|
||||
}
|
||||
|
@ -32,8 +32,8 @@ export class KeybindingSrv {
|
||||
|
||||
this.setupGlobal();
|
||||
appEvents.on('show-modal', () => (this.modalOpen = true));
|
||||
$rootScope.onAppEvent('timepickerOpen', () => (this.timepickerOpen = true));
|
||||
$rootScope.onAppEvent('timepickerClosed', () => (this.timepickerOpen = false));
|
||||
appEvents.on('timepickerOpen', () => (this.timepickerOpen = true));
|
||||
appEvents.on('timepickerClosed', () => (this.timepickerOpen = false));
|
||||
}
|
||||
|
||||
setupGlobal() {
|
||||
|
108
public/app/core/specs/logs_model.test.ts
Normal file
108
public/app/core/specs/logs_model.test.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { dedupLogRows, LogsDedupStrategy, LogsModel } from '../logs_model';
|
||||
|
||||
describe('dedupLogRows()', () => {
|
||||
test('should return rows as is when dedup is set to none', () => {
|
||||
const logs = {
|
||||
rows: [
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toMatchObject(logs.rows);
|
||||
});
|
||||
|
||||
test('should dedup on exact matches', () => {
|
||||
const logs = {
|
||||
rows: [
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'INFO test 2.44 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([
|
||||
{
|
||||
duplicates: 1,
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
duplicates: 0,
|
||||
entry: 'INFO test 2.44 on [xxx]',
|
||||
},
|
||||
{
|
||||
duplicates: 0,
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should dedup on number matches', () => {
|
||||
const logs = {
|
||||
rows: [
|
||||
{
|
||||
entry: 'WARN test 1.2323423 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'INFO test 2.44 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.numbers).rows).toEqual([
|
||||
{
|
||||
duplicates: 1,
|
||||
entry: 'WARN test 1.2323423 on [xxx]',
|
||||
},
|
||||
{
|
||||
duplicates: 0,
|
||||
entry: 'INFO test 2.44 on [xxx]',
|
||||
},
|
||||
{
|
||||
duplicates: 0,
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should dedup on signature matches', () => {
|
||||
const logs = {
|
||||
rows: [
|
||||
{
|
||||
entry: 'WARN test 1.2323423 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'INFO test 2.44 on [xxx]',
|
||||
},
|
||||
{
|
||||
entry: 'WARN test 1.23 on [xxx]',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.signature).rows).toEqual([
|
||||
{
|
||||
duplicates: 3,
|
||||
entry: 'WARN test 1.2323423 on [xxx]',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -86,11 +86,10 @@ export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]):
|
||||
if (arguments.length === 1) {
|
||||
return model;
|
||||
}
|
||||
|
||||
// Single query returns data columns and rows as is
|
||||
if (arguments.length === 2) {
|
||||
model.columns = [...tables[0].columns];
|
||||
model.rows = [...tables[0].rows];
|
||||
model.columns = tables[0].hasOwnProperty('columns') ? [...tables[0].columns] : [];
|
||||
model.rows = tables[0].hasOwnProperty('rows') ? [...tables[0].rows] : [];
|
||||
return model;
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)';
|
||||
export const OK_COLOR = 'rgba(11, 237, 50, 1)';
|
||||
export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)';
|
||||
export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)';
|
||||
export const PENDING_COLOR = 'rgba(247, 149, 32, 1)';
|
||||
export const REGION_FILL_ALPHA = 0.09;
|
||||
|
||||
const colors = [
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { DEFAULT_RANGE, serializeStateToUrlParam, parseUrlState } from './explore';
|
||||
import {
|
||||
DEFAULT_RANGE,
|
||||
serializeStateToUrlParam,
|
||||
parseUrlState,
|
||||
updateHistory,
|
||||
clearHistory,
|
||||
hasNonEmptyQuery,
|
||||
} from './explore';
|
||||
import { ExploreState } from 'app/types/explore';
|
||||
import store from 'app/core/store';
|
||||
|
||||
const DEFAULT_EXPLORE_STATE: ExploreState = {
|
||||
datasource: null,
|
||||
@ -10,7 +18,7 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
|
||||
exploreDatasources: [],
|
||||
graphRange: DEFAULT_RANGE,
|
||||
history: [],
|
||||
queries: [],
|
||||
initialQueries: [],
|
||||
queryTransactions: [],
|
||||
range: DEFAULT_RANGE,
|
||||
showingGraph: true,
|
||||
@ -33,10 +41,10 @@ describe('state functions', () => {
|
||||
|
||||
it('returns a valid Explore state from URL parameter', () => {
|
||||
const paramValue =
|
||||
'%7B"datasource":"Local","queries":%5B%7B"query":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D';
|
||||
'%7B"datasource":"Local","queries":%5B%7B"expr":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D';
|
||||
expect(parseUrlState(paramValue)).toMatchObject({
|
||||
datasource: 'Local',
|
||||
queries: [{ query: 'metric' }],
|
||||
queries: [{ expr: 'metric' }],
|
||||
range: {
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
@ -45,10 +53,10 @@ describe('state functions', () => {
|
||||
});
|
||||
|
||||
it('returns a valid Explore state from a compact URL parameter', () => {
|
||||
const paramValue = '%5B"now-1h","now","Local","metric"%5D';
|
||||
const paramValue = '%5B"now-1h","now","Local",%7B"expr":"metric"%7D%5D';
|
||||
expect(parseUrlState(paramValue)).toMatchObject({
|
||||
datasource: 'Local',
|
||||
queries: [{ query: 'metric' }],
|
||||
queries: [{ expr: 'metric' }],
|
||||
range: {
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
@ -66,18 +74,20 @@ describe('state functions', () => {
|
||||
from: 'now-5h',
|
||||
to: 'now',
|
||||
},
|
||||
queries: [
|
||||
initialQueries: [
|
||||
{
|
||||
query: 'metric{test="a/b"}',
|
||||
refId: '1',
|
||||
expr: 'metric{test="a/b"}',
|
||||
},
|
||||
{
|
||||
query: 'super{foo="x/z"}',
|
||||
refId: '2',
|
||||
expr: 'super{foo="x/z"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(serializeStateToUrlParam(state)).toBe(
|
||||
'{"datasource":"foo","queries":[{"query":"metric{test=\\"a/b\\"}"},' +
|
||||
'{"query":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}'
|
||||
'{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
|
||||
'{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}'
|
||||
);
|
||||
});
|
||||
|
||||
@ -89,17 +99,19 @@ describe('state functions', () => {
|
||||
from: 'now-5h',
|
||||
to: 'now',
|
||||
},
|
||||
queries: [
|
||||
initialQueries: [
|
||||
{
|
||||
query: 'metric{test="a/b"}',
|
||||
refId: '1',
|
||||
expr: 'metric{test="a/b"}',
|
||||
},
|
||||
{
|
||||
query: 'super{foo="x/z"}',
|
||||
refId: '2',
|
||||
expr: 'super{foo="x/z"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(serializeStateToUrlParam(state, true)).toBe(
|
||||
'["now-5h","now","foo","metric{test=\\"a/b\\"}","super{foo=\\"x/z\\"}"]'
|
||||
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -113,12 +125,14 @@ describe('state functions', () => {
|
||||
from: 'now - 5h',
|
||||
to: 'now',
|
||||
},
|
||||
queries: [
|
||||
initialQueries: [
|
||||
{
|
||||
query: 'metric{test="a/b"}',
|
||||
refId: '1',
|
||||
expr: 'metric{test="a/b"}',
|
||||
},
|
||||
{
|
||||
query: 'super{foo="x/z"}',
|
||||
refId: '2',
|
||||
expr: 'super{foo="x/z"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -126,14 +140,50 @@ describe('state functions', () => {
|
||||
const parsed = parseUrlState(serialized);
|
||||
|
||||
// Account for datasource vs datasourceName
|
||||
const { datasource, ...rest } = parsed;
|
||||
const sameState = {
|
||||
const { datasource, queries, ...rest } = parsed;
|
||||
const resultState = {
|
||||
...rest,
|
||||
datasource: DEFAULT_EXPLORE_STATE.datasource,
|
||||
datasourceName: datasource,
|
||||
initialQueries: queries,
|
||||
};
|
||||
|
||||
expect(state).toMatchObject(sameState);
|
||||
expect(state).toMatchObject(resultState);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateHistory()', () => {
|
||||
const datasourceId = 'myDatasource';
|
||||
const key = `grafana.explore.history.${datasourceId}`;
|
||||
|
||||
beforeEach(() => {
|
||||
clearHistory(datasourceId);
|
||||
expect(store.exists(key)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should save history item to localStorage', () => {
|
||||
const expected = [
|
||||
{
|
||||
query: { refId: '1', expr: 'metric' },
|
||||
},
|
||||
];
|
||||
expect(updateHistory([], datasourceId, [{ refId: '1', expr: 'metric' }])).toMatchObject(expected);
|
||||
expect(store.exists(key)).toBeTruthy();
|
||||
expect(store.getObject(key)).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasNonEmptyQuery', () => {
|
||||
test('should return true if one query is non-empty', () => {
|
||||
expect(hasNonEmptyQuery([{ refId: '1', key: '2', expr: 'foo' }])).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should return false if query is empty', () => {
|
||||
expect(hasNonEmptyQuery([{ refId: '1', key: '2' }])).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should return false if no queries exist', () => {
|
||||
expect(hasNonEmptyQuery([])).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,20 @@
|
||||
import { renderUrl } from 'app/core/utils/url';
|
||||
import { ExploreState, ExploreUrlState } from 'app/types/explore';
|
||||
import { ExploreState, ExploreUrlState, HistoryItem } from 'app/types/explore';
|
||||
import { DataQuery, RawTimeRange } from 'app/types/series';
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import colors from 'app/core/utils/colors';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import { parse as parseDate } from 'app/core/utils/datemath';
|
||||
import store from 'app/core/store';
|
||||
|
||||
export const DEFAULT_RANGE = {
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
};
|
||||
|
||||
const MAX_HISTORY_ITEMS = 100;
|
||||
|
||||
/**
|
||||
* Returns an Explore-URL that contains a panel's queries and the dashboard time range.
|
||||
*
|
||||
@ -23,7 +32,7 @@ export async function getExploreUrl(
|
||||
timeSrv: any
|
||||
) {
|
||||
let exploreDatasource = panelDatasource;
|
||||
let exploreTargets = panelTargets;
|
||||
let exploreTargets: DataQuery[] = panelTargets;
|
||||
let url;
|
||||
|
||||
// Mixed datasources need to choose only one datasource
|
||||
@ -57,6 +66,8 @@ export async function getExploreUrl(
|
||||
return url;
|
||||
}
|
||||
|
||||
const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
||||
|
||||
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||
if (initial) {
|
||||
try {
|
||||
@ -70,7 +81,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||
to: parsed[1],
|
||||
};
|
||||
const datasource = parsed[2];
|
||||
const queries = parsed.slice(3).map(query => ({ query }));
|
||||
const queries = parsed.slice(3);
|
||||
return { datasource, queries, range };
|
||||
}
|
||||
return parsed;
|
||||
@ -84,16 +95,97 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||
export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
|
||||
const urlState: ExploreUrlState = {
|
||||
datasource: state.datasourceName,
|
||||
queries: state.queries.map(q => ({ query: q.query })),
|
||||
queries: state.initialQueries.map(clearQueryKeys),
|
||||
range: state.range,
|
||||
};
|
||||
if (compact) {
|
||||
return JSON.stringify([
|
||||
urlState.range.from,
|
||||
urlState.range.to,
|
||||
urlState.datasource,
|
||||
...urlState.queries.map(q => q.query),
|
||||
]);
|
||||
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
|
||||
}
|
||||
return JSON.stringify(urlState);
|
||||
}
|
||||
|
||||
export function generateKey(index = 0): string {
|
||||
return `Q-${Date.now()}-${Math.random()}-${index}`;
|
||||
}
|
||||
|
||||
export function generateRefId(index = 0): string {
|
||||
return `${index + 1}`;
|
||||
}
|
||||
|
||||
export function generateQueryKeys(index = 0): { refId: string; key: string } {
|
||||
return { refId: generateRefId(index), key: generateKey(index) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure at least one target exists and that targets have the necessary keys
|
||||
*/
|
||||
export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
|
||||
if (queries && typeof queries === 'object' && queries.length > 0) {
|
||||
return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) }));
|
||||
}
|
||||
return [{ ...generateQueryKeys() }];
|
||||
}
|
||||
|
||||
/**
|
||||
* A target is non-empty when it has keys other than refId and key.
|
||||
*/
|
||||
export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
|
||||
return queries.some(query => Object.keys(query).length > 2);
|
||||
}
|
||||
|
||||
export function getIntervals(
|
||||
range: RawTimeRange,
|
||||
datasource,
|
||||
resolution: number
|
||||
): { interval: string; intervalMs: number } {
|
||||
if (!datasource || !resolution) {
|
||||
return { interval: '1s', intervalMs: 1000 };
|
||||
}
|
||||
const absoluteRange: RawTimeRange = {
|
||||
from: parseDate(range.from, false),
|
||||
to: parseDate(range.to, true),
|
||||
};
|
||||
return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
|
||||
}
|
||||
|
||||
export function makeTimeSeriesList(dataList) {
|
||||
return dataList.map((seriesData, index) => {
|
||||
const datapoints = seriesData.datapoints || [];
|
||||
const alias = seriesData.target;
|
||||
const colorIndex = index % colors.length;
|
||||
const color = colors[colorIndex];
|
||||
|
||||
const series = new TimeSeries({
|
||||
datapoints,
|
||||
alias,
|
||||
color,
|
||||
unit: seriesData.unit,
|
||||
});
|
||||
|
||||
return series;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the query history. Side-effect: store history in local storage
|
||||
*/
|
||||
export function updateHistory(history: HistoryItem[], datasourceId: string, queries: DataQuery[]): HistoryItem[] {
|
||||
const ts = Date.now();
|
||||
queries.forEach(query => {
|
||||
history = [{ query, ts }, ...history];
|
||||
});
|
||||
|
||||
if (history.length > MAX_HISTORY_ITEMS) {
|
||||
history = history.slice(0, MAX_HISTORY_ITEMS);
|
||||
}
|
||||
|
||||
// Combine all queries of a datasource type into one history
|
||||
const historyKey = `grafana.explore.history.${datasourceId}`;
|
||||
store.setObject(historyKey, history);
|
||||
return history;
|
||||
}
|
||||
|
||||
export function clearHistory(datasourceId: string) {
|
||||
const historyKey = `grafana.explore.history.${datasourceId}`;
|
||||
store.delete(historyKey);
|
||||
}
|
||||
|
@ -585,8 +585,8 @@ kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms');
|
||||
kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs');
|
||||
kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm');
|
||||
kbn.valueFormats.litreh = kbn.formatBuilders.fixedUnit('l/h');
|
||||
kbn.valueFormats.flowlpm = kbn.formatBuilders.decimalSIPrefix('L');
|
||||
kbn.valueFormats.flowmlpm = kbn.formatBuilders.decimalSIPrefix('L', -1);
|
||||
kbn.valueFormats.flowlpm = kbn.formatBuilders.decimalSIPrefix('l/min');
|
||||
kbn.valueFormats.flowmlpm = kbn.formatBuilders.decimalSIPrefix('mL/min', -1);
|
||||
|
||||
// Angle
|
||||
kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°');
|
||||
|
@ -159,3 +159,12 @@ export function describeTimeRange(range: RawTimeRange): string {
|
||||
|
||||
return range.from.toString() + ' to ' + range.to.toString();
|
||||
}
|
||||
|
||||
export const isValidTimeSpan = (value: string) => {
|
||||
if (value.indexOf('$') === 0 || value.indexOf('+$') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const info = describeTextRange(value);
|
||||
return info.invalid !== true;
|
||||
};
|
||||
|
16
public/app/core/utils/validate.ts
Normal file
16
public/app/core/utils/validate.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ValidationRule, ValidationEvents } from 'app/types';
|
||||
import { EventsWithValidation } from 'app/core/components/Form/Input';
|
||||
|
||||
export const validate = (value: string, validationRules: ValidationRule[]) => {
|
||||
const errors = validationRules.reduce((acc, currRule) => {
|
||||
if (!currRule.rule(value)) {
|
||||
return acc.concat(currRule.errorMessage);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
return errors.length > 0 ? errors : null;
|
||||
};
|
||||
|
||||
export const hasValidationEvent = (event: EventsWithValidation, validationEvents: ValidationEvents) => {
|
||||
return validationEvents && validationEvents[event];
|
||||
};
|
@ -29,6 +29,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
|
||||
{ text: 'Alerting', value: 'alerting' },
|
||||
{ text: 'No Data', value: 'no_data' },
|
||||
{ text: 'Paused', value: 'paused' },
|
||||
{ text: 'Pending', value: 'pending' },
|
||||
];
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { ThresholdMapper } from './state/ThresholdMapper';
|
||||
import { QueryPart } from 'app/core/components/query_part/query_part';
|
||||
import alertDef from './state/alertDef';
|
||||
@ -169,6 +170,7 @@ export class AlertTabCtrl {
|
||||
alert.frequency = alert.frequency || '1m';
|
||||
alert.handler = alert.handler || 1;
|
||||
alert.notifications = alert.notifications || [];
|
||||
alert.for = alert.for || '0m';
|
||||
|
||||
const defaultName = this.panel.title + ' alert';
|
||||
alert.name = alert.name || defaultName;
|
||||
@ -217,7 +219,7 @@ export class AlertTabCtrl {
|
||||
buildDefaultCondition() {
|
||||
return {
|
||||
type: 'query',
|
||||
query: { params: ['A', '15m', 'now'] },
|
||||
query: { params: ['A', '5m', 'now'] },
|
||||
reducer: { type: 'avg', params: [] },
|
||||
evaluator: { type: 'gt', params: [null] },
|
||||
operator: { type: 'and' },
|
||||
@ -354,6 +356,7 @@ export class AlertTabCtrl {
|
||||
enable() {
|
||||
this.panel.alert = {};
|
||||
this.initModel();
|
||||
this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes
|
||||
}
|
||||
|
||||
evaluatorParamsChanged() {
|
||||
@ -428,3 +431,5 @@ export function alertTab() {
|
||||
controller: AlertTabCtrl,
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('alertTab', alertTab);
|
||||
|
@ -81,6 +81,12 @@ exports[`Render should render alert rules 1`] = `
|
||||
>
|
||||
Paused
|
||||
</option>
|
||||
<option
|
||||
key="pending"
|
||||
value="pending"
|
||||
>
|
||||
Pending
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -230,6 +236,12 @@ exports[`Render should render component 1`] = `
|
||||
>
|
||||
Paused
|
||||
</option>
|
||||
<option
|
||||
key="pending"
|
||||
value="pending"
|
||||
>
|
||||
Pending
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,147 +1,159 @@
|
||||
<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert">
|
||||
<aside class="edit-sidemenu-aside">
|
||||
<ul class="edit-sidemenu">
|
||||
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
||||
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 1}">
|
||||
<a ng-click="ctrl.changeTabIndex(1)">
|
||||
Notifications <span class="muted">({{ctrl.alertNotifications.length}})</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
|
||||
</li>
|
||||
<aside class="edit-sidemenu-aside">
|
||||
<ul class="edit-sidemenu">
|
||||
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
||||
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 1}">
|
||||
<a ng-click="ctrl.changeTabIndex(1)">
|
||||
Notifications <span class="muted">({{ctrl.alertNotifications.length}})</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="ctrl.delete()">Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<a ng-click="ctrl.delete()">Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<div class="edit-tab-content">
|
||||
<div ng-if="ctrl.subTabIndex === 0">
|
||||
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
|
||||
<i class="fa fa-warning"></i> {{ctrl.error}}
|
||||
</div>
|
||||
<div class="edit-tab-content">
|
||||
<div ng-if="ctrl.subTabIndex === 0">
|
||||
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
|
||||
<i class="fa fa-warning"></i> {{ctrl.error}}
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Alert Config</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
|
||||
<span class="gf-form-label">Evaluate every</span>
|
||||
<input class="gf-form-input max-width-5" type="text" ng-model="ctrl.alert.frequency"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Alert Config</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Evaluate every</span>
|
||||
<input class="gf-form-input max-width-6" type="text" ng-model="ctrl.alert.frequency">
|
||||
</div>
|
||||
<div class="gf-form max-width-11">
|
||||
<label class="gf-form-label width-5">For</label>
|
||||
<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for" spellcheck='false' placeholder="5m">
|
||||
<info-popover mode="right-absolute">
|
||||
If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending.
|
||||
Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Conditions</h5>
|
||||
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
|
||||
<div class="gf-form">
|
||||
<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
|
||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part width-9" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
||||
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Conditions</h5>
|
||||
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
|
||||
<div class="gf-form">
|
||||
<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
|
||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part width-9" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()">
|
||||
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label dropdown">
|
||||
<a class="pointer dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
|
||||
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label dropdown">
|
||||
<a class="pointer dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
|
||||
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If no data or all values are null</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If no data or all values are null</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If execution error or timeout</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState" ng-options="f.value as f.text for f in ctrl.executionErrorModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If execution error or timeout</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState" ng-options="f.value as f.text for f in ctrl.executionErrorModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
||||
Test Rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
||||
Test Rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.testing">
|
||||
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.testing">
|
||||
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.testResult">
|
||||
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.testResult">
|
||||
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.subTabIndex === 1">
|
||||
<h5 class="section-heading">Notifications</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Send to</span>
|
||||
<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications" ng-style="{'background-color': nc.bgColor }">
|
||||
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
||||
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
|
||||
</span>
|
||||
<metric-segment segment="ctrl.addNotificationSegment" get-options="ctrl.getNotifications()" on-change="ctrl.notificationAdded()"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-8">Message</span>
|
||||
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.subTabIndex === 1">
|
||||
<h5 class="section-heading">Notifications</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Send to</span>
|
||||
<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications" ng-style="{'background-color': nc.bgColor }">
|
||||
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
||||
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
|
||||
</span>
|
||||
<metric-segment segment="ctrl.addNotificationSegment" get-options="ctrl.getNotifications()" on-change="ctrl.notificationAdded()"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-8">Message</span>
|
||||
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
|
||||
<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i> Clear history</button>
|
||||
<h5 class="section-heading" style="whitespace: nowrap">
|
||||
State history <span class="muted small">(last 50 state changes)</span>
|
||||
</h5>
|
||||
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
|
||||
<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i> Clear history</button>
|
||||
<h5 class="section-heading" style="whitespace: nowrap">
|
||||
State history <span class="muted small">(last 50 state changes)</span>
|
||||
</h5>
|
||||
|
||||
<div ng-show="ctrl.alertHistory.length === 0">
|
||||
<br>
|
||||
<i>No state changes recorded</i>
|
||||
</div>
|
||||
<div ng-show="ctrl.alertHistory.length === 0">
|
||||
<br>
|
||||
<i>No state changes recorded</i>
|
||||
</div>
|
||||
|
||||
<ol class="alert-rule-list" >
|
||||
<li class="alert-rule-item" ng-repeat="al in ctrl.alertHistory">
|
||||
|
@ -8,9 +8,9 @@ const alertQueryDef = new QueryPartDef({
|
||||
{
|
||||
name: 'from',
|
||||
type: 'string',
|
||||
options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h', '24h', '48h'],
|
||||
options: ['10s', '1m', '5m', '10m', '15m', '1h', '24h', '48h'],
|
||||
},
|
||||
{ name: 'to', type: 'string', options: ['now'] },
|
||||
{ name: 'to', type: 'string', options: ['now', 'now-1m', 'now-5m', 'now-10m', 'now-1h'] },
|
||||
],
|
||||
defaultParams: ['#A', '15m', 'now', 'avg'],
|
||||
});
|
||||
@ -99,6 +99,13 @@ function getStateDisplayModel(state) {
|
||||
stateClass: 'alert-state-warning',
|
||||
};
|
||||
}
|
||||
case 'unknown': {
|
||||
return {
|
||||
text: 'UNKNOWN',
|
||||
iconClass: 'fa fa-question',
|
||||
stateClass: 'alert-state-paused',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw { message: 'Unknown alert state' };
|
||||
|
@ -32,7 +32,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
|
||||
if (event.alertId) {
|
||||
const stateModel = alertDef.getStateDisplayModel(event.newState);
|
||||
titleStateClass = stateModel.stateClass;
|
||||
title = `<i class="icon-gf ${stateModel.iconClass}"></i> ${stateModel.text}`;
|
||||
title = `<i class="${stateModel.iconClass}"></i> ${stateModel.text}`;
|
||||
text = alertDef.getAlertAnnotationInfo(event);
|
||||
if (event.text) {
|
||||
text = text + '<br />' + event.text;
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
OK_COLOR,
|
||||
ALERTING_COLOR,
|
||||
NO_DATA_COLOR,
|
||||
PENDING_COLOR,
|
||||
DEFAULT_ANNOTATION_COLOR,
|
||||
REGION_FILL_ALPHA,
|
||||
} from 'app/core/utils/colors';
|
||||
@ -71,6 +72,11 @@ export class EventManager {
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
$__pending: {
|
||||
color: PENDING_COLOR,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
$__editing: {
|
||||
color: DEFAULT_ANNOTATION_COLOR,
|
||||
position: 'BOTTOM',
|
||||
|
@ -77,6 +77,10 @@ export class DashboardSrv {
|
||||
postSave(clone, data) {
|
||||
this.dash.version = data.version;
|
||||
|
||||
// important that these happens before location redirect below
|
||||
this.$rootScope.appEvent('dashboard-saved', this.dash);
|
||||
this.$rootScope.appEvent('alert-success', ['Dashboard saved']);
|
||||
|
||||
const newUrl = locationUtil.stripBaseFromUrl(data.url);
|
||||
const currentPath = this.$location.path();
|
||||
|
||||
@ -84,9 +88,6 @@ export class DashboardSrv {
|
||||
this.$location.url(newUrl).replace();
|
||||
}
|
||||
|
||||
this.$rootScope.appEvent('dashboard-saved', this.dash);
|
||||
this.$rootScope.appEvent('alert-success', ['Dashboard saved']);
|
||||
|
||||
return this.dash;
|
||||
}
|
||||
|
||||
|
72
public/app/features/dashboard/dashgrid/AlertTab.tsx
Normal file
72
public/app/features/dashboard/dashgrid/AlertTab.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
import { EditorTabBody } from './EditorTabBody';
|
||||
import 'app/features/alerting/AlertTabCtrl';
|
||||
|
||||
interface Props {
|
||||
angularPanel?: AngularComponent;
|
||||
}
|
||||
|
||||
export class AlertTab extends PureComponent<Props> {
|
||||
element: any;
|
||||
component: AngularComponent;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.shouldLoadAlertTab()) {
|
||||
this.loadAlertTab();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (this.shouldLoadAlertTab()) {
|
||||
this.loadAlertTab();
|
||||
}
|
||||
}
|
||||
|
||||
shouldLoadAlertTab() {
|
||||
return this.props.angularPanel && this.element;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.component) {
|
||||
this.component.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
loadAlertTab() {
|
||||
const { angularPanel } = this.props;
|
||||
|
||||
const scope = angularPanel.getScope();
|
||||
|
||||
// When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet
|
||||
if (!scope.$$childHead) {
|
||||
setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const panelCtrl = scope.$$childHead.ctrl;
|
||||
const loader = getAngularLoader();
|
||||
const template = '<alert-tab />';
|
||||
|
||||
const scopeProps = {
|
||||
ctrl: panelCtrl,
|
||||
};
|
||||
|
||||
this.component = loader.load(this.element, scopeProps, template);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditorTabBody heading="Alert" toolbarItems={[]}>
|
||||
<div ref={element => (this.element = element)} />
|
||||
</EditorTabBody>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import ReactGridLayout from 'react-grid-layout';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
import { DashboardPanel } from './DashboardPanel';
|
||||
@ -176,10 +177,9 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
|
||||
renderPanels() {
|
||||
const panelElements = [];
|
||||
console.log('render panels');
|
||||
|
||||
for (const panel of this.props.dashboard.panels) {
|
||||
const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen });
|
||||
const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.fullscreen });
|
||||
panelElements.push(
|
||||
<div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id}`}>
|
||||
<DashboardPanel
|
||||
@ -214,3 +214,5 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(module)(DashboardGrid);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
import { DashboardGrid } from './DashboardGrid';
|
||||
import DashboardGrid from './DashboardGrid';
|
||||
|
||||
react2AngularDirective('dashboardGrid', DashboardGrid, [['dashboard', { watchDepth: 'reference' }]]);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user