mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/master' into noop-services-poc
This commit is contained in:
commit
4d719b0d05
@ -83,13 +83,14 @@ jobs:
|
||||
- checkout
|
||||
- run: 'go get -u github.com/alecthomas/gometalinter'
|
||||
- run: 'go get -u github.com/tsenart/deadcode'
|
||||
- run: 'go get -u github.com/jgautheron/goconst/cmd/goconst'
|
||||
- run: 'go get -u github.com/gordonklaus/ineffassign'
|
||||
- run: 'go get -u github.com/opennota/check/cmd/structcheck'
|
||||
- run: 'go get -u github.com/mdempsky/unconvert'
|
||||
- run: 'go get -u github.com/opennota/check/cmd/varcheck'
|
||||
- run:
|
||||
name: run linters
|
||||
command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
|
||||
command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=goconst --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
|
||||
- run:
|
||||
name: run go vet
|
||||
command: 'go vet ./pkg/...'
|
||||
@ -157,14 +158,18 @@ jobs:
|
||||
name: sha-sum packages
|
||||
command: 'go run build.go sha-dist'
|
||||
- run:
|
||||
name: Build Grafana.com publisher
|
||||
name: Build Grafana.com master publisher
|
||||
command: 'go build -o scripts/publish scripts/build/publish.go'
|
||||
- run:
|
||||
name: Build Grafana.com release publisher
|
||||
command: 'cd scripts/build/release_publisher && go build -o release_publisher .'
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- dist/grafana*
|
||||
- scripts/*.sh
|
||||
- scripts/publish
|
||||
- scripts/build/release_publisher/release_publisher
|
||||
|
||||
build:
|
||||
docker:
|
||||
@ -298,8 +303,8 @@ jobs:
|
||||
name: deploy to s3
|
||||
command: 'aws s3 sync ./dist s3://$BUCKET_NAME/release'
|
||||
- run:
|
||||
name: Trigger Windows build
|
||||
command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release'
|
||||
name: Deploy to Grafana.com
|
||||
command: './scripts/build/publish.sh'
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
22
.github/CONTRIBUTING.md
vendored
22
.github/CONTRIBUTING.md
vendored
@ -1,22 +0,0 @@
|
||||
Follow the setup guide in README.md
|
||||
|
||||
### Rebuild frontend assets on source change
|
||||
```
|
||||
yarn watch
|
||||
```
|
||||
|
||||
### Rerun tests on source change
|
||||
```
|
||||
yarn jest
|
||||
```
|
||||
|
||||
### Run tests for backend assets before commit
|
||||
```
|
||||
test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)' | tee /dev/stderr)"
|
||||
```
|
||||
|
||||
### Run tests for frontend assets before commit
|
||||
```
|
||||
yarn test
|
||||
go test -v ./pkg/...
|
||||
```
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -73,3 +73,5 @@ debug.test
|
||||
|
||||
/devenv/bulk-dashboards/*.json
|
||||
/devenv/bulk_alerting_dashboards/*.json
|
||||
|
||||
/scripts/build/release_publisher/release_publisher
|
||||
|
25
CHANGELOG.md
25
CHANGELOG.md
@ -1,23 +1,38 @@
|
||||
# 5.4.0 (unreleased)
|
||||
|
||||
### Minor
|
||||
|
||||
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
|
||||
|
||||
# 5.3.0 (unreleased)
|
||||
|
||||
# 5.3.0-beta3 (2018-10-03)
|
||||
|
||||
* **Stackdriver**: Fix for missing ngInject [#13511](https://github.com/grafana/grafana/pull/13511)
|
||||
* **Permissions**: Fix for broken permissions selector [#13507](https://github.com/grafana/grafana/issues/13507)
|
||||
* **Alerting**: Alert reminders deduping not working as expected when running multiple Grafana instances [#13492](https://github.com/grafana/grafana/issues/13492)
|
||||
|
||||
# 5.3.0-beta2 (2018-10-01)
|
||||
|
||||
### New Features
|
||||
|
||||
* **Annotations**: Enable template variables in tagged annotations queries [#9735](https://github.com/grafana/grafana/issues/9735)
|
||||
* **Stackdriver**: Support for Google Stackdriver Datasource [#13289](https://github.com/grafana/grafana/pull/13289)
|
||||
|
||||
### Minor
|
||||
|
||||
* **Provisioning**: Dashboard Provisioning now support symlinks that changes target [#12534](https://github.com/grafana/grafana/issues/12534), thx [@auhlig](https://github.com/auhlig)
|
||||
* **OAuth**: Allow oauth email attribute name to be configurable [#12986](https://github.com/grafana/grafana/issues/12986), thx [@bobmshannon](https://github.com/bobmshannon)
|
||||
* **Tags**: Default sort order for GetDashboardTags [#11681](https://github.com/grafana/grafana/pull/11681), thx [@Jonnymcc](https://github.com/Jonnymcc)
|
||||
* **Prometheus**: Label completion queries respect dashboard time range [#12251](https://github.com/grafana/grafana/pull/12251), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Prometheus**: Allow to display annotations based on Prometheus series value [#10159](https://github.com/grafana/grafana/issues/10159), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Prometheus**: Adhoc-filtering for Prometheus dashboards [#13212](https://github.com/grafana/grafana/issues/13212)
|
||||
* **Singlestat**: Fix gauge display accuracy for percents [#13270](https://github.com/grafana/grafana/issues/13270), thx [@tianon](https://github.com/tianon)
|
||||
|
||||
# 5.3.0 (unreleased)
|
||||
|
||||
### Minor
|
||||
|
||||
* **Dashboard**: Prevent auto refresh from starting when loading dashboard with absolute time range [#12030](https://github.com/grafana/grafana/issues/12030)
|
||||
* **Templating**: New templating variable type `Text box` that allows free text input [#3173](https://github.com/grafana/grafana/issues/3173)
|
||||
* **Alerting**: Link to view full size image in Microsoft Teams alert notifier [#13121](https://github.com/grafana/grafana/issues/13121), thx [@holiiveira](https://github.com/holiiveira)
|
||||
* **Alerting**: Fixes a bug where all alerts would send reminders after upgrade & restart [#13402](https://github.com/grafana/grafana/pull/13402)
|
||||
* **Alerting**: Concurrent render limit for graphs used in notifications [#13401](https://github.com/grafana/grafana/pull/13401)
|
||||
* **Postgres/MySQL/MSSQL**: Add support for replacing $__interval and $__interval_ms in alert queries [#11555](https://github.com/grafana/grafana/issues/11555), thx [@svenklemm](https://github.com/svenklemm)
|
||||
|
||||
# 5.3.0-beta1 (2018-09-06)
|
||||
|
56
CONTRIBUTING.md
Normal file
56
CONTRIBUTING.md
Normal file
@ -0,0 +1,56 @@
|
||||
|
||||
# Contributing
|
||||
|
||||
Grafana uses GitHub to manage contributions.
|
||||
Contributions take the form of pull requests that will be reviewed by the core team.
|
||||
|
||||
* If you are a new contributor see: [Steps to Contribute](#steps-to-contribute)
|
||||
|
||||
* If you have a trivial fix or improvement, go ahead and create a pull request.
|
||||
|
||||
* If you plan to do something more involved, discuss your idea on the respective [issue](https://github.com/grafana/grafana/issues) or create a [new issue](https://github.com/grafana/grafana/issues/new) if it does not exist. This will avoid unnecessary work and surely give you and us a good deal of inspiration.
|
||||
|
||||
|
||||
## Steps to Contribute
|
||||
|
||||
Should you wish to work on a GitHub issue, check first if it is not already assigned to someone. If it is free, you claim it by commenting on the issue that you want to work on it. This is to prevent duplicated efforts from contributors on the same issue.
|
||||
|
||||
Please check the [`beginner friendly`](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) label to find issues that are good for getting started. If you have questions about one of the issues, with or without the tag, please comment on them and one of the core team or the original poster will clarify it.
|
||||
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
Follow the setup guide in README.md
|
||||
|
||||
### Rebuild frontend assets on source change
|
||||
```
|
||||
yarn watch
|
||||
```
|
||||
|
||||
### Rerun tests on source change
|
||||
```
|
||||
yarn jest
|
||||
```
|
||||
|
||||
### Run tests for backend assets before commit
|
||||
```
|
||||
test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)' | tee /dev/stderr)"
|
||||
```
|
||||
|
||||
### Run tests for frontend assets before commit
|
||||
```
|
||||
yarn test
|
||||
go test -v ./pkg/...
|
||||
```
|
||||
|
||||
|
||||
## Pull Request Checklist
|
||||
|
||||
* Branch from the master branch and, if needed, rebase to the current master branch before submitting your pull request. If it doesn't merge cleanly with master you may be asked to rebase your changes.
|
||||
|
||||
* Commits should be as small as possible, while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests).
|
||||
|
||||
* If your patch is not getting reviewed or you need a specific person to review it, you can @-reply a reviewer asking for a review in the pull request or a comment.
|
||||
|
||||
* Add tests relevant to the fixed bug or new feature.
|
8
Gopkg.lock
generated
8
Gopkg.lock
generated
@ -19,6 +19,12 @@
|
||||
packages = ["."]
|
||||
revision = "7677a1d7c1137cd3dd5ba7a076d0c898a1ef4520"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/VividCortex/mysqlerr"
|
||||
packages = ["."]
|
||||
revision = "6c6b55f8796f578c870b7e19bafb16103bc40095"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/aws/aws-sdk-go"
|
||||
packages = [
|
||||
@ -673,6 +679,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "81a37e747b875cf870c1b9486fa3147e704dea7db8ba86f7cb942d3ddc01d3e3"
|
||||
inputs-digest = "6e9458f912a5f0eb3430b968f1b4dbc4e3b7671b282cf4fe1573419a6d9ba0d4"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
@ -203,3 +203,7 @@ ignored = [
|
||||
[[constraint]]
|
||||
name = "github.com/denisenkom/go-mssqldb"
|
||||
revision = "270bc3860bb94dd3a3ffd047377d746c5e276726"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/VividCortex/mysqlerr"
|
||||
branch = "master"
|
||||
|
@ -6,8 +6,8 @@ upgrading Grafana please check here before creating an issue.
|
||||
|
||||
## Links
|
||||
|
||||
- [Datasource plugin written in typescript](https://github.com/grafana/typescript-template-datasource)
|
||||
- [Simple json dataource plugin](https://github.com/grafana/simple-json-datasource)
|
||||
- [Datasource plugin written in TypeScript](https://github.com/grafana/typescript-template-datasource)
|
||||
- [Simple JSON datasource plugin](https://github.com/grafana/simple-json-datasource)
|
||||
- [Plugin development guide](http://docs.grafana.org/plugins/developing/development/)
|
||||
- [Webpack Grafana plugin template project](https://github.com/CorpGlory/grafana-plugin-template-webpack)
|
||||
|
||||
|
17
build.go
17
build.go
@ -22,6 +22,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
windows = "windows"
|
||||
linux = "linux"
|
||||
)
|
||||
|
||||
var (
|
||||
//versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
|
||||
goarch string
|
||||
@ -110,13 +115,13 @@ func main() {
|
||||
case "package":
|
||||
grunt(gruntBuildArg("build")...)
|
||||
grunt(gruntBuildArg("package")...)
|
||||
if goos == "linux" {
|
||||
if goos == linux {
|
||||
createLinuxPackages()
|
||||
}
|
||||
|
||||
case "package-only":
|
||||
grunt(gruntBuildArg("package")...)
|
||||
if goos == "linux" {
|
||||
if goos == linux {
|
||||
createLinuxPackages()
|
||||
}
|
||||
|
||||
@ -378,7 +383,7 @@ func ensureGoPath() {
|
||||
}
|
||||
|
||||
func grunt(params ...string) {
|
||||
if runtime.GOOS == "windows" {
|
||||
if runtime.GOOS == windows {
|
||||
runPrint(`.\node_modules\.bin\grunt`, params...)
|
||||
} else {
|
||||
runPrint("./node_modules/.bin/grunt", params...)
|
||||
@ -420,7 +425,7 @@ func build(binaryName, pkg string, tags []string) {
|
||||
binary = fmt.Sprintf("./bin/%s", binaryName)
|
||||
}
|
||||
|
||||
if goos == "windows" {
|
||||
if goos == windows {
|
||||
binary += ".exe"
|
||||
}
|
||||
|
||||
@ -484,11 +489,11 @@ func clean() {
|
||||
|
||||
func setBuildEnv() {
|
||||
os.Setenv("GOOS", goos)
|
||||
if goos == "windows" {
|
||||
if goos == windows {
|
||||
// require windows >=7
|
||||
os.Setenv("CGO_CFLAGS", "-D_WIN32_WINNT=0x0601")
|
||||
}
|
||||
if goarch != "amd64" || goos != "linux" {
|
||||
if goarch != "amd64" || goos != linux {
|
||||
// needed for all other archs
|
||||
cgo = true
|
||||
}
|
||||
|
@ -474,6 +474,10 @@ error_or_timeout = alerting
|
||||
# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
|
||||
nodata_or_nullvalues = no_data
|
||||
|
||||
# Alert notifications can include images, but rendering many images at the same time can overload the server
|
||||
# This limit will protect the server from render overloading and make sure notifications are sent out quickly
|
||||
concurrent_render_limit = 5
|
||||
|
||||
#################################### Explore #############################
|
||||
[explore]
|
||||
# Enable the Explore section
|
||||
|
@ -393,6 +393,10 @@ log_queries =
|
||||
# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
|
||||
;nodata_or_nullvalues = no_data
|
||||
|
||||
# Alert notifications can include images, but rendering many images at the same time can overload the server
|
||||
# This limit will protect the server from render overloading and make sure notifications are sent out quickly
|
||||
;concurrent_render_limit = 5
|
||||
|
||||
#################################### Explore #############################
|
||||
[explore]
|
||||
# Enable the Explore section
|
||||
@ -431,7 +435,7 @@ log_queries =
|
||||
;sampler_param = 1
|
||||
|
||||
#################################### Grafana.com integration ##########################
|
||||
# Url used to to import dashboards directly from Grafana.com
|
||||
# Url used to import dashboards directly from Grafana.com
|
||||
[grafana_com]
|
||||
;url = https://grafana.com
|
||||
|
||||
|
1166
devenv/dev-dashboards/panel_tests_slow_queries_and_annotations.json
Normal file
1166
devenv/dev-dashboards/panel_tests_slow_queries_and_annotations.json
Normal file
File diff suppressed because it is too large
Load Diff
1
devenv/docker/ha_test/.gitignore
vendored
Normal file
1
devenv/docker/ha_test/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
grafana/provisioning/dashboards/alerts/alert-*
|
137
devenv/docker/ha_test/README.md
Normal file
137
devenv/docker/ha_test/README.md
Normal file
@ -0,0 +1,137 @@
|
||||
# Grafana High Availability (HA) test setup
|
||||
|
||||
A set of docker compose services which together creates a Grafana HA test setup with capability of easily
|
||||
scaling up/down number of Grafana instances.
|
||||
|
||||
Included services
|
||||
|
||||
* Grafana
|
||||
* Mysql - Grafana configuration database and session storage
|
||||
* Prometheus - Monitoring of Grafana and used as datasource of provisioned alert rules
|
||||
* Nginx - Reverse proxy for Grafana and Prometheus. Enables browsing Grafana/Prometheus UI using a hostname
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Build grafana docker container
|
||||
|
||||
Build a Grafana docker container from current branch and commit and tag it as grafana/grafana:dev.
|
||||
|
||||
```bash
|
||||
$ cd <grafana repo>
|
||||
$ make build-docker-full
|
||||
```
|
||||
|
||||
### Virtual host names
|
||||
|
||||
#### Alternative 1 - Use dnsmasq
|
||||
|
||||
```bash
|
||||
$ sudo apt-get install dnsmasq
|
||||
$ echo 'address=/loc/127.0.0.1' | sudo tee /etc/dnsmasq.d/dnsmasq-loc.conf > /dev/null
|
||||
$ sudo /etc/init.d/dnsmasq restart
|
||||
$ ping whatever.loc
|
||||
PING whatever.loc (127.0.0.1) 56(84) bytes of data.
|
||||
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.076 ms
|
||||
--- whatever.loc ping statistics ---
|
||||
1 packet transmitted, 1 received, 0% packet loss, time 1998ms
|
||||
```
|
||||
|
||||
#### Alternative 2 - Manually update /etc/hosts
|
||||
|
||||
Update your `/etc/hosts` to be able to access Grafana and/or Prometheus UI using a hostname.
|
||||
|
||||
```bash
|
||||
$ cat /etc/hosts
|
||||
127.0.0.1 grafana.loc
|
||||
127.0.0.1 prometheus.loc
|
||||
```
|
||||
|
||||
## Start services
|
||||
|
||||
```bash
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||
Browse
|
||||
* http://grafana.loc/
|
||||
* http://prometheus.loc/
|
||||
|
||||
Check for any errors
|
||||
|
||||
```bash
|
||||
$ docker-compose logs | grep error
|
||||
```
|
||||
|
||||
### Scale Grafana instances up/down
|
||||
|
||||
Scale number of Grafana instances to `<instances>`
|
||||
|
||||
```bash
|
||||
$ docker-compose up --scale grafana=<instances> -d
|
||||
# for example 3 instances
|
||||
$ docker-compose up --scale grafana=3 -d
|
||||
```
|
||||
|
||||
## Test alerting
|
||||
|
||||
### Create notification channels
|
||||
|
||||
Creates default notification channels, if not already exists
|
||||
|
||||
```bash
|
||||
$ ./alerts.sh setup
|
||||
```
|
||||
|
||||
### Slack notifications
|
||||
|
||||
Disable
|
||||
|
||||
```bash
|
||||
$ ./alerts.sh slack -d
|
||||
```
|
||||
|
||||
Enable and configure url
|
||||
|
||||
```bash
|
||||
$ ./alerts.sh slack -u https://hooks.slack.com/services/...
|
||||
```
|
||||
|
||||
Enable, configure url and enable reminders
|
||||
|
||||
```bash
|
||||
$ ./alerts.sh slack -u https://hooks.slack.com/services/... -r -e 10m
|
||||
```
|
||||
|
||||
### Provision alert dashboards with alert rules
|
||||
|
||||
Provision 1 dashboard/alert rule (default)
|
||||
|
||||
```bash
|
||||
$ ./alerts.sh provision
|
||||
```
|
||||
|
||||
Provision 10 dashboards/alert rules
|
||||
|
||||
```bash
|
||||
$ ./alerts.sh provision -a 10
|
||||
```
|
||||
|
||||
Provision 10 dashboards/alert rules and change condition to `gt > 100`
|
||||
|
||||
```bash
|
||||
$ ./alerts.sh provision -a 10 -c 100
|
||||
```
|
||||
|
||||
### Pause/unpause all alert rules
|
||||
|
||||
Pause
|
||||
|
||||
```bash
|
||||
$ ./alerts.sh pause
|
||||
```
|
||||
|
||||
Unpause
|
||||
|
||||
```bash
|
||||
$ ./alerts.sh unpause
|
||||
```
|
156
devenv/docker/ha_test/alerts.sh
Executable file
156
devenv/docker/ha_test/alerts.sh
Executable file
@ -0,0 +1,156 @@
|
||||
#!/bin/bash
|
||||
|
||||
requiresJsonnet() {
|
||||
if ! type "jsonnet" > /dev/null; then
|
||||
echo "you need you install jsonnet to run this script"
|
||||
echo "follow the instructions on https://github.com/google/jsonnet"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
setup() {
|
||||
STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://admin:admin@grafana.loc/api/alert-notifications/1)
|
||||
if [ $STATUS -eq 200 ]; then
|
||||
echo "Email already exists, skipping..."
|
||||
else
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Email",
|
||||
"type": "email",
|
||||
"isDefault": false,
|
||||
"sendReminder": false,
|
||||
"uploadImage": true,
|
||||
"settings": {
|
||||
"addresses": "user@test.com"
|
||||
}
|
||||
}' \
|
||||
http://admin:admin@grafana.loc/api/alert-notifications
|
||||
fi
|
||||
|
||||
STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://admin:admin@grafana.loc/api/alert-notifications/2)
|
||||
if [ $STATUS -eq 200 ]; then
|
||||
echo "Slack already exists, skipping..."
|
||||
else
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Slack",
|
||||
"type": "slack",
|
||||
"isDefault": false,
|
||||
"sendReminder": false,
|
||||
"uploadImage": true
|
||||
}' \
|
||||
http://admin:admin@grafana.loc/api/alert-notifications
|
||||
fi
|
||||
}
|
||||
|
||||
slack() {
|
||||
enabled=true
|
||||
url=''
|
||||
remind=false
|
||||
remindEvery='10m'
|
||||
|
||||
while getopts ":e:u:dr" o; do
|
||||
case "${o}" in
|
||||
e)
|
||||
remindEvery=${OPTARG}
|
||||
;;
|
||||
u)
|
||||
url=${OPTARG}
|
||||
;;
|
||||
d)
|
||||
enabled=false
|
||||
;;
|
||||
r)
|
||||
remind=true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
|
||||
curl -X PUT \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id": 2,
|
||||
"name": "Slack",
|
||||
"type": "slack",
|
||||
"isDefault": '$enabled',
|
||||
"sendReminder": '$remind',
|
||||
"frequency": "'$remindEvery'",
|
||||
"uploadImage": true,
|
||||
"settings": {
|
||||
"url": "'$url'"
|
||||
}
|
||||
}' \
|
||||
http://admin:admin@grafana.loc/api/alert-notifications/2
|
||||
}
|
||||
|
||||
provision() {
|
||||
alerts=1
|
||||
condition=65
|
||||
while getopts ":a:c:" o; do
|
||||
case "${o}" in
|
||||
a)
|
||||
alerts=${OPTARG}
|
||||
;;
|
||||
c)
|
||||
condition=${OPTARG}
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
|
||||
requiresJsonnet
|
||||
|
||||
rm -rf grafana/provisioning/dashboards/alerts/alert-*.json
|
||||
jsonnet -m grafana/provisioning/dashboards/alerts grafana/provisioning/alerts.jsonnet --ext-code alerts=$alerts --ext-code condition=$condition
|
||||
}
|
||||
|
||||
pause() {
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d '{"paused":true}' \
|
||||
http://admin:admin@grafana.loc/api/admin/pause-all-alerts
|
||||
}
|
||||
|
||||
unpause() {
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d '{"paused":false}' \
|
||||
http://admin:admin@grafana.loc/api/admin/pause-all-alerts
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo -e "Usage: ./alerts.sh COMMAND [OPTIONS]\n"
|
||||
echo -e "Commands"
|
||||
echo -e " setup\t\t creates default alert notification channels"
|
||||
echo -e " slack\t\t configure slack notification channel"
|
||||
echo -e " [-d]\t\t\t disable notifier, default enabled"
|
||||
echo -e " [-u]\t\t\t url"
|
||||
echo -e " [-r]\t\t\t send reminders"
|
||||
echo -e " [-e <remind every>]\t\t default 10m\n"
|
||||
echo -e " provision\t provision alerts"
|
||||
echo -e " [-a <alert rule count>]\t default 1"
|
||||
echo -e " [-c <condition value>]\t default 65\n"
|
||||
echo -e " pause\t\t pause all alerts"
|
||||
echo -e " unpause\t unpause all alerts"
|
||||
}
|
||||
|
||||
main() {
|
||||
local cmd=$1
|
||||
|
||||
if [[ $cmd == "setup" ]]; then
|
||||
setup
|
||||
elif [[ $cmd == "slack" ]]; then
|
||||
slack "${@:2}"
|
||||
elif [[ $cmd == "provision" ]]; then
|
||||
provision "${@:2}"
|
||||
elif [[ $cmd == "pause" ]]; then
|
||||
pause
|
||||
elif [[ $cmd == "unpause" ]]; then
|
||||
unpause
|
||||
fi
|
||||
|
||||
if [[ -z "$cmd" ]]; then
|
||||
usage
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
78
devenv/docker/ha_test/docker-compose.yaml
Normal file
78
devenv/docker/ha_test/docker-compose.yaml
Normal file
@ -0,0 +1,78 @@
|
||||
version: "2.1"
|
||||
|
||||
services:
|
||||
nginx-proxy:
|
||||
image: jwilder/nginx-proxy
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
|
||||
db:
|
||||
image: mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
MYSQL_DATABASE: grafana
|
||||
MYSQL_USER: grafana
|
||||
MYSQL_PASSWORD: password
|
||||
ports:
|
||||
- 3306
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
|
||||
# db:
|
||||
# image: postgres:9.3
|
||||
# environment:
|
||||
# POSTGRES_DATABASE: grafana
|
||||
# POSTGRES_USER: grafana
|
||||
# POSTGRES_PASSWORD: password
|
||||
# ports:
|
||||
# - 5432
|
||||
# healthcheck:
|
||||
# test: ["CMD-SHELL", "pg_isready -d grafana -U grafana"]
|
||||
# timeout: 10s
|
||||
# retries: 10
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:dev
|
||||
volumes:
|
||||
- ./grafana/provisioning/:/etc/grafana/provisioning/
|
||||
environment:
|
||||
- VIRTUAL_HOST=grafana.loc
|
||||
- GF_SERVER_ROOT_URL=http://grafana.loc
|
||||
- GF_DATABASE_NAME=grafana
|
||||
- GF_DATABASE_USER=grafana
|
||||
- GF_DATABASE_PASSWORD=password
|
||||
- GF_DATABASE_TYPE=mysql
|
||||
- GF_DATABASE_HOST=db:3306
|
||||
- GF_SESSION_PROVIDER=mysql
|
||||
- GF_SESSION_PROVIDER_CONFIG=grafana:password@tcp(db:3306)/grafana?allowNativePasswords=true
|
||||
# - GF_DATABASE_TYPE=postgres
|
||||
# - GF_DATABASE_HOST=db:5432
|
||||
# - GF_DATABASE_SSL_MODE=disable
|
||||
# - GF_SESSION_PROVIDER=postgres
|
||||
# - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
|
||||
- GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug
|
||||
ports:
|
||||
- 3000
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:v2.4.2
|
||||
volumes:
|
||||
- ./prometheus/:/etc/prometheus/
|
||||
environment:
|
||||
- VIRTUAL_HOST=prometheus.loc
|
||||
ports:
|
||||
- 9090
|
||||
|
||||
# mysqld-exporter:
|
||||
# image: prom/mysqld-exporter
|
||||
# environment:
|
||||
# - DATA_SOURCE_NAME=grafana:password@(mysql:3306)/
|
||||
# ports:
|
||||
# - 9104
|
202
devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet
Normal file
202
devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet
Normal file
@ -0,0 +1,202 @@
|
||||
local numAlerts = std.extVar('alerts');
|
||||
local condition = std.extVar('condition');
|
||||
local arr = std.range(1, numAlerts);
|
||||
|
||||
local alertDashboardTemplate = {
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
65
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"operator": {
|
||||
"type": "and"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"executionErrorState": "alerting",
|
||||
"frequency": "10s",
|
||||
"handler": 1,
|
||||
"name": "bulk alerting",
|
||||
"noDataState": "no_data",
|
||||
"notifications": [
|
||||
{
|
||||
"id": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"$$hashKey": "object:117",
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 50
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"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": "",
|
||||
"title": "New dashboard",
|
||||
"uid": null,
|
||||
"version": 0
|
||||
};
|
||||
|
||||
|
||||
{
|
||||
['alert-' + std.toString(x) + '.json']:
|
||||
alertDashboardTemplate + {
|
||||
panels: [
|
||||
alertDashboardTemplate.panels[0] +
|
||||
{
|
||||
alert+: {
|
||||
name: 'Alert rule ' + x,
|
||||
conditions: [
|
||||
alertDashboardTemplate.panels[0].alert.conditions[0] +
|
||||
{
|
||||
evaluator+: {
|
||||
params: [condition]
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
uid: 'alert-' + x,
|
||||
title: 'Alert ' + x
|
||||
},
|
||||
for x in arr
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'Alerts'
|
||||
folder: 'Alerts'
|
||||
type: file
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards/alerts
|
@ -0,0 +1,172 @@
|
||||
{
|
||||
"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": {
|
||||
"Active alerts": "#bf1b00"
|
||||
},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 12,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"interval": "",
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": false,
|
||||
"current": true,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"rightSide": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [
|
||||
{
|
||||
"alias": "Active grafana instances",
|
||||
"dashes": true,
|
||||
"fill": 0
|
||||
}
|
||||
],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(increase(grafana_alerting_notification_sent_total[1m])) by(job)",
|
||||
"format": "time_series",
|
||||
"instant": false,
|
||||
"interval": "1m",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "Notifications sent",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "min(grafana_alerting_active_alerts) without(instance)",
|
||||
"format": "time_series",
|
||||
"interval": "1m",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "Active alerts",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "count(up{job=\"grafana\"})",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "Active grafana instances",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Notifications sent vs active alerts",
|
||||
"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": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": 3
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"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": "",
|
||||
"title": "Overview",
|
||||
"uid": "xHy7-hAik",
|
||||
"version": 6
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
jsonData:
|
||||
timeInterval: 10s
|
||||
queryTimeout: 30s
|
||||
httpMethod: POST
|
39
devenv/docker/ha_test/prometheus/prometheus.yml
Normal file
39
devenv/docker/ha_test/prometheus/prometheus.yml
Normal file
@ -0,0 +1,39 @@
|
||||
# my global config
|
||||
global:
|
||||
scrape_interval: 10s # By default, scrape targets every 15 seconds.
|
||||
evaluation_interval: 10s # By default, scrape targets every 15 seconds.
|
||||
# scrape_timeout is set to the global default (10s).
|
||||
|
||||
# Load and evaluate rules in this file every 'evaluation_interval' seconds.
|
||||
#rule_files:
|
||||
# - "alert.rules"
|
||||
# - "first.rules"
|
||||
# - "second.rules"
|
||||
|
||||
# alerting:
|
||||
# alertmanagers:
|
||||
# - scheme: http
|
||||
# static_configs:
|
||||
# - targets:
|
||||
# - "127.0.0.1:9093"
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
- job_name: 'grafana'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'grafana'
|
||||
type: 'A'
|
||||
port: 3000
|
||||
refresh_interval: 10s
|
||||
|
||||
# - job_name: 'mysql'
|
||||
# dns_sd_configs:
|
||||
# - names:
|
||||
# - 'mysqld-exporter'
|
||||
# type: 'A'
|
||||
# port: 9104
|
||||
# refresh_interval: 10s
|
@ -11,7 +11,7 @@ bulkDashboard() {
|
||||
let COUNTER=COUNTER+1
|
||||
done
|
||||
|
||||
ln -s -f -r ./bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
|
||||
ln -s -f ../../../devenv/bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
|
||||
}
|
||||
|
||||
bulkAlertingDashboard() {
|
||||
@ -25,7 +25,7 @@ bulkAlertingDashboard() {
|
||||
let COUNTER=COUNTER+1
|
||||
done
|
||||
|
||||
ln -s -f -r ./bulk_alerting_dashboards/bulk_alerting_dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
|
||||
ln -s -f ../../../devenv/bulk_alerting_dashboards/bulk_alerting_dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
|
||||
}
|
||||
|
||||
requiresJsonnet() {
|
||||
|
@ -55,7 +55,7 @@ This admin flag makes a user a `Super Admin`. This means they can access the `Se
|
||||
{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}}
|
||||
|
||||
For dashboards and dashboard folders there is a **Permissions** page that make it possible to
|
||||
remove the default role based permssions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**.
|
||||
remove the default role based permissions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**.
|
||||
|
||||
You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**.
|
||||
|
||||
@ -102,7 +102,7 @@ Permissions for a dashboard:
|
||||
|
||||
Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins.
|
||||
|
||||
- **View**: Can only view existing dashboars/folders.
|
||||
- **View**: Can only view existing dashboards/folders.
|
||||
- You cannot override permissions for users with **Org Admin Role**
|
||||
- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule.
|
||||
|
||||
|
@ -200,7 +200,7 @@ providers:
|
||||
folder: ''
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 3 #how often Grafana will scan for changed dashboards
|
||||
updateIntervalSeconds: 10 #how often Grafana will scan for changed dashboards
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
```
|
||||
@ -217,7 +217,7 @@ Note: The JSON shown in input field and when using `Copy JSON to Clipboard` and/
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v51/provisioning_cannot_save_dashboard.png" max-width="500px" class="docs-image--no-shadow" >}}
|
||||
|
||||
### Reuseable Dashboard Urls
|
||||
### Reusable Dashboard Urls
|
||||
|
||||
If the dashboard in the json file contains an [uid](/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifier.
|
||||
When Grafana starts, it will update/insert all dashboards available in the configured folders. If you modify the file, the dashboard will also be updated.
|
||||
|
@ -181,6 +181,7 @@ group_search_filter = "(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU
|
||||
group_search_filter = "(|(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])(member:1.2.840.113556.1.4.1941:=CN=%s,[another user container/OU]))"
|
||||
group_search_filter_user_attribute = "cn"
|
||||
```
|
||||
For more information on AD searches see [Microsoft's Search Filter Syntax](https://docs.microsoft.com/en-us/windows/desktop/adsi/search-filter-syntax) documentation.
|
||||
|
||||
For troubleshooting, by changing `member_of` in `[servers.attributes]` to "dn" it will show you more accurate group memberships when [debug is enabled](#troubleshooting).
|
||||
|
||||
|
@ -58,7 +58,7 @@ If you change your organization name in the Grafana UI this setting needs to be
|
||||
### Basic authentication
|
||||
|
||||
Basic auth is enabled by default and works with the built in Grafana user password authentication system and LDAP
|
||||
authenticaten integration.
|
||||
authentication integration.
|
||||
|
||||
To disable basic auth:
|
||||
|
||||
|
@ -101,4 +101,4 @@ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU [OR US]
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
This CLA agreement is based on the [Harmony Contributor Aggrement Template (combined)](http://www.harmonyagreements.org/agreements.html), [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/)
|
||||
This CLA agreement is based on the [Harmony Contributor Agreement Template (combined)](http://www.harmonyagreements.org/agreements.html), [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/)
|
||||
|
@ -225,7 +225,7 @@ When above query are used in a graph panel the result will be two series named `
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v51/mssql_time_series_two.png" class="docs-image--no-shadow docs-image--right" >}}
|
||||
|
||||
**Example with multiple `value` culumns:**
|
||||
**Example with multiple `value` columns:**
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
|
@ -59,7 +59,7 @@ Identifier | Description
|
||||
The database user you specify when you add the data source should only be granted SELECT permissions on
|
||||
the specified database & tables you want to query. Grafana does not validate that the query is safe. The query
|
||||
could include any SQL statement. For example, statements like `USE otherdb;` and `DROP TABLE user;` would be
|
||||
executed. To protect against this we **Highly** recommmend you create a specific mysql user with restricted permissions.
|
||||
executed. To protect against this we **Highly** recommend you create a specific mysql user with restricted permissions.
|
||||
|
||||
Example:
|
||||
|
||||
|
@ -84,7 +84,7 @@ Some examples are mentioned below to make nested template queries work successfu
|
||||
Query | Description
|
||||
------------ | -------------
|
||||
*tag_values(cpu, hostname, env=$env)* | Return tag values for cpu metric, selected env tag value and tag key hostname
|
||||
*tag_values(cpu, hostanme, env=$env, region=$region)* | Return tag values for cpu metric, selected env tag value, selected region tag value and tag key hostname
|
||||
*tag_values(cpu, hostname, env=$env, region=$region)* | Return tag values for cpu metric, selected env tag value, selected region tag value and tag key hostname
|
||||
|
||||
For details on OpenTSDB metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html)
|
||||
|
||||
|
171
docs/sources/features/datasources/stackdriver.md
Normal file
171
docs/sources/features/datasources/stackdriver.md
Normal file
@ -0,0 +1,171 @@
|
||||
+++
|
||||
title = "Using Stackdriver in Grafana"
|
||||
description = "Guide for using Stackdriver in Grafana"
|
||||
keywords = ["grafana", "stackdriver", "google", "guide"]
|
||||
type = "docs"
|
||||
aliases = ["/datasources/stackdriver"]
|
||||
[menu.docs]
|
||||
name = "Stackdriver"
|
||||
parent = "datasources"
|
||||
weight = 11
|
||||
+++
|
||||
|
||||
# Using Google Stackdriver in Grafana
|
||||
|
||||
> Only available in Grafana v5.3+.
|
||||
> The datasource is currently a beta feature and is subject to change.
|
||||
|
||||
Grafana ships with built-in support for Google Stackdriver. Just add it as a datasource and you are ready to build dashboards for your Stackdriver metrics.
|
||||
|
||||
## Adding the data source to Grafana
|
||||
|
||||
1. Open the side menu by clicking the Grafana icon in the top header.
|
||||
2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
|
||||
3. Click the `+ Add data source` button in the top header.
|
||||
4. Select `Stackdriver` from the *Type* dropdown.
|
||||
5. Upload or paste in the Service Account Key file. See below for steps on how to create a Service Account Key file.
|
||||
|
||||
> NOTE: If you're not seeing the `Data Sources` link in your side menu it means that your current user does not have the `Admin` role for the current organization.
|
||||
|
||||
| Name | Description |
|
||||
| --------------------- | ----------------------------------------------------------------------------------- |
|
||||
| _Name_ | The datasource name. This is how you refer to the datasource in panels & queries. |
|
||||
| _Default_ | Default datasource means that it will be pre-selected for new panels. |
|
||||
| _Service Account Key_ | Service Account Key File for a GCP Project. Instructions below on how to create it. |
|
||||
|
||||
## Authentication
|
||||
|
||||
### Service Account Credentials - Private Key File
|
||||
|
||||
To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to visualize data from multiple GCP Projects then you need to create one datasource per GCP Project.
|
||||
|
||||
#### Enable APIs
|
||||
|
||||
The following APIs need to be enabled first:
|
||||
|
||||
- [Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com)
|
||||
- [Cloud Resource Manager API](https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com)
|
||||
|
||||
Click on the links above and click the `Enable` button:
|
||||
|
||||

|
||||
|
||||
#### Create a GCP Service Account for a Project
|
||||
|
||||
1. Navigate to the [APIs & Services Credentials page](https://console.cloud.google.com/apis/credentials).
|
||||
2. Click on the `Create credentials` dropdown/button and choose the `Service account key` option.
|
||||
|
||||

|
||||
3. On the `Create service account key` page, choose key type `JSON`. Then in the `Service Account` dropdown, choose the `New service account` option:
|
||||
|
||||

|
||||
4. Some new fields will appear. Fill in a name for the service account in the `Service account name` field and then choose the `Monitoring Viewer` role from the `Role` dropdown:
|
||||
|
||||

|
||||
5. Click the Create button. A JSON key file will be created and downloaded to your computer. Store this file in a secure place as it allows access to your Stackdriver data.
|
||||
6. Upload it to Grafana on the datasource Configuration page. You can either upload the file or paste in the contents of the file.
|
||||
|
||||

|
||||
7. The file contents will be encrypted and saved in the Grafana database. Don't forget to save after uploading the file!
|
||||
|
||||

|
||||
|
||||
## Metric Query Editor
|
||||
|
||||
Choose a metric from the `Metric` dropdown.
|
||||
|
||||
To add a filter, click the plus icon and choose a field to filter by and enter a filter value e.g. `instance_name = grafana-1`
|
||||
|
||||
### Aggregation
|
||||
|
||||
The aggregation field lets you combine time series based on common statistics. Read more about this option [here](https://cloud.google.com/monitoring/charts/metrics-selector#aggregation-options).
|
||||
|
||||
The `Aligner` field allows you to align multiple time series after the same group by time interval. Read more about how it works [here](https://cloud.google.com/monitoring/charts/metrics-selector#alignment).
|
||||
|
||||
#### Alignment Period/Group by Time
|
||||
|
||||
The `Alignment Period` groups a metric by time if an aggregation is chosen. The default is to use the GCP Stackdriver default groupings (which allows you to compare graphs in Grafana with graphs in the Stackdriver UI).
|
||||
The option is called `Stackdriver auto` and the defaults are:
|
||||
|
||||
- 1m for time ranges < 23 hours
|
||||
- 5m for time ranges >= 23 hours and < 6 days
|
||||
- 1h for time ranges >= 6 days
|
||||
|
||||
The other automatic option is `Grafana auto`. This will automatically set the group by time depending on the time range chosen and the width of the graph panel. Read more about the details [here](http://docs.grafana.org/reference/templating/#the-interval-variable).
|
||||
|
||||
It is also possible to choose fixed time intervals to group by, like `1h` or `1d`.
|
||||
|
||||
### Group By
|
||||
|
||||
Group by resource or metric labels to reduce the number of time series and to aggregate the results by a group by. E.g. Group by instance_name to see an aggregated metric for a Compute instance.
|
||||
|
||||
### Alias Patterns
|
||||
|
||||
The Alias By field allows you to control the format of the legend keys. The default is to show the metric name and labels. This can be long and hard to read. Using the following patterns in the alias field, you can format the legend key the way you want it.
|
||||
|
||||
#### Metric Type Patterns
|
||||
|
||||
Alias Pattern | Description | Example Result
|
||||
----------------- | ---------------------------- | -------------
|
||||
`{{metric.type}}` | returns the full Metric Type | `compute.googleapis.com/instance/cpu/utilization`
|
||||
`{{metric.name}}` | returns the metric name part | `instance/cpu/utilization`
|
||||
`{{metric.service}}` | returns the service part | `compute`
|
||||
|
||||
#### Label Patterns
|
||||
|
||||
In the Group By dropdown, you can see a list of metric and resource labels for a metric. These can be included in the legend key using alias patterns.
|
||||
|
||||
Alias Pattern Format | Description | Alias Pattern Example | Example Result
|
||||
---------------------- | ---------------------------------- | ---------------------------- | -------------
|
||||
`{{metric.label.xxx}}` | returns the metric label value | `{{metric.label.instance_name}}` | `grafana-1-prod`
|
||||
`{{resource.label.xxx}}` | returns the resource label value | `{{resource.label.zone}}` | `us-east1-b`
|
||||
|
||||
Example Alias By: `{{metric.type}} - {{metric.labels.instance_name}}`
|
||||
|
||||
Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod`
|
||||
|
||||
## Templating
|
||||
|
||||
Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place.
|
||||
Variables are shown as dropdown select boxes at the top of the dashboard. These dropdowns makes it easy to change the data
|
||||
being displayed in your dashboard.
|
||||
|
||||
Checkout the [Templating]({{< relref "reference/templating.md" >}}) documentation for an introduction to the templating feature and the different
|
||||
types of template variables.
|
||||
|
||||
### Query Variable
|
||||
|
||||
Writing variable queries is not supported yet.
|
||||
|
||||
### Using variables in queries
|
||||
|
||||
There are two syntaxes:
|
||||
|
||||
- `$<varname>` Example: rate(http_requests_total{job=~"$job"}[5m])
|
||||
- `[[varname]]` Example: rate(http_requests_total{job=~"[[job]]"}[5m])
|
||||
|
||||
Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of a word. When the *Multi-value* or *Include all value* options are enabled, Grafana converts the labels from plain text to a regex compatible string, which means you have to use `=~` instead of `=`.
|
||||
|
||||
## Annotations
|
||||
|
||||
[Annotations]({{< relref "reference/annotations.md" >}}) allows you to overlay rich event information on top of graphs. You add annotation
|
||||
queries via the Dashboard menu / Annotations view.
|
||||
|
||||
## Configure the Datasource with Provisioning
|
||||
|
||||
It's now possible to configure datasources using config files with Grafana's provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
|
||||
|
||||
Here is a provisioning example for this datasource.
|
||||
|
||||
```yaml
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Stackdriver
|
||||
type: stackdriver
|
||||
jsonData:
|
||||
tokenUri: https://oauth2.googleapis.com/token
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
secureJsonData:
|
||||
privateKey: "<contents of your Service Account JWT Key file>"
|
||||
```
|
@ -22,6 +22,6 @@ The alert list panel allows you to display your dashboards alerts. The list can
|
||||
|
||||
1. **Show**: Lets you choose between current state or recent state changes.
|
||||
2. **Max Items**: Max items set the maximum of items in a list.
|
||||
3. **Sort Order**: Lets you sort your list alphabeticaly(asc/desc) or by importance.
|
||||
3. **Sort Order**: Lets you sort your list alphabetically(asc/desc) or by importance.
|
||||
4. **Alerts From** This Dashboard`: Shows alerts only from the dashboard the alert list is in.
|
||||
5. **State Filter**: Here you can filter your list by one or more parameters.
|
||||
|
@ -80,7 +80,7 @@ the upper or lower bound of the interval.
|
||||
There are a number of datasources supporting histogram over time like Elasticsearch (by using a Histogram bucket
|
||||
aggregation) or Prometheus (with [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) metric type
|
||||
and *Format as* option set to Heatmap). But generally, any datasource could be used if it meets the requirements:
|
||||
returns series with names representing bucket bound or returns sereis sorted by the bound in ascending order.
|
||||
returns series with names representing bucket bound or returns series sorted by the bound in ascending order.
|
||||
|
||||
With Elasticsearch you control the size of the buckets using the Histogram interval (Y-Axis) and the Date Histogram interval (X-axis).
|
||||
|
||||
|
@ -25,7 +25,7 @@ correctly in UTC mode.
|
||||
<br>
|
||||
|
||||
This release brings a fully featured query editor for Elasticsearch. You will now be able to visualize
|
||||
logs or any kind of data stored in Elasticserarch. The query editor allows you to build both simple
|
||||
logs or any kind of data stored in Elasticsearch. The query editor allows you to build both simple
|
||||
and complex queries for logs or metrics.
|
||||
|
||||
- Compute metrics from your documents, supported Elasticsearch aggregations:
|
||||
|
@ -34,7 +34,7 @@ Organizations via a role. That role can be:
|
||||
|
||||
There are currently no permissions on individual dashboards.
|
||||
|
||||
Read more about Grafanas new user model on the [Admin section](../reference/admin/)
|
||||
Read more about Grafana's new user model on the [Admin section](../reference/admin/)
|
||||
|
||||
## Dashboard Snapshot sharing
|
||||
|
||||
|
@ -21,7 +21,7 @@ The export feature is now accessed from the share menu.
|
||||
Dashboards exported from Grafana 3.1 are now more portable and easier for others to import than before.
|
||||
The export process extracts information data source types used by panels and adds these to a new `inputs`
|
||||
section in the dashboard json. So when you or another person tries to import the dashboard they will be asked to
|
||||
select data source and optional metrix prefix options.
|
||||
select data source and optional metric prefix options.
|
||||
|
||||
<img src="/img/docs/v31/import_step1.png">
|
||||
|
||||
@ -53,7 +53,7 @@ Grafana url to share with a colleague without having to use the Share modal.
|
||||
|
||||
## Internal metrics
|
||||
|
||||
Do you want metrics about viewing metrics? Ofc you do! In this release we added support for sending metrics about Grafana to graphite.
|
||||
Do you want metrics about viewing metrics? Of course you do! In this release we added support for sending metrics about Grafana to graphite.
|
||||
You can configure interval and server in the config file.
|
||||
|
||||
## Logging
|
||||
|
@ -197,7 +197,7 @@ you can install it manually from [Grafana.com](https://grafana.com)
|
||||
## Plugin showcase
|
||||
|
||||
Discovering and installing plugins is very quick and easy with Grafana 3.0 and [Grafana.com](https://grafana.com). Here
|
||||
are a couple that I incurage you try!
|
||||
are a couple that I encourage you try!
|
||||
|
||||
#### [Clock Panel](https://grafana.com/plugins/grafana-clock-panel)
|
||||
Support's both current time and count down mode.
|
||||
|
@ -45,7 +45,7 @@ We might add more global built in variables in the future and if we do we will p
|
||||
|
||||
### Dedupe alert notifications when running multiple servers
|
||||
|
||||
In this release we will dedupe alert notificiations when you are running multiple servers.
|
||||
In this release we will dedupe alert notifications when you are running multiple servers.
|
||||
This makes it possible to run alerting on multiple servers and only get one notification.
|
||||
|
||||
We currently solve this with sql transactions which puts some limitations for how many servers you can use to execute the same rules.
|
||||
|
@ -45,7 +45,7 @@ More information [here](https://community.grafana.com/t/using-grafanas-query-ins
|
||||
### Enhancements
|
||||
|
||||
* **GitHub OAuth**: Support for GitHub organizations with 100+ teams. [#8846](https://github.com/grafana/grafana/issues/8846), thx [@skwashd](https://github.com/skwashd)
|
||||
* **Graphite**: Calls to Graphite api /metrics/find now include panel or dashboad time range (from & until) in most cases, [#8055](https://github.com/grafana/grafana/issues/8055)
|
||||
* **Graphite**: Calls to Graphite api /metrics/find now include panel or dashboard time range (from & until) in most cases, [#8055](https://github.com/grafana/grafana/issues/8055)
|
||||
* **Graphite**: Added new graphite 1.0 functions, available if you set version to 1.0.x in data source settings. New Functions: mapSeries, reduceSeries, isNonNull, groupByNodes, offsetToZero, grep, weightedAverage, removeEmptySeries, aggregateLine, averageOutsidePercentile, delay, exponentialMovingAverage, fallbackSeries, integralByInterval, interpolate, invert, linearRegression, movingMin, movingMax, movingSum, multiplySeriesWithWildcards, pow, powSeries, removeBetweenPercentile, squareRoot, timeSlice, closes [#8261](https://github.com/grafana/grafana/issues/8261)
|
||||
- **Elasticsearch**: Ad-hoc filters now use query phrase match filters instead of term filters, works on non keyword/raw fields [#9095](https://github.com/grafana/grafana/issues/9095).
|
||||
|
||||
@ -53,7 +53,7 @@ More information [here](https://community.grafana.com/t/using-grafanas-query-ins
|
||||
|
||||
* **InfluxDB/Elasticsearch**: The panel & data source option named "Group by time interval" is now named "Min time interval" and does now always define a lower limit for the auto group by time. Without having to use `>` prefix (that prefix still works). This should in theory have close to zero actual impact on existing dashboards. It does mean that if you used this setting to define a hard group by time interval of, say "1d", if you zoomed to a time range wide enough the time range could increase above the "1d" range as the setting is now always considered a lower limit.
|
||||
|
||||
This option is now rennamed (and moved to Options sub section above your queries):
|
||||
This option is now renamed (and moved to Options sub section above your queries):
|
||||

|
||||
|
||||
Datas source selection & options & help are now above your metric queries.
|
||||
|
@ -61,7 +61,7 @@ This makes exploring and filtering Prometheus data much easier.
|
||||
### Minor Changes
|
||||
|
||||
* **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319)
|
||||
* **Dataproxy**: Allow grafan to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250)
|
||||
* **Dataproxy**: Allow Grafana to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250)
|
||||
* **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367)
|
||||
* **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739)
|
||||
* **Slack**: Allow images to be uploaded to slack when Token is present [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8)
|
||||
|
@ -227,7 +227,7 @@ Content-Type: application/json
|
||||
|
||||
## Create alert notification
|
||||
|
||||
You can find the full list of [supported notifers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page.
|
||||
You can find the full list of [supported notifiers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page.
|
||||
|
||||
`POST /api/alert-notifications`
|
||||
|
||||
|
@ -291,7 +291,7 @@ Content-Type: text/html; charset=UTF-8
|
||||
</p>
|
||||
```
|
||||
|
||||
The response is a textual respresentation of the diff, with the dashboard values being in JSON, similar to the diffs seen on sites like GitHub or GitLab.
|
||||
The response is a textual representation of the diff, with the dashboard values being in JSON, similar to the diffs seen on sites like GitHub or GitLab.
|
||||
|
||||
Status Codes:
|
||||
|
||||
|
@ -127,10 +127,13 @@ Another way is put a webserver like Nginx or Apache in front of Grafana and have
|
||||
|
||||
### protocol
|
||||
|
||||
`http` or `https`
|
||||
`http`,`https` or `socket`
|
||||
|
||||
> **Note** Grafana versions earlier than 3.0 are vulnerable to [POODLE](https://en.wikipedia.org/wiki/POODLE). So we strongly recommend to upgrade to 3.x or use a reverse proxy for ssl termination.
|
||||
|
||||
### socket
|
||||
Path where the socket should be created when `protocol=socket`. Please make sure that Grafana has appropriate permissions.
|
||||
|
||||
### domain
|
||||
|
||||
This setting is only used in as a part of the `root_url` setting (see below). Important if you
|
||||
@ -566,3 +569,11 @@ Default setting for new alert rules. Defaults to categorize error and timeouts a
|
||||
> Available in 5.3 and above
|
||||
|
||||
Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
|
||||
|
||||
# concurrent_render_limit
|
||||
|
||||
> Available in 5.3 and above
|
||||
|
||||
Alert notifications can include images, but rendering many images at the same time can overload the server.
|
||||
This limit will protect the server from render overloading and make sure notifications are sent out quickly. Default
|
||||
value is `5`.
|
||||
|
@ -26,9 +26,9 @@ Grafana will now persist all long term data in the database. How to configure th
|
||||
|
||||
## User sessions
|
||||
|
||||
The second thing to consider is how to deal with user sessions and how to configure your load balancer infront of Grafana.
|
||||
The second thing to consider is how to deal with user sessions and how to configure your load balancer in front of Grafana.
|
||||
Grafana supports two ways of storing session data: locally on disk or in a database/cache-server.
|
||||
If you want to store sessions on disk you can use `sticky sessions` in your load balanacer. If you prefer to store session data in a database/cache-server
|
||||
If you want to store sessions on disk you can use `sticky sessions` in your load balancer. If you prefer to store session data in a database/cache-server
|
||||
you can use any stateless routing strategy in your load balancer (ex round robin or least connections).
|
||||
|
||||
### Sticky sessions
|
||||
|
@ -1,4 +1,5 @@
|
||||
[
|
||||
{ "version": "v5.3", "path": "/v5.3", "archived": false, "current": false },
|
||||
{ "version": "v5.2", "path": "/", "archived": false, "current": true },
|
||||
{ "version": "v5.1", "path": "/v5.1", "archived": true },
|
||||
{ "version": "v5.0", "path": "/v5.0", "archived": true },
|
||||
|
@ -4,7 +4,7 @@
|
||||
"company": "Grafana Labs"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "5.3.0-pre1",
|
||||
"version": "5.4.0-pre1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
@ -12,7 +12,7 @@
|
||||
"devDependencies": {
|
||||
"@types/d3": "^4.10.1",
|
||||
"@types/enzyme": "^3.1.13",
|
||||
"@types/jest": "^21.1.4",
|
||||
"@types/jest": "^23.3.2",
|
||||
"@types/node": "^8.0.31",
|
||||
"@types/react": "^16.4.14",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
|
@ -97,15 +97,6 @@ type CacheServer struct {
|
||||
cache *gocache.Cache
|
||||
}
|
||||
|
||||
func (this *CacheServer) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) {
|
||||
for _, k := range keys {
|
||||
if _, err := fmt.Sscanf(r.FormValue(k), "%d", &v); err == nil {
|
||||
defaultValue = v
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (this *CacheServer) Handler(ctx *macaron.Context) {
|
||||
urlPath := ctx.Req.URL.Path
|
||||
hash := urlPath[strings.LastIndex(urlPath, "/")+1:]
|
||||
|
@ -22,6 +22,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
anonString = "Anonymous"
|
||||
)
|
||||
|
||||
func isDashboardStarredByUser(c *m.ReqContext, dashID int64) (bool, error) {
|
||||
if !c.IsSignedIn {
|
||||
return false, nil
|
||||
@ -64,7 +68,7 @@ func GetDashboard(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
// Finding creator and last updater of the dashboard
|
||||
updater, creator := "Anonymous", "Anonymous"
|
||||
updater, creator := anonString, anonString
|
||||
if dash.UpdatedBy > 0 {
|
||||
updater = getUserLogin(dash.UpdatedBy)
|
||||
}
|
||||
@ -128,7 +132,7 @@ func getUserLogin(userID int64) string {
|
||||
query := m.GetUserByIdQuery{Id: userID}
|
||||
err := bus.Dispatch(&query)
|
||||
if err != nil {
|
||||
return "Anonymous"
|
||||
return anonString
|
||||
}
|
||||
return query.Result.Login
|
||||
}
|
||||
@ -403,7 +407,7 @@ func GetDashboardVersion(c *m.ReqContext) Response {
|
||||
return Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err)
|
||||
}
|
||||
|
||||
creator := "Anonymous"
|
||||
creator := anonString
|
||||
if query.Result.CreatedBy > 0 {
|
||||
creator = getUserLogin(query.Result.CreatedBy)
|
||||
}
|
||||
|
@ -51,7 +51,21 @@ func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
|
||||
return
|
||||
}
|
||||
|
||||
proxyPath := c.Params("*")
|
||||
// macaron does not include trailing slashes when resolving a wildcard path
|
||||
proxyPath := ensureProxyPathTrailingSlash(c.Req.URL.Path, c.Params("*"))
|
||||
|
||||
proxy := pluginproxy.NewDataSourceProxy(ds, plugin, c, proxyPath)
|
||||
proxy.HandleRequest()
|
||||
}
|
||||
|
||||
// ensureProxyPathTrailingSlash Check for a trailing slash in original path and makes
|
||||
// sure that a trailing slash is added to proxy path, if not already exists.
|
||||
func ensureProxyPathTrailingSlash(originalPath, proxyPath string) string {
|
||||
if len(proxyPath) > 1 {
|
||||
if originalPath[len(originalPath)-1] == '/' && proxyPath[len(proxyPath)-1] != '/' {
|
||||
return proxyPath + "/"
|
||||
}
|
||||
}
|
||||
|
||||
return proxyPath
|
||||
}
|
||||
|
19
pkg/api/dataproxy_test.go
Normal file
19
pkg/api/dataproxy_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDataProxy(t *testing.T) {
|
||||
Convey("Data proxy test", t, func() {
|
||||
Convey("Should append trailing slash to proxy path if original path has a trailing slash", func() {
|
||||
So(ensureProxyPathTrailingSlash("/api/datasources/proxy/6/api/v1/query_range/", "api/v1/query_range/"), ShouldEqual, "api/v1/query_range/")
|
||||
})
|
||||
|
||||
Convey("Should not append trailing slash to proxy path if original path doesn't have a trailing slash", func() {
|
||||
So(ensureProxyPathTrailingSlash("/api/datasources/proxy/6/api/v1/query_range", "api/v1/query_range"), ShouldEqual, "api/v1/query_range")
|
||||
})
|
||||
})
|
||||
}
|
@ -95,7 +95,7 @@ func toFolderDto(g guardian.DashboardGuardian, folder *m.Folder) dtos.Folder {
|
||||
canAdmin, _ := g.CanAdmin()
|
||||
|
||||
// Finding creator and last updater of the folder
|
||||
updater, creator := "Anonymous", "Anonymous"
|
||||
updater, creator := anonString, anonString
|
||||
if folder.CreatedBy > 0 {
|
||||
creator = getUserLogin(folder.CreatedBy)
|
||||
}
|
||||
|
@ -133,16 +133,6 @@ func TestFoldersApiEndpoint(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func callGetFolderByUID(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetFolderByUID
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func callDeleteFolder(sc *scenarioContext) {
|
||||
sc.handlerFunc = DeleteFolder
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func callCreateFolder(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
@ -11,6 +11,12 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
// Themes
|
||||
lightName = "light"
|
||||
darkName = "dark"
|
||||
)
|
||||
|
||||
func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
settings, err := getFrontendSettingsMap(c)
|
||||
if err != nil {
|
||||
@ -60,7 +66,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
OrgRole: c.OrgRole,
|
||||
GravatarUrl: dtos.GetGravatarUrl(c.Email),
|
||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||
LightTheme: prefs.Theme == "light",
|
||||
LightTheme: prefs.Theme == lightName,
|
||||
Timezone: prefs.Timezone,
|
||||
Locale: locale,
|
||||
HelpFlags1: c.HelpFlags1,
|
||||
@ -88,12 +94,12 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
}
|
||||
|
||||
themeURLParam := c.Query("theme")
|
||||
if themeURLParam == "light" {
|
||||
if themeURLParam == lightName {
|
||||
data.User.LightTheme = true
|
||||
data.Theme = "light"
|
||||
} else if themeURLParam == "dark" {
|
||||
data.Theme = lightName
|
||||
} else if themeURLParam == darkName {
|
||||
data.User.LightTheme = false
|
||||
data.Theme = "dark"
|
||||
data.Theme = darkName
|
||||
}
|
||||
|
||||
if hasEditPermissionInFoldersQuery.Result {
|
||||
|
@ -37,9 +37,6 @@ func newHub() *hub {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hub) removeConnection() {
|
||||
}
|
||||
|
||||
func (h *hub) run(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
|
171
pkg/api/pluginproxy/access_token_provider.go
Normal file
171
pkg/api/pluginproxy/access_token_provider.go
Normal file
@ -0,0 +1,171 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenCache = tokenCacheType{
|
||||
cache: map[string]*jwtToken{},
|
||||
}
|
||||
oauthJwtTokenCache = oauthJwtTokenCacheType{
|
||||
cache: map[string]*oauth2.Token{},
|
||||
}
|
||||
)
|
||||
|
||||
type tokenCacheType struct {
|
||||
cache map[string]*jwtToken
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
type oauthJwtTokenCacheType struct {
|
||||
cache map[string]*oauth2.Token
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
type accessTokenProvider struct {
|
||||
route *plugins.AppPluginRoute
|
||||
datasourceId int64
|
||||
datasourceVersion int
|
||||
}
|
||||
|
||||
type jwtToken struct {
|
||||
ExpiresOn time.Time `json:"-"`
|
||||
ExpiresOnString string `json:"expires_on"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
func newAccessTokenProvider(ds *models.DataSource, pluginRoute *plugins.AppPluginRoute) *accessTokenProvider {
|
||||
return &accessTokenProvider{
|
||||
datasourceId: ds.Id,
|
||||
datasourceVersion: ds.Version,
|
||||
route: pluginRoute,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *accessTokenProvider) getAccessToken(data templateData) (string, error) {
|
||||
tokenCache.Lock()
|
||||
defer tokenCache.Unlock()
|
||||
if cachedToken, found := tokenCache.cache[provider.getAccessTokenCacheKey()]; found {
|
||||
if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) {
|
||||
logger.Info("Using token from cache")
|
||||
return cachedToken.AccessToken, nil
|
||||
}
|
||||
}
|
||||
|
||||
urlInterpolated, err := interpolateString(provider.route.TokenAuth.Url, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
params := make(url.Values)
|
||||
for key, value := range provider.route.TokenAuth.Params {
|
||||
interpolatedParam, err := interpolateString(value, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
params.Add(key, interpolatedParam)
|
||||
}
|
||||
|
||||
getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode()))
|
||||
getTokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
getTokenReq.Header.Add("Content-Length", strconv.Itoa(len(params.Encode())))
|
||||
|
||||
resp, err := client.Do(getTokenReq)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
var token jwtToken
|
||||
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
|
||||
token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
|
||||
tokenCache.cache[provider.getAccessTokenCacheKey()] = &token
|
||||
|
||||
logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
|
||||
|
||||
return token.AccessToken, nil
|
||||
}
|
||||
|
||||
func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data templateData) (string, error) {
|
||||
oauthJwtTokenCache.Lock()
|
||||
defer oauthJwtTokenCache.Unlock()
|
||||
if cachedToken, found := oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()]; found {
|
||||
if cachedToken.Expiry.After(time.Now().Add(time.Second * 10)) {
|
||||
logger.Debug("Using token from cache")
|
||||
return cachedToken.AccessToken, nil
|
||||
}
|
||||
}
|
||||
|
||||
conf := &jwt.Config{}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["client_email"]; ok {
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
conf.Email = interpolatedVal
|
||||
}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["private_key"]; ok {
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
conf.PrivateKey = []byte(interpolatedVal)
|
||||
}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["token_uri"]; ok {
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
conf.TokenURL = interpolatedVal
|
||||
}
|
||||
|
||||
conf.Scopes = provider.route.JwtTokenAuth.Scopes
|
||||
|
||||
token, err := getTokenSource(conf, ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()] = token
|
||||
|
||||
logger.Info("Got new access token", "ExpiresOn", token.Expiry)
|
||||
|
||||
return token.AccessToken, nil
|
||||
}
|
||||
|
||||
var getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
|
||||
tokenSrc := conf.TokenSource(ctx)
|
||||
token, err := tokenSrc.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (provider *accessTokenProvider) getAccessTokenCacheKey() string {
|
||||
return fmt.Sprintf("%v_%v_%v_%v", provider.datasourceId, provider.datasourceVersion, provider.route.Path, provider.route.Method)
|
||||
}
|
94
pkg/api/pluginproxy/access_token_provider_test.go
Normal file
94
pkg/api/pluginproxy/access_token_provider_test.go
Normal file
@ -0,0 +1,94 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
)
|
||||
|
||||
func TestAccessToken(t *testing.T) {
|
||||
Convey("Plugin with JWT token auth route", t, func() {
|
||||
pluginRoute := &plugins.AppPluginRoute{
|
||||
Path: "pathwithjwttoken1",
|
||||
Url: "https://api.jwt.io/some/path",
|
||||
Method: "GET",
|
||||
JwtTokenAuth: &plugins.JwtTokenAuth{
|
||||
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
Scopes: []string{
|
||||
"https://www.testapi.com/auth/monitoring.read",
|
||||
"https://www.testapi.com/auth/cloudplatformprojects.readonly",
|
||||
},
|
||||
Params: map[string]string{
|
||||
"token_uri": "{{.JsonData.tokenUri}}",
|
||||
"client_email": "{{.JsonData.clientEmail}}",
|
||||
"private_key": "{{.SecureJsonData.privateKey}}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
templateData := templateData{
|
||||
JsonData: map[string]interface{}{
|
||||
"clientEmail": "test@test.com",
|
||||
"tokenUri": "login.url.com/token",
|
||||
},
|
||||
SecureJsonData: map[string]string{
|
||||
"privateKey": "testkey",
|
||||
},
|
||||
}
|
||||
|
||||
ds := &models.DataSource{Id: 1, Version: 2}
|
||||
|
||||
Convey("should fetch token using jwt private key", func() {
|
||||
getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
|
||||
return &oauth2.Token{AccessToken: "abc"}, nil
|
||||
}
|
||||
provider := newAccessTokenProvider(ds, pluginRoute)
|
||||
token, err := provider.getJwtAccessToken(context.Background(), templateData)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(token, ShouldEqual, "abc")
|
||||
})
|
||||
|
||||
Convey("should set jwt config values", func() {
|
||||
getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
|
||||
So(conf.Email, ShouldEqual, "test@test.com")
|
||||
So(conf.PrivateKey, ShouldResemble, []byte("testkey"))
|
||||
So(len(conf.Scopes), ShouldEqual, 2)
|
||||
So(conf.Scopes[0], ShouldEqual, "https://www.testapi.com/auth/monitoring.read")
|
||||
So(conf.Scopes[1], ShouldEqual, "https://www.testapi.com/auth/cloudplatformprojects.readonly")
|
||||
So(conf.TokenURL, ShouldEqual, "login.url.com/token")
|
||||
|
||||
return &oauth2.Token{AccessToken: "abc"}, nil
|
||||
}
|
||||
|
||||
provider := newAccessTokenProvider(ds, pluginRoute)
|
||||
_, err := provider.getJwtAccessToken(context.Background(), templateData)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("should use cached token on second call", func() {
|
||||
getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
|
||||
return &oauth2.Token{
|
||||
AccessToken: "abc",
|
||||
Expiry: time.Now().Add(1 * time.Minute)}, nil
|
||||
}
|
||||
provider := newAccessTokenProvider(ds, pluginRoute)
|
||||
token1, err := provider.getJwtAccessToken(context.Background(), templateData)
|
||||
So(err, ShouldBeNil)
|
||||
So(token1, ShouldEqual, "abc")
|
||||
|
||||
getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
|
||||
return &oauth2.Token{AccessToken: "error: cache not used"}, nil
|
||||
}
|
||||
token2, err := provider.getJwtAccessToken(context.Background(), templateData)
|
||||
So(err, ShouldBeNil)
|
||||
So(token2, ShouldEqual, "abc")
|
||||
})
|
||||
})
|
||||
}
|
93
pkg/api/pluginproxy/ds_auth_provider.go
Normal file
93
pkg/api/pluginproxy/ds_auth_provider.go
Normal file
@ -0,0 +1,93 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
//ApplyRoute should use the plugin route data to set auth headers and custom headers
|
||||
func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route *plugins.AppPluginRoute, ds *m.DataSource) {
|
||||
proxyPath = strings.TrimPrefix(proxyPath, route.Path)
|
||||
|
||||
data := templateData{
|
||||
JsonData: ds.JsonData.Interface().(map[string]interface{}),
|
||||
SecureJsonData: ds.SecureJsonData.Decrypt(),
|
||||
}
|
||||
|
||||
interpolatedURL, err := interpolateString(route.Url, data)
|
||||
if err != nil {
|
||||
logger.Error("Error interpolating proxy url", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
routeURL, err := url.Parse(interpolatedURL)
|
||||
if err != nil {
|
||||
logger.Error("Error parsing plugin route url", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.URL.Scheme = routeURL.Scheme
|
||||
req.URL.Host = routeURL.Host
|
||||
req.Host = routeURL.Host
|
||||
req.URL.Path = util.JoinUrlFragments(routeURL.Path, proxyPath)
|
||||
|
||||
if err := addHeaders(&req.Header, route, data); err != nil {
|
||||
logger.Error("Failed to render plugin headers", "error", err)
|
||||
}
|
||||
|
||||
tokenProvider := newAccessTokenProvider(ds, route)
|
||||
|
||||
if route.TokenAuth != nil {
|
||||
if token, err := tokenProvider.getAccessToken(data); err != nil {
|
||||
logger.Error("Failed to get access token", "error", err)
|
||||
} else {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
}
|
||||
|
||||
if route.JwtTokenAuth != nil {
|
||||
if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil {
|
||||
logger.Error("Failed to get access token", "error", err)
|
||||
} else {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
}
|
||||
logger.Info("Requesting", "url", req.URL.String())
|
||||
|
||||
}
|
||||
|
||||
func interpolateString(text string, data templateData) (string, error) {
|
||||
t, err := template.New("content").Parse(text)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse template %s", text)
|
||||
}
|
||||
|
||||
var contentBuf bytes.Buffer
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute template %s", text)
|
||||
}
|
||||
|
||||
return contentBuf.String(), nil
|
||||
}
|
||||
|
||||
func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
|
||||
for _, header := range route.Headers {
|
||||
interpolated, err := interpolateString(header.Content, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqHeaders.Add(header.Name, interpolated)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
21
pkg/api/pluginproxy/ds_auth_provider_test.go
Normal file
21
pkg/api/pluginproxy/ds_auth_provider_test.go
Normal file
@ -0,0 +1,21 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDsAuthProvider(t *testing.T) {
|
||||
Convey("When interpolating string", t, func() {
|
||||
data := templateData{
|
||||
SecureJsonData: map[string]string{
|
||||
"Test": "0asd+asd",
|
||||
},
|
||||
}
|
||||
|
||||
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
|
||||
So(err, ShouldBeNil)
|
||||
So(interpolated, ShouldEqual, "0asd+asd")
|
||||
})
|
||||
}
|
@ -2,7 +2,6 @@ package pluginproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@ -12,7 +11,6 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/opentracing/opentracing-go"
|
||||
@ -25,17 +23,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
logger = log.New("data-proxy-log")
|
||||
tokenCache = map[string]*jwtToken{}
|
||||
client = newHTTPClient()
|
||||
logger = log.New("data-proxy-log")
|
||||
client = newHTTPClient()
|
||||
)
|
||||
|
||||
type jwtToken struct {
|
||||
ExpiresOn time.Time `json:"-"`
|
||||
ExpiresOnString string `json:"expires_on"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
type DataSourceProxy struct {
|
||||
ds *m.DataSource
|
||||
ctx *m.ReqContext
|
||||
@ -162,7 +153,6 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
|
||||
} else {
|
||||
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, proxy.proxyPath)
|
||||
}
|
||||
|
||||
if proxy.ds.BasicAuth {
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser, proxy.ds.BasicAuthPassword))
|
||||
@ -219,7 +209,7 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
|
||||
}
|
||||
|
||||
if proxy.route != nil {
|
||||
proxy.applyRoute(req)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -311,120 +301,3 @@ func checkWhiteList(c *m.ReqContext, host string) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
|
||||
proxy.proxyPath = strings.TrimPrefix(proxy.proxyPath, proxy.route.Path)
|
||||
|
||||
data := templateData{
|
||||
JsonData: proxy.ds.JsonData.Interface().(map[string]interface{}),
|
||||
SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
|
||||
}
|
||||
|
||||
interpolatedURL, err := interpolateString(proxy.route.Url, data)
|
||||
if err != nil {
|
||||
logger.Error("Error interpolating proxy url", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
routeURL, err := url.Parse(interpolatedURL)
|
||||
if err != nil {
|
||||
logger.Error("Error parsing plugin route url", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.URL.Scheme = routeURL.Scheme
|
||||
req.URL.Host = routeURL.Host
|
||||
req.Host = routeURL.Host
|
||||
req.URL.Path = util.JoinUrlFragments(routeURL.Path, proxy.proxyPath)
|
||||
|
||||
if err := addHeaders(&req.Header, proxy.route, data); err != nil {
|
||||
logger.Error("Failed to render plugin headers", "error", err)
|
||||
}
|
||||
|
||||
if proxy.route.TokenAuth != nil {
|
||||
if token, err := proxy.getAccessToken(data); err != nil {
|
||||
logger.Error("Failed to get access token", "error", err)
|
||||
} else {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Requesting", "url", req.URL.String())
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error) {
|
||||
if cachedToken, found := tokenCache[proxy.getAccessTokenCacheKey()]; found {
|
||||
if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) {
|
||||
logger.Info("Using token from cache")
|
||||
return cachedToken.AccessToken, nil
|
||||
}
|
||||
}
|
||||
|
||||
urlInterpolated, err := interpolateString(proxy.route.TokenAuth.Url, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
params := make(url.Values)
|
||||
for key, value := range proxy.route.TokenAuth.Params {
|
||||
interpolatedParam, err := interpolateString(value, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
params.Add(key, interpolatedParam)
|
||||
}
|
||||
|
||||
getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode()))
|
||||
getTokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
getTokenReq.Header.Add("Content-Length", strconv.Itoa(len(params.Encode())))
|
||||
|
||||
resp, err := client.Do(getTokenReq)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
var token jwtToken
|
||||
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
|
||||
token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
|
||||
tokenCache[proxy.getAccessTokenCacheKey()] = &token
|
||||
|
||||
logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
|
||||
return token.AccessToken, nil
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) getAccessTokenCacheKey() string {
|
||||
return fmt.Sprintf("%v_%v_%v", proxy.ds.Id, proxy.route.Path, proxy.route.Method)
|
||||
}
|
||||
|
||||
func interpolateString(text string, data templateData) (string, error) {
|
||||
t, err := template.New("content").Parse(text)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse template %s", text)
|
||||
}
|
||||
|
||||
var contentBuf bytes.Buffer
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute template %s", text)
|
||||
}
|
||||
|
||||
return contentBuf.String(), nil
|
||||
}
|
||||
|
||||
func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
|
||||
for _, header := range route.Headers {
|
||||
interpolated, err := interpolateString(header.Content, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqHeaders.Add(header.Name, interpolated)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ func TestDSRouteRule(t *testing.T) {
|
||||
Convey("When matching route path", func() {
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
|
||||
proxy.route = plugin.Routes[0]
|
||||
proxy.applyRoute(req)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
|
||||
|
||||
Convey("should add headers and update url", func() {
|
||||
So(req.URL.String(), ShouldEqual, "https://www.google.com/some/method")
|
||||
@ -94,7 +94,7 @@ func TestDSRouteRule(t *testing.T) {
|
||||
Convey("When matching route path and has dynamic url", func() {
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method")
|
||||
proxy.route = plugin.Routes[3]
|
||||
proxy.applyRoute(req)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
|
||||
|
||||
Convey("should add headers and interpolate the url", func() {
|
||||
So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method")
|
||||
@ -188,7 +188,7 @@ func TestDSRouteRule(t *testing.T) {
|
||||
client = newFakeHTTPClient(json)
|
||||
proxy1 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
|
||||
proxy1.route = plugin.Routes[0]
|
||||
proxy1.applyRoute(req)
|
||||
ApplyRoute(proxy1.ctx.Req.Context(), req, proxy1.proxyPath, proxy1.route, proxy1.ds)
|
||||
|
||||
authorizationHeaderCall1 = req.Header.Get("Authorization")
|
||||
So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
|
||||
@ -202,7 +202,7 @@ func TestDSRouteRule(t *testing.T) {
|
||||
client = newFakeHTTPClient(json2)
|
||||
proxy2 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2")
|
||||
proxy2.route = plugin.Routes[1]
|
||||
proxy2.applyRoute(req)
|
||||
ApplyRoute(proxy2.ctx.Req.Context(), req, proxy2.proxyPath, proxy2.route, proxy2.ds)
|
||||
|
||||
authorizationHeaderCall2 = req.Header.Get("Authorization")
|
||||
|
||||
@ -217,7 +217,7 @@ func TestDSRouteRule(t *testing.T) {
|
||||
client = newFakeHTTPClient([]byte{})
|
||||
proxy3 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
|
||||
proxy3.route = plugin.Routes[0]
|
||||
proxy3.applyRoute(req)
|
||||
ApplyRoute(proxy3.ctx.Req.Context(), req, proxy3.proxyPath, proxy3.route, proxy3.ds)
|
||||
|
||||
authorizationHeaderCall3 := req.Header.Get("Authorization")
|
||||
So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
|
||||
@ -331,18 +331,6 @@ func TestDSRouteRule(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When interpolating string", func() {
|
||||
data := templateData{
|
||||
SecureJsonData: map[string]string{
|
||||
"Test": "0asd+asd",
|
||||
},
|
||||
}
|
||||
|
||||
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
|
||||
So(err, ShouldBeNil)
|
||||
So(interpolated, ShouldEqual, "0asd+asd")
|
||||
})
|
||||
|
||||
Convey("When proxying a data source with custom headers specified", func() {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
@ -374,6 +362,23 @@ func TestDSRouteRule(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When proxying a custom datasource", func() {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
ds := &m.DataSource{
|
||||
Type: "custom-datasource",
|
||||
Url: "http://host/root/",
|
||||
}
|
||||
ctx := &m.ReqContext{}
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/")
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
proxy.getDirector()(req)
|
||||
|
||||
Convey("Shoudl keep user request (including trailing slash)", func() {
|
||||
So(req.URL.String(), ShouldEqual, "http://host/root/path/to/folder/")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -41,15 +41,16 @@ func (hs *HTTPServer) RenderToPng(c *m.ReqContext) {
|
||||
}
|
||||
|
||||
result, err := hs.RenderService.Render(c.Req.Context(), rendering.Opts{
|
||||
Width: width,
|
||||
Height: height,
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
OrgRole: c.OrgRole,
|
||||
Path: c.Params("*") + queryParams,
|
||||
Timezone: queryReader.Get("tz", ""),
|
||||
Encoding: queryReader.Get("encoding", ""),
|
||||
Width: width,
|
||||
Height: height,
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
OrgRole: c.OrgRole,
|
||||
Path: c.Params("*") + queryParams,
|
||||
Timezone: queryReader.Get("tz", ""),
|
||||
Encoding: queryReader.Get("encoding", ""),
|
||||
ConcurrentLimit: 30,
|
||||
})
|
||||
|
||||
if err != nil && err == rendering.ErrTimeout {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/fatih/color"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -24,6 +25,7 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli
|
||||
|
||||
engine := &sqlstore.SqlStore{}
|
||||
engine.Cfg = cfg
|
||||
engine.Bus = bus.GetBus()
|
||||
engine.Init()
|
||||
|
||||
if err := command(cmd); err != nil {
|
||||
|
@ -29,6 +29,7 @@ import (
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/opentsdb"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/postgres"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/prometheus"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/stackdriver"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/testdata"
|
||||
)
|
||||
|
||||
@ -103,7 +104,7 @@ func listenToSystemSignals(server *GrafanaServerImpl) {
|
||||
|
||||
for {
|
||||
select {
|
||||
case _ = <-sighupChan:
|
||||
case <-sighupChan:
|
||||
log.Reload()
|
||||
case sig := <-signalChan:
|
||||
server.Shutdown(fmt.Sprintf("System signal: %s", sig))
|
||||
|
@ -127,8 +127,6 @@ type xmlError struct {
|
||||
const ms_date_layout = "Mon, 02 Jan 2006 15:04:05 GMT"
|
||||
const version = "2017-04-17"
|
||||
|
||||
var client = &http.Client{}
|
||||
|
||||
type StorageClient struct {
|
||||
Auth *Auth
|
||||
Transport http.RoundTripper
|
||||
|
@ -2,12 +2,15 @@ package imguploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
|
||||
"github.com/aws/aws-sdk-go/aws/defaults"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
@ -50,7 +53,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
|
||||
SecretAccessKey: u.secretKey,
|
||||
}},
|
||||
&credentials.EnvProvider{},
|
||||
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
|
||||
remoteCredProvider(sess),
|
||||
})
|
||||
cfg := &aws.Config{
|
||||
Region: aws.String(u.region),
|
||||
@ -85,3 +88,27 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
|
||||
}
|
||||
return image_url, nil
|
||||
}
|
||||
|
||||
func remoteCredProvider(sess *session.Session) credentials.Provider {
|
||||
ecsCredURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
|
||||
|
||||
if len(ecsCredURI) > 0 {
|
||||
return ecsCredProvider(sess, ecsCredURI)
|
||||
}
|
||||
return ec2RoleProvider(sess)
|
||||
}
|
||||
|
||||
func ecsCredProvider(sess *session.Session, uri string) credentials.Provider {
|
||||
const host = `169.254.170.2`
|
||||
|
||||
d := defaults.Get()
|
||||
return endpointcreds.NewProviderClient(
|
||||
*d.Config,
|
||||
d.Handlers,
|
||||
fmt.Sprintf("http://%s%s", host, uri),
|
||||
func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute })
|
||||
}
|
||||
|
||||
func ec2RoleProvider(sess *session.Session) credentials.Provider {
|
||||
return &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute}
|
||||
}
|
||||
|
@ -8,6 +8,10 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
nullString = "null"
|
||||
)
|
||||
|
||||
// Float is a nullable float64.
|
||||
// It does not consider zero values to be null.
|
||||
// It will decode to null, not zero, if null.
|
||||
@ -68,7 +72,7 @@ func (f *Float) UnmarshalJSON(data []byte) error {
|
||||
// It will return an error if the input is not an integer, blank, or "null".
|
||||
func (f *Float) UnmarshalText(text []byte) error {
|
||||
str := string(text)
|
||||
if str == "" || str == "null" {
|
||||
if str == "" || str == nullString {
|
||||
f.Valid = false
|
||||
return nil
|
||||
}
|
||||
@ -82,7 +86,7 @@ func (f *Float) UnmarshalText(text []byte) error {
|
||||
// It will encode null if this Float is null.
|
||||
func (f Float) MarshalJSON() ([]byte, error) {
|
||||
if !f.Valid {
|
||||
return []byte("null"), nil
|
||||
return []byte(nullString), nil
|
||||
}
|
||||
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
|
||||
}
|
||||
@ -100,7 +104,7 @@ func (f Float) MarshalText() ([]byte, error) {
|
||||
// It will encode a blank string if this Float is null.
|
||||
func (f Float) String() string {
|
||||
if !f.Valid {
|
||||
return "null"
|
||||
return nullString
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%1.3f", f.Float64)
|
||||
@ -109,7 +113,7 @@ func (f Float) String() string {
|
||||
// FullString returns float as string in full precision
|
||||
func (f Float) FullString() string {
|
||||
if !f.Valid {
|
||||
return "null"
|
||||
return nullString
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%f", f.Float64)
|
||||
|
@ -435,11 +435,6 @@ func (sc *scenarioContext) withValidApiKey() *scenarioContext {
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) withInvalidApiKey() *scenarioContext {
|
||||
sc.apiKey = "nvalidhhhhds"
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext {
|
||||
sc.authHeader = authHeader
|
||||
return sc
|
||||
|
@ -75,7 +75,7 @@ type Alert struct {
|
||||
|
||||
EvalData *simplejson.Json
|
||||
NewStateDate time.Time
|
||||
StateChanges int
|
||||
StateChanges int64
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
@ -156,7 +156,7 @@ type SetAlertStateCommand struct {
|
||||
Error string
|
||||
EvalData *simplejson.Json
|
||||
|
||||
Timestamp time.Time
|
||||
Result Alert
|
||||
}
|
||||
|
||||
//Queries
|
||||
|
@ -8,8 +8,18 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified")
|
||||
ErrJournalingNotFound = errors.New("alert notification journaling not found")
|
||||
ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified")
|
||||
ErrAlertNotificationStateNotFound = errors.New("alert notification state not found")
|
||||
ErrAlertNotificationStateVersionConflict = errors.New("alert notification state update version conflict")
|
||||
ErrAlertNotificationStateAlreadyExist = errors.New("alert notification state already exists.")
|
||||
)
|
||||
|
||||
type AlertNotificationStateType string
|
||||
|
||||
var (
|
||||
AlertNotificationStatePending = AlertNotificationStateType("pending")
|
||||
AlertNotificationStateCompleted = AlertNotificationStateType("completed")
|
||||
AlertNotificationStateUnknown = AlertNotificationStateType("unknown")
|
||||
)
|
||||
|
||||
type AlertNotification struct {
|
||||
@ -76,33 +86,34 @@ type GetAllAlertNotificationsQuery struct {
|
||||
Result []*AlertNotification
|
||||
}
|
||||
|
||||
type AlertNotificationJournal struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
AlertId int64
|
||||
NotifierId int64
|
||||
SentAt int64
|
||||
Success bool
|
||||
type AlertNotificationState struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
AlertId int64
|
||||
NotifierId int64
|
||||
State AlertNotificationStateType
|
||||
Version int64
|
||||
UpdatedAt int64
|
||||
AlertRuleStateUpdatedVersion int64
|
||||
}
|
||||
|
||||
type RecordNotificationJournalCommand struct {
|
||||
OrgId int64
|
||||
AlertId int64
|
||||
NotifierId int64
|
||||
SentAt int64
|
||||
Success bool
|
||||
type SetAlertNotificationStateToPendingCommand struct {
|
||||
Id int64
|
||||
AlertRuleStateUpdatedVersion int64
|
||||
Version int64
|
||||
|
||||
ResultVersion int64
|
||||
}
|
||||
|
||||
type GetLatestNotificationQuery struct {
|
||||
type SetAlertNotificationStateToCompleteCommand struct {
|
||||
Id int64
|
||||
Version int64
|
||||
}
|
||||
|
||||
type GetOrCreateNotificationStateQuery struct {
|
||||
OrgId int64
|
||||
AlertId int64
|
||||
NotifierId int64
|
||||
|
||||
Result *AlertNotificationJournal
|
||||
}
|
||||
|
||||
type CleanNotificationJournalCommand struct {
|
||||
OrgId int64
|
||||
AlertId int64
|
||||
NotifierId int64
|
||||
Result *AlertNotificationState
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ const (
|
||||
DS_MSSQL = "mssql"
|
||||
DS_ACCESS_DIRECT = "direct"
|
||||
DS_ACCESS_PROXY = "proxy"
|
||||
DS_STACKDRIVER = "stackdriver"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -71,12 +72,12 @@ var knownDatasourcePlugins = map[string]bool{
|
||||
DS_POSTGRES: true,
|
||||
DS_MYSQL: true,
|
||||
DS_MSSQL: true,
|
||||
DS_STACKDRIVER: true,
|
||||
"opennms": true,
|
||||
"abhisant-druid-datasource": true,
|
||||
"dalmatinerdb-datasource": true,
|
||||
"gnocci": true,
|
||||
"zabbix": true,
|
||||
"alexanderzobnin-zabbix-datasource": true,
|
||||
"newrelic-app": true,
|
||||
"grafana-datadog-datasource": true,
|
||||
"grafana-simple-json": true,
|
||||
@ -89,6 +90,7 @@ var knownDatasourcePlugins = map[string]bool{
|
||||
"ayoungprogrammer-finance-datasource": true,
|
||||
"monasca-datasource": true,
|
||||
"vertamedia-clickhouse-datasource": true,
|
||||
"alexanderzobnin-zabbix-datasource": true,
|
||||
}
|
||||
|
||||
func IsKnownDataSourcePlugin(dsType string) bool {
|
||||
|
@ -23,12 +23,13 @@ type AppPlugin struct {
|
||||
}
|
||||
|
||||
type AppPluginRoute struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
ReqRole models.RoleType `json:"reqRole"`
|
||||
Url string `json:"url"`
|
||||
Headers []AppPluginRouteHeader `json:"headers"`
|
||||
TokenAuth *JwtTokenAuth `json:"tokenAuth"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
ReqRole models.RoleType `json:"reqRole"`
|
||||
Url string `json:"url"`
|
||||
Headers []AppPluginRouteHeader `json:"headers"`
|
||||
TokenAuth *JwtTokenAuth `json:"tokenAuth"`
|
||||
JwtTokenAuth *JwtTokenAuth `json:"jwtTokenAuth"`
|
||||
}
|
||||
|
||||
type AppPluginRouteHeader struct {
|
||||
@ -36,8 +37,11 @@ type AppPluginRouteHeader struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// JwtTokenAuth struct is both for normal Token Auth and JWT Token Auth with
|
||||
// an uploaded JWT file.
|
||||
type JwtTokenAuth struct {
|
||||
Url string `json:"url"`
|
||||
Scopes []string `json:"scopes"`
|
||||
Params map[string]string `json:"params"`
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@ package alerting
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type EvalHandler interface {
|
||||
@ -20,7 +22,7 @@ type Notifier interface {
|
||||
NeedsImage() bool
|
||||
|
||||
// ShouldNotify checks this evaluation should send an alert notification
|
||||
ShouldNotify(ctx context.Context, evalContext *EvalContext) bool
|
||||
ShouldNotify(ctx context.Context, evalContext *EvalContext, notificationState *models.AlertNotificationState) bool
|
||||
|
||||
GetNotifierId() int64
|
||||
GetIsDefault() bool
|
||||
@ -28,11 +30,16 @@ type Notifier interface {
|
||||
GetFrequency() time.Duration
|
||||
}
|
||||
|
||||
type NotifierSlice []Notifier
|
||||
type notifierState struct {
|
||||
notifier Notifier
|
||||
state *models.AlertNotificationState
|
||||
}
|
||||
|
||||
func (notifiers NotifierSlice) ShouldUploadImage() bool {
|
||||
for _, notifier := range notifiers {
|
||||
if notifier.NeedsImage() {
|
||||
type notifierStateSlice []*notifierState
|
||||
|
||||
func (notifiers notifierStateSlice) ShouldUploadImage() bool {
|
||||
for _, ns := range notifiers {
|
||||
if ns.notifier.NeedsImage() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,15 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/imguploader"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
@ -40,61 +39,78 @@ type notificationService struct {
|
||||
}
|
||||
|
||||
func (n *notificationService) SendIfNeeded(context *EvalContext) error {
|
||||
notifiers, err := n.getNeededNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||
notifierStates, err := n.getNeededNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(notifiers) == 0 {
|
||||
if len(notifierStates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if notifiers.ShouldUploadImage() {
|
||||
if notifierStates.ShouldUploadImage() {
|
||||
if err = n.uploadImage(context); err != nil {
|
||||
n.log.Error("Failed to upload alert panel image.", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return n.sendNotifications(context, notifiers)
|
||||
return n.sendNotifications(context, notifierStates)
|
||||
}
|
||||
|
||||
func (n *notificationService) sendNotifications(evalContext *EvalContext, notifiers []Notifier) error {
|
||||
for _, notifier := range notifiers {
|
||||
not := notifier
|
||||
func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *notifierState) error {
|
||||
notifier := notifierState.notifier
|
||||
|
||||
err := bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error {
|
||||
n.log.Debug("trying to send notification", "id", not.GetNotifierId())
|
||||
n.log.Debug("Sending notification", "type", notifier.GetType(), "id", notifier.GetNotifierId(), "isDefault", notifier.GetIsDefault())
|
||||
metrics.M_Alerting_Notification_Sent.WithLabelValues(notifier.GetType()).Inc()
|
||||
|
||||
// Verify that we can send the notification again
|
||||
// but this time within the same transaction.
|
||||
if !evalContext.IsTestRun && !not.ShouldNotify(context.Background(), evalContext) {
|
||||
return nil
|
||||
}
|
||||
err := notifier.Notify(evalContext)
|
||||
|
||||
n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
|
||||
metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc()
|
||||
if err != nil {
|
||||
n.log.Error("failed to send notification", "id", notifier.GetNotifierId(), "error", err)
|
||||
}
|
||||
|
||||
//send notification
|
||||
success := not.Notify(evalContext) == nil
|
||||
if evalContext.IsTestRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
if evalContext.IsTestRun {
|
||||
return nil
|
||||
}
|
||||
cmd := &m.SetAlertNotificationStateToCompleteCommand{
|
||||
Id: notifierState.state.Id,
|
||||
Version: notifierState.state.Version,
|
||||
}
|
||||
|
||||
//write result to db.
|
||||
cmd := &m.RecordNotificationJournalCommand{
|
||||
OrgId: evalContext.Rule.OrgId,
|
||||
AlertId: evalContext.Rule.Id,
|
||||
NotifierId: not.GetNotifierId(),
|
||||
SentAt: time.Now().Unix(),
|
||||
Success: success,
|
||||
}
|
||||
return bus.DispatchCtx(evalContext.Ctx, cmd)
|
||||
}
|
||||
|
||||
return bus.DispatchCtx(ctx, cmd)
|
||||
})
|
||||
func (n *notificationService) sendNotification(evalContext *EvalContext, notifierState *notifierState) error {
|
||||
if !evalContext.IsTestRun {
|
||||
setPendingCmd := &m.SetAlertNotificationStateToPendingCommand{
|
||||
Id: notifierState.state.Id,
|
||||
Version: notifierState.state.Version,
|
||||
AlertRuleStateUpdatedVersion: evalContext.Rule.StateChanges,
|
||||
}
|
||||
|
||||
err := bus.DispatchCtx(evalContext.Ctx, setPendingCmd)
|
||||
if err == m.ErrAlertNotificationStateVersionConflict {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
n.log.Error("failed to send notification", "id", not.GetNotifierId())
|
||||
return err
|
||||
}
|
||||
|
||||
// We need to update state version to be able to log
|
||||
// unexpected version conflicts when marking notifications as ok
|
||||
notifierState.state.Version = setPendingCmd.ResultVersion
|
||||
}
|
||||
|
||||
return n.sendAndMarkAsComplete(evalContext, notifierState)
|
||||
}
|
||||
|
||||
func (n *notificationService) sendNotifications(evalContext *EvalContext, notifierStates notifierStateSlice) error {
|
||||
for _, notifierState := range notifierStates {
|
||||
err := n.sendNotification(evalContext, notifierState)
|
||||
if err != nil {
|
||||
n.log.Error("failed to send notification", "id", notifierState.notifier.GetNotifierId(), "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,11 +124,12 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
}
|
||||
|
||||
renderOpts := rendering.Opts{
|
||||
Width: 1000,
|
||||
Height: 500,
|
||||
Timeout: alertTimeout / 2,
|
||||
OrgId: context.Rule.OrgId,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
Width: 1000,
|
||||
Height: 500,
|
||||
Timeout: alertTimeout / 2,
|
||||
OrgId: context.Rule.OrgId,
|
||||
OrgRole: m.ROLE_ADMIN,
|
||||
ConcurrentLimit: setting.AlertingRenderLimit,
|
||||
}
|
||||
|
||||
ref, err := context.GetDashboardUID()
|
||||
@ -140,22 +157,38 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (NotifierSlice, error) {
|
||||
func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (notifierStateSlice, error) {
|
||||
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
|
||||
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []Notifier
|
||||
var result notifierStateSlice
|
||||
for _, notification := range query.Result {
|
||||
not, err := n.createNotifierFor(notification)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
n.log.Error("Could not create notifier", "notifier", notification.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if not.ShouldNotify(evalContext.Ctx, evalContext) {
|
||||
result = append(result, not)
|
||||
query := &m.GetOrCreateNotificationStateQuery{
|
||||
NotifierId: notification.Id,
|
||||
AlertId: evalContext.Rule.Id,
|
||||
OrgId: evalContext.Rule.OrgId,
|
||||
}
|
||||
|
||||
err = bus.DispatchCtx(evalContext.Ctx, query)
|
||||
if err != nil {
|
||||
n.log.Error("Could not get notification state.", "notifier", notification.Id, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if not.ShouldNotify(evalContext.Ctx, evalContext, query.Result) {
|
||||
result = append(result, ¬ifierState{
|
||||
notifier: not,
|
||||
state: query.Result,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ type AlertmanagerNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *AlertmanagerNotifier) ShouldNotify(ctx context.Context, evalContext *alerting.EvalContext) bool {
|
||||
func (this *AlertmanagerNotifier) ShouldNotify(ctx context.Context, evalContext *alerting.EvalContext, notificationState *m.AlertNotificationState) bool {
|
||||
this.log.Debug("Should notify", "ruleId", evalContext.Rule.Id, "state", evalContext.Rule.State, "previousState", evalContext.PrevAlertState)
|
||||
|
||||
// Do not notify when we become OK for the first time.
|
||||
|
@ -4,13 +4,16 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
const (
|
||||
triggMetrString = "Triggered metrics:\n\n"
|
||||
)
|
||||
|
||||
type NotifierBase struct {
|
||||
Name string
|
||||
Type string
|
||||
@ -42,55 +45,47 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase {
|
||||
}
|
||||
}
|
||||
|
||||
func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, lastNotify time.Time) bool {
|
||||
// ShouldNotify checks this evaluation should send an alert notification
|
||||
func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalContext, notiferState *models.AlertNotificationState) bool {
|
||||
// Only notify on state change.
|
||||
if context.PrevAlertState == context.Rule.State && !sendReminder {
|
||||
if context.PrevAlertState == context.Rule.State && !n.SendReminder {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notify if interval has not elapsed
|
||||
if sendReminder && !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) {
|
||||
return false
|
||||
}
|
||||
if context.PrevAlertState == context.Rule.State && n.SendReminder {
|
||||
// Do not notify if interval has not elapsed
|
||||
lastNotify := time.Unix(notiferState.UpdatedAt, 0)
|
||||
if notiferState.UpdatedAt != 0 && lastNotify.Add(n.Frequency).After(time.Now()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notify if alert state if OK or pending even on repeated notify
|
||||
if sendReminder && (context.Rule.State == models.AlertStateOK || context.Rule.State == models.AlertStatePending) {
|
||||
return false
|
||||
// Do not notify if alert state is OK or pending even on repeated notify
|
||||
if context.Rule.State == models.AlertStateOK || context.Rule.State == models.AlertStatePending {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Do not notify when we become OK for the first time.
|
||||
if (context.PrevAlertState == models.AlertStatePending) && (context.Rule.State == models.AlertStateOK) {
|
||||
if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notify when we OK -> Pending
|
||||
if context.PrevAlertState == models.AlertStateOK && context.Rule.State == models.AlertStatePending {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notifu if state pending and it have been updated last minute
|
||||
if notiferState.State == models.AlertNotificationStatePending {
|
||||
lastUpdated := time.Unix(notiferState.UpdatedAt, 0)
|
||||
if lastUpdated.Add(1 * time.Minute).After(time.Now()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ShouldNotify checks this evaluation should send an alert notification
|
||||
func (n *NotifierBase) ShouldNotify(ctx context.Context, c *alerting.EvalContext) bool {
|
||||
cmd := &models.GetLatestNotificationQuery{
|
||||
OrgId: c.Rule.OrgId,
|
||||
AlertId: c.Rule.Id,
|
||||
NotifierId: n.Id,
|
||||
}
|
||||
|
||||
err := bus.DispatchCtx(ctx, cmd)
|
||||
if err == models.ErrJournalingNotFound {
|
||||
return true
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if !cmd.Result.Success {
|
||||
return true
|
||||
}
|
||||
|
||||
return defaultShouldNotify(c, n.SendReminder, n.Frequency, time.Unix(cmd.Result.SentAt, 0))
|
||||
}
|
||||
|
||||
func (n *NotifierBase) GetType() string {
|
||||
return n.Type
|
||||
}
|
||||
|
@ -2,12 +2,9 @@ package notifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
@ -15,100 +12,144 @@ import (
|
||||
)
|
||||
|
||||
func TestShouldSendAlertNotification(t *testing.T) {
|
||||
tnow := time.Now()
|
||||
|
||||
tcs := []struct {
|
||||
name string
|
||||
prevState m.AlertStateType
|
||||
newState m.AlertStateType
|
||||
expected bool
|
||||
sendReminder bool
|
||||
frequency time.Duration
|
||||
state *m.AlertNotificationState
|
||||
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
name: "pending -> ok should not trigger an notification",
|
||||
newState: m.AlertStatePending,
|
||||
prevState: m.AlertStateOK,
|
||||
expected: false,
|
||||
name: "pending -> ok should not trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStatePending,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "ok -> alerting should trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateAlerting,
|
||||
expected: true,
|
||||
name: "ok -> alerting should trigger an notification",
|
||||
newState: m.AlertStateAlerting,
|
||||
prevState: m.AlertStateOK,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "ok -> pending should not trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStatePending,
|
||||
expected: false,
|
||||
name: "ok -> pending should not trigger an notification",
|
||||
newState: m.AlertStatePending,
|
||||
prevState: m.AlertStateOK,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "ok -> ok should not trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateOK,
|
||||
expected: false,
|
||||
sendReminder: false,
|
||||
},
|
||||
{
|
||||
name: "ok -> alerting should not trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateAlerting,
|
||||
expected: true,
|
||||
sendReminder: true,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "ok -> ok with reminder should not trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateOK,
|
||||
expected: false,
|
||||
sendReminder: true,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "alerting -> ok should trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateAlerting,
|
||||
sendReminder: false,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "alerting -> ok should trigger an notification when reminders enabled",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateAlerting,
|
||||
frequency: time.Minute * 10,
|
||||
sendReminder: true,
|
||||
state: &m.AlertNotificationState{UpdatedAt: tnow.Add(-time.Minute).Unix()},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "alerting -> alerting with reminder and no state should trigger",
|
||||
newState: m.AlertStateAlerting,
|
||||
prevState: m.AlertStateAlerting,
|
||||
frequency: time.Minute * 10,
|
||||
sendReminder: true,
|
||||
state: &m.AlertNotificationState{},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "alerting -> alerting with reminder and last notification sent 1 minute ago should not trigger",
|
||||
newState: m.AlertStateAlerting,
|
||||
prevState: m.AlertStateAlerting,
|
||||
frequency: time.Minute * 10,
|
||||
sendReminder: true,
|
||||
state: &m.AlertNotificationState{UpdatedAt: tnow.Add(-time.Minute).Unix()},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "alerting -> alerting with reminder and last notifciation sent 11 minutes ago should trigger",
|
||||
newState: m.AlertStateAlerting,
|
||||
prevState: m.AlertStateAlerting,
|
||||
frequency: time.Minute * 10,
|
||||
sendReminder: true,
|
||||
state: &m.AlertNotificationState{UpdatedAt: tnow.Add(-11 * time.Minute).Unix()},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "OK -> alerting with notifciation state pending and updated 30 seconds ago should not trigger",
|
||||
newState: m.AlertStateAlerting,
|
||||
prevState: m.AlertStateOK,
|
||||
state: &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-30 * time.Second).Unix()},
|
||||
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "OK -> alerting with notifciation state pending and updated 2 minutes ago should trigger",
|
||||
newState: m.AlertStateAlerting,
|
||||
prevState: m.AlertStateOK,
|
||||
state: &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-2 * time.Minute).Unix()},
|
||||
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
||||
State: tc.newState,
|
||||
State: tc.prevState,
|
||||
})
|
||||
|
||||
evalContext.Rule.State = tc.prevState
|
||||
if defaultShouldNotify(evalContext, true, 0, time.Now()) != tc.expected {
|
||||
t.Errorf("failed %s. expected %+v to return %v", tc.name, tc, tc.expected)
|
||||
evalContext.Rule.State = tc.newState
|
||||
nb := &NotifierBase{SendReminder: tc.sendReminder, Frequency: tc.frequency}
|
||||
|
||||
if nb.ShouldNotify(evalContext.Ctx, evalContext, tc.state) != tc.expect {
|
||||
t.Errorf("failed test %s.\n expected \n%+v \nto return: %v", tc.name, tc, tc.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) {
|
||||
Convey("base notifier", t, func() {
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
notifier := NewNotifierBase(&m.AlertNotification{
|
||||
Id: 1,
|
||||
Name: "name",
|
||||
Type: "email",
|
||||
Settings: simplejson.New(),
|
||||
})
|
||||
evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{})
|
||||
|
||||
Convey("should notify if no journaling is found", func() {
|
||||
bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error {
|
||||
return m.ErrJournalingNotFound
|
||||
})
|
||||
|
||||
if !notifier.ShouldNotify(context.Background(), evalContext) {
|
||||
t.Errorf("should send notifications when ErrJournalingNotFound is returned")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("should not notify query returns error", func() {
|
||||
bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error {
|
||||
return errors.New("some kind of error unknown error")
|
||||
})
|
||||
|
||||
if notifier.ShouldNotify(context.Background(), evalContext) {
|
||||
t.Errorf("should not send notifications when query returns error")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBaseNotifier(t *testing.T) {
|
||||
Convey("default constructor for notifiers", t, func() {
|
||||
bJson := simplejson.New()
|
||||
|
@ -61,7 +61,7 @@ func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
state := evalContext.Rule.State
|
||||
|
||||
customData := "Triggered metrics:\n\n"
|
||||
customData := triggMetrString
|
||||
for _, evt := range evalContext.EvalMatches {
|
||||
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err
|
||||
return err
|
||||
}
|
||||
|
||||
customData := "Triggered metrics:\n\n"
|
||||
customData := triggMetrString
|
||||
for _, evt := range evalContext.EvalMatches {
|
||||
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
if evalContext.Rule.State == m.AlertStateOK {
|
||||
eventType = "resolve"
|
||||
}
|
||||
customData := "Triggered metrics:\n\n"
|
||||
customData := triggMetrString
|
||||
for _, evt := range evalContext.EvalMatches {
|
||||
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@ -52,11 +53,12 @@ func TestTelegramNotifier(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("generateCaption should generate a message with all pertinent details", func() {
|
||||
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message.",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
evalContext := alerting.NewEvalContext(context.Background(),
|
||||
&alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message.",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
|
||||
caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
|
||||
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
|
||||
@ -68,11 +70,12 @@ func TestTelegramNotifier(t *testing.T) {
|
||||
Convey("When generating a message", func() {
|
||||
|
||||
Convey("URL should be skipped if it's too long", func() {
|
||||
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message.",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
evalContext := alerting.NewEvalContext(context.Background(),
|
||||
&alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message.",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
|
||||
caption := generateImageCaption(evalContext,
|
||||
"http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
@ -85,11 +88,12 @@ func TestTelegramNotifier(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Message should be trimmed if it's too long", func() {
|
||||
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it.",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
evalContext := alerting.NewEvalContext(context.Background(),
|
||||
&alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it.",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
|
||||
caption := generateImageCaption(evalContext,
|
||||
"http://grafa.url/foo",
|
||||
@ -101,11 +105,12 @@ func TestTelegramNotifier(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Metrics should be skipped if they don't fit", func() {
|
||||
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
evalContext := alerting.NewEvalContext(context.Background(),
|
||||
&alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
|
||||
caption := generateImageCaption(evalContext,
|
||||
"http://grafa.url/foo",
|
||||
|
@ -67,6 +67,12 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
}
|
||||
|
||||
handler.log.Error("Failed to save state", "error", err)
|
||||
} else {
|
||||
|
||||
// StateChanges is used for de duping alert notifications
|
||||
// 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
|
||||
}
|
||||
|
||||
// save annotation
|
||||
@ -88,19 +94,6 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
}
|
||||
}
|
||||
|
||||
if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK {
|
||||
for _, notifierId := range evalContext.Rule.Notifications {
|
||||
cmd := &m.CleanNotificationJournalCommand{
|
||||
AlertId: evalContext.Rule.Id,
|
||||
NotifierId: notifierId,
|
||||
OrgId: evalContext.Rule.OrgId,
|
||||
}
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
handler.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.notifier.SendIfNeeded(evalContext)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ type Rule struct {
|
||||
State m.AlertStateType
|
||||
Conditions []Condition
|
||||
Notifications []int64
|
||||
|
||||
StateChanges int64
|
||||
}
|
||||
|
||||
type ValidationError struct {
|
||||
@ -100,6 +102,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
||||
model.State = ruleDef.State
|
||||
model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data"))
|
||||
model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
|
||||
model.StateChanges = ruleDef.StateChanges
|
||||
|
||||
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
|
||||
jsonModel := simplejson.NewFromAny(v)
|
||||
|
@ -39,7 +39,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return notifier.sendNotifications(createTestEvalContext(cmd), []Notifier{notifiers})
|
||||
return notifier.sendNotifications(createTestEvalContext(cmd), notifierStateSlice{{notifier: notifiers}})
|
||||
}
|
||||
|
||||
func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext {
|
||||
|
@ -37,10 +37,6 @@ func NewTicker(last time.Time, initialOffset time.Duration, c clock.Clock) *Tick
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Ticker) updateOffset(offset time.Duration) {
|
||||
t.newOffset <- offset
|
||||
}
|
||||
|
||||
func (t *Ticker) run() {
|
||||
for {
|
||||
next := t.last.Add(time.Duration(1) * time.Second)
|
||||
|
@ -9,12 +9,6 @@ import (
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type testTriggeredAlert struct {
|
||||
ActualValue float64
|
||||
Name string
|
||||
State string
|
||||
}
|
||||
|
||||
func TestNotifications(t *testing.T) {
|
||||
|
||||
Convey("Given the notifications service", t, func() {
|
||||
|
@ -83,7 +83,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
|
||||
}
|
||||
|
||||
if dashboards[i].UpdateIntervalSeconds == 0 {
|
||||
dashboards[i].UpdateIntervalSeconds = 3
|
||||
dashboards[i].UpdateIntervalSeconds = 10
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,7 @@ func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) {
|
||||
So(len(ds.Options), ShouldEqual, 1)
|
||||
So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
|
||||
So(ds.DisableDeletion, ShouldBeTrue)
|
||||
So(ds.UpdateIntervalSeconds, ShouldEqual, 10)
|
||||
So(ds.UpdateIntervalSeconds, ShouldEqual, 15)
|
||||
|
||||
ds2 := cfg[1]
|
||||
So(ds2.Name, ShouldEqual, "default")
|
||||
@ -81,5 +81,5 @@ func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) {
|
||||
So(len(ds2.Options), ShouldEqual, 1)
|
||||
So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
|
||||
So(ds2.DisableDeletion, ShouldBeFalse)
|
||||
So(ds2.UpdateIntervalSeconds, ShouldEqual, 3)
|
||||
So(ds2.UpdateIntervalSeconds, ShouldEqual, 10)
|
||||
}
|
||||
|
@ -43,26 +43,6 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
|
||||
log.Warn("[Deprecated] The folder property is deprecated. Please use path instead.")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
log.Error("Cannot read directory", "error", err)
|
||||
}
|
||||
|
||||
copy := path
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error("Could not create absolute path ", "path", path)
|
||||
}
|
||||
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
log.Error("Failed to read content of symlinked path: %s", path)
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
path = copy
|
||||
log.Info("falling back to original path due to EvalSymlink/Abs failure")
|
||||
}
|
||||
|
||||
return &fileReader{
|
||||
Cfg: cfg,
|
||||
Path: path,
|
||||
@ -99,7 +79,8 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (fr *fileReader) startWalkingDisk() error {
|
||||
if _, err := os.Stat(fr.Path); err != nil {
|
||||
resolvedPath := fr.resolvePath(fr.Path)
|
||||
if _, err := os.Stat(resolvedPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
@ -116,7 +97,7 @@ func (fr *fileReader) startWalkingDisk() error {
|
||||
}
|
||||
|
||||
filesFoundOnDisk := map[string]os.FileInfo{}
|
||||
err = filepath.Walk(fr.Path, createWalkFn(filesFoundOnDisk))
|
||||
err = filepath.Walk(resolvedPath, createWalkFn(filesFoundOnDisk))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -156,7 +137,7 @@ func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs ma
|
||||
cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId}
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
fr.log.Error("failed to delete dashboard", "id", cmd.Id)
|
||||
fr.log.Error("failed to delete dashboard", "id", cmd.Id, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -344,6 +325,29 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) resolvePath(path string) string {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
fr.log.Error("Cannot read directory", "error", err)
|
||||
}
|
||||
|
||||
copy := path
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
fr.log.Error("Could not create absolute path ", "path", path)
|
||||
}
|
||||
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
fr.log.Error("Failed to read content of symlinked path: %s", path)
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
path = copy
|
||||
fr.log.Info("falling back to original path due to EvalSymlink/Abs failure")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
type provisioningMetadata struct {
|
||||
uid string
|
||||
title string
|
||||
|
@ -30,10 +30,11 @@ func TestProvsionedSymlinkedFolder(t *testing.T) {
|
||||
want, err := filepath.Abs(containingId)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expected err to be nill")
|
||||
t.Errorf("expected err to be nil")
|
||||
}
|
||||
|
||||
if reader.Path != want {
|
||||
t.Errorf("got %s want %s", reader.Path, want)
|
||||
resolvedPath := reader.resolvePath(reader.Path)
|
||||
if resolvedPath != want {
|
||||
t.Errorf("got %s want %s", resolvedPath, want)
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,8 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(filepath.IsAbs(reader.Path), ShouldBeTrue)
|
||||
resolvedPath := reader.resolvePath(reader.Path)
|
||||
So(filepath.IsAbs(resolvedPath), ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ providers:
|
||||
folder: 'developers'
|
||||
editable: true
|
||||
disableDeletion: true
|
||||
updateIntervalSeconds: 10
|
||||
updateIntervalSeconds: 15
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
@ -3,7 +3,7 @@
|
||||
folder: 'developers'
|
||||
editable: true
|
||||
disableDeletion: true
|
||||
updateIntervalSeconds: 10
|
||||
updateIntervalSeconds: 15
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
@ -13,15 +13,16 @@ var ErrNoRenderer = errors.New("No renderer plugin found nor is an external rend
|
||||
var ErrPhantomJSNotInstalled = errors.New("PhantomJS executable not found")
|
||||
|
||||
type Opts struct {
|
||||
Width int
|
||||
Height int
|
||||
Timeout time.Duration
|
||||
OrgId int64
|
||||
UserId int64
|
||||
OrgRole models.RoleType
|
||||
Path string
|
||||
Encoding string
|
||||
Timezone string
|
||||
Width int
|
||||
Height int
|
||||
Timeout time.Duration
|
||||
OrgId int64
|
||||
UserId int64
|
||||
OrgRole models.RoleType
|
||||
Path string
|
||||
Encoding string
|
||||
Timezone string
|
||||
ConcurrentLimit int
|
||||
}
|
||||
|
||||
type RenderResult struct {
|
||||
|
@ -24,12 +24,13 @@ func init() {
|
||||
}
|
||||
|
||||
type RenderingService struct {
|
||||
log log.Logger
|
||||
pluginClient *plugin.Client
|
||||
grpcPlugin pluginModel.RendererPlugin
|
||||
pluginInfo *plugins.RendererPlugin
|
||||
renderAction renderFunc
|
||||
domain string
|
||||
log log.Logger
|
||||
pluginClient *plugin.Client
|
||||
grpcPlugin pluginModel.RendererPlugin
|
||||
pluginInfo *plugins.RendererPlugin
|
||||
renderAction renderFunc
|
||||
domain string
|
||||
inProgressCount int
|
||||
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
}
|
||||
@ -90,6 +91,18 @@ func (rs *RenderingService) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResult, error) {
|
||||
if rs.inProgressCount > opts.ConcurrentLimit {
|
||||
return &RenderResult{
|
||||
FilePath: filepath.Join(setting.HomePath, "public/img/rendering_limit.png"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
rs.inProgressCount -= 1
|
||||
}()
|
||||
|
||||
rs.inProgressCount += 1
|
||||
|
||||
if rs.renderAction != nil {
|
||||
return rs.renderAction(ctx, opts)
|
||||
} else {
|
||||
|
@ -60,6 +60,10 @@ func deleteAlertByIdInternal(alertId int64, reason string, sess *DBSession) erro
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("DELETE FROM alert_notification_state WHERE alert_id = ?", alertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -275,6 +279,8 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
|
||||
}
|
||||
|
||||
sess.ID(alert.Id).Update(&alert)
|
||||
|
||||
cmd.Result = alert
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package sqlstore
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@ -18,16 +19,23 @@ func init() {
|
||||
bus.AddHandler("sql", DeleteAlertNotification)
|
||||
bus.AddHandler("sql", GetAlertNotificationsToSend)
|
||||
bus.AddHandler("sql", GetAllAlertNotifications)
|
||||
bus.AddHandlerCtx("sql", RecordNotificationJournal)
|
||||
bus.AddHandlerCtx("sql", GetLatestNotification)
|
||||
bus.AddHandlerCtx("sql", CleanNotificationJournal)
|
||||
bus.AddHandlerCtx("sql", GetOrCreateAlertNotificationState)
|
||||
bus.AddHandlerCtx("sql", SetAlertNotificationStateToCompleteCommand)
|
||||
bus.AddHandlerCtx("sql", SetAlertNotificationStateToPendingCommand)
|
||||
}
|
||||
|
||||
func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?"
|
||||
_, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
|
||||
return err
|
||||
if _, err := sess.Exec(sql, cmd.OrgId, cmd.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("DELETE FROM alert_notification_state WHERE alert_notification_state.org_id = ? AND alert_notification_state.notifier_id = ?", cmd.OrgId, cmd.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@ -229,46 +237,123 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJournalCommand) error {
|
||||
func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToCompleteCommand) error {
|
||||
return inTransactionCtx(ctx, func(sess *DBSession) error {
|
||||
journalEntry := &m.AlertNotificationJournal{
|
||||
OrgId: cmd.OrgId,
|
||||
AlertId: cmd.AlertId,
|
||||
NotifierId: cmd.NotifierId,
|
||||
SentAt: cmd.SentAt,
|
||||
Success: cmd.Success,
|
||||
}
|
||||
version := cmd.Version
|
||||
var current m.AlertNotificationState
|
||||
sess.ID(cmd.Id).Get(¤t)
|
||||
|
||||
_, err := sess.Insert(journalEntry)
|
||||
return err
|
||||
})
|
||||
}
|
||||
newVersion := cmd.Version + 1
|
||||
|
||||
func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error {
|
||||
return inTransactionCtx(ctx, func(sess *DBSession) error {
|
||||
nj := &m.AlertNotificationJournal{}
|
||||
sql := `UPDATE alert_notification_state SET
|
||||
state = ?,
|
||||
version = ?,
|
||||
updated_at = ?
|
||||
WHERE
|
||||
id = ?`
|
||||
|
||||
_, err := sess.Desc("alert_notification_journal.sent_at").
|
||||
Limit(1).
|
||||
Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(nj)
|
||||
_, err := sess.Exec(sql, m.AlertNotificationStateCompleted, newVersion, timeNow().Unix(), cmd.Id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if nj.AlertId == 0 && nj.Id == 0 && nj.NotifierId == 0 && nj.OrgId == 0 {
|
||||
return m.ErrJournalingNotFound
|
||||
if current.Version != version {
|
||||
sqlog.Error("notification state out of sync. the notification is marked as complete but has been modified between set as pending and completion.", "notifierId", current.NotifierId)
|
||||
}
|
||||
|
||||
cmd.Result = nj
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CleanNotificationJournal(ctx context.Context, cmd *m.CleanNotificationJournalCommand) error {
|
||||
return inTransactionCtx(ctx, func(sess *DBSession) error {
|
||||
sql := "DELETE FROM alert_notification_journal WHERE alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?"
|
||||
_, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId)
|
||||
return err
|
||||
func SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToPendingCommand) error {
|
||||
return withDbSession(ctx, func(sess *DBSession) error {
|
||||
newVersion := cmd.Version + 1
|
||||
sql := `UPDATE alert_notification_state SET
|
||||
state = ?,
|
||||
version = ?,
|
||||
updated_at = ?,
|
||||
alert_rule_state_updated_version = ?
|
||||
WHERE
|
||||
id = ? AND
|
||||
(version = ? OR alert_rule_state_updated_version < ?)`
|
||||
|
||||
res, err := sess.Exec(sql,
|
||||
m.AlertNotificationStatePending,
|
||||
newVersion,
|
||||
timeNow().Unix(),
|
||||
cmd.AlertRuleStateUpdatedVersion,
|
||||
cmd.Id,
|
||||
cmd.Version,
|
||||
cmd.AlertRuleStateUpdatedVersion)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
affected, _ := res.RowsAffected()
|
||||
if affected == 0 {
|
||||
return m.ErrAlertNotificationStateVersionConflict
|
||||
}
|
||||
|
||||
cmd.ResultVersion = newVersion
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetOrCreateAlertNotificationState(ctx context.Context, cmd *m.GetOrCreateNotificationStateQuery) error {
|
||||
return inTransactionCtx(ctx, func(sess *DBSession) error {
|
||||
nj := &m.AlertNotificationState{}
|
||||
|
||||
exist, err := getAlertNotificationState(sess, cmd, nj)
|
||||
|
||||
// if exists, return it, otherwise create it with default values
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exist {
|
||||
cmd.Result = nj
|
||||
return nil
|
||||
}
|
||||
|
||||
notificationState := &m.AlertNotificationState{
|
||||
OrgId: cmd.OrgId,
|
||||
AlertId: cmd.AlertId,
|
||||
NotifierId: cmd.NotifierId,
|
||||
State: m.AlertNotificationStateUnknown,
|
||||
UpdatedAt: timeNow().Unix(),
|
||||
}
|
||||
|
||||
if _, err := sess.Insert(notificationState); err != nil {
|
||||
if dialect.IsUniqueConstraintViolation(err) {
|
||||
exist, err = getAlertNotificationState(sess, cmd, nj)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exist {
|
||||
return errors.New("Should not happen")
|
||||
}
|
||||
|
||||
cmd.Result = nj
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Result = notificationState
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func getAlertNotificationState(sess *DBSession, cmd *m.GetOrCreateNotificationStateQuery, nj *m.AlertNotificationState) (bool, error) {
|
||||
return sess.
|
||||
Where("alert_notification_state.org_id = ?", cmd.OrgId).
|
||||
Where("alert_notification_state.alert_id = ?", cmd.AlertId).
|
||||
Where("alert_notification_state.notifier_id = ?", cmd.NotifierId).
|
||||
Get(nj)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
@ -14,50 +14,133 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
Convey("Testing Alert notification sql access", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
Convey("Alert notification journal", func() {
|
||||
var alertId int64 = 5
|
||||
var orgId int64 = 5
|
||||
var notifierId int64 = 5
|
||||
Convey("Alert notification state", func() {
|
||||
var alertID int64 = 7
|
||||
var orgID int64 = 5
|
||||
var notifierID int64 = 10
|
||||
oldTimeNow := timeNow
|
||||
now := time.Date(2018, 9, 30, 0, 0, 0, 0, time.UTC)
|
||||
timeNow = func() time.Time { return now }
|
||||
|
||||
Convey("Getting last journal should raise error if no one exists", func() {
|
||||
query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId}
|
||||
err := GetLatestNotification(context.Background(), query)
|
||||
So(err, ShouldEqual, m.ErrJournalingNotFound)
|
||||
Convey("Get no existing state should create a new state", func() {
|
||||
query := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
|
||||
err := GetOrCreateAlertNotificationState(context.Background(), query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldNotBeNil)
|
||||
So(query.Result.State, ShouldEqual, "unknown")
|
||||
So(query.Result.Version, ShouldEqual, 0)
|
||||
So(query.Result.UpdatedAt, ShouldEqual, now.Unix())
|
||||
|
||||
Convey("shoulbe be able to record two journaling events", func() {
|
||||
createCmd := &m.RecordNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId, Success: true, SentAt: 1}
|
||||
|
||||
err := RecordNotificationJournal(context.Background(), createCmd)
|
||||
Convey("Get existing state should not create a new state", func() {
|
||||
query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
|
||||
err := GetOrCreateAlertNotificationState(context.Background(), query2)
|
||||
So(err, ShouldBeNil)
|
||||
So(query2.Result, ShouldNotBeNil)
|
||||
So(query2.Result.Id, ShouldEqual, query.Result.Id)
|
||||
So(query2.Result.UpdatedAt, ShouldEqual, now.Unix())
|
||||
})
|
||||
|
||||
createCmd.SentAt += 1000 //increase epoch
|
||||
Convey("Update existing state to pending with correct version should update database", func() {
|
||||
s := *query.Result
|
||||
|
||||
err = RecordNotificationJournal(context.Background(), createCmd)
|
||||
cmd := models.SetAlertNotificationStateToPendingCommand{
|
||||
Id: s.Id,
|
||||
Version: s.Version,
|
||||
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
|
||||
}
|
||||
|
||||
err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.ResultVersion, ShouldEqual, 1)
|
||||
|
||||
Convey("get last journaling event", func() {
|
||||
err := GetLatestNotification(context.Background(), query)
|
||||
query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
|
||||
err = GetOrCreateAlertNotificationState(context.Background(), query2)
|
||||
So(err, ShouldBeNil)
|
||||
So(query2.Result.Version, ShouldEqual, 1)
|
||||
So(query2.Result.State, ShouldEqual, models.AlertNotificationStatePending)
|
||||
So(query2.Result.UpdatedAt, ShouldEqual, now.Unix())
|
||||
|
||||
Convey("Update existing state to completed should update database", func() {
|
||||
s := *query.Result
|
||||
setStateCmd := models.SetAlertNotificationStateToCompleteCommand{
|
||||
Id: s.Id,
|
||||
Version: cmd.ResultVersion,
|
||||
}
|
||||
err := SetAlertNotificationStateToCompleteCommand(context.Background(), &setStateCmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.SentAt, ShouldEqual, 1001)
|
||||
|
||||
Convey("be able to clear all journaling for an notifier", func() {
|
||||
cmd := &m.CleanNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId}
|
||||
err := CleanNotificationJournal(context.Background(), cmd)
|
||||
So(err, ShouldBeNil)
|
||||
query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
|
||||
err = GetOrCreateAlertNotificationState(context.Background(), query3)
|
||||
So(err, ShouldBeNil)
|
||||
So(query3.Result.Version, ShouldEqual, 2)
|
||||
So(query3.Result.State, ShouldEqual, models.AlertNotificationStateCompleted)
|
||||
So(query3.Result.UpdatedAt, ShouldEqual, now.Unix())
|
||||
})
|
||||
|
||||
Convey("querying for last junaling should raise error", func() {
|
||||
query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId}
|
||||
err := GetLatestNotification(context.Background(), query)
|
||||
So(err, ShouldEqual, m.ErrJournalingNotFound)
|
||||
})
|
||||
})
|
||||
Convey("Update existing state to completed should update database. regardless of version", func() {
|
||||
s := *query.Result
|
||||
unknownVersion := int64(1000)
|
||||
cmd := models.SetAlertNotificationStateToCompleteCommand{
|
||||
Id: s.Id,
|
||||
Version: unknownVersion,
|
||||
}
|
||||
err := SetAlertNotificationStateToCompleteCommand(context.Background(), &cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
|
||||
err = GetOrCreateAlertNotificationState(context.Background(), query3)
|
||||
So(err, ShouldBeNil)
|
||||
So(query3.Result.Version, ShouldEqual, unknownVersion+1)
|
||||
So(query3.Result.State, ShouldEqual, models.AlertNotificationStateCompleted)
|
||||
So(query3.Result.UpdatedAt, ShouldEqual, now.Unix())
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Update existing state to pending with incorrect version should return version mismatch error", func() {
|
||||
s := *query.Result
|
||||
s.Version = 1000
|
||||
cmd := models.SetAlertNotificationStateToPendingCommand{
|
||||
Id: s.NotifierId,
|
||||
Version: s.Version,
|
||||
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
|
||||
}
|
||||
err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
|
||||
So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict)
|
||||
})
|
||||
|
||||
Convey("Updating existing state to pending with incorrect version since alert rule state update version is higher", func() {
|
||||
s := *query.Result
|
||||
cmd := models.SetAlertNotificationStateToPendingCommand{
|
||||
Id: s.Id,
|
||||
Version: s.Version,
|
||||
AlertRuleStateUpdatedVersion: 1000,
|
||||
}
|
||||
err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(cmd.ResultVersion, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("different version and same alert state change version should return error", func() {
|
||||
s := *query.Result
|
||||
s.Version = 1000
|
||||
cmd := models.SetAlertNotificationStateToPendingCommand{
|
||||
Id: s.Id,
|
||||
Version: s.Version,
|
||||
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
|
||||
}
|
||||
err := SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
timeNow = oldTimeNow
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Alert notifications should be empty", func() {
|
||||
cmd := &m.GetAlertNotificationsQuery{
|
||||
cmd := &models.GetAlertNotificationsQuery{
|
||||
OrgId: 2,
|
||||
Name: "email",
|
||||
}
|
||||
@ -68,7 +151,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Cannot save alert notifier with send reminder = true", func() {
|
||||
cmd := &m.CreateAlertNotificationCommand{
|
||||
cmd := &models.CreateAlertNotificationCommand{
|
||||
Name: "ops",
|
||||
Type: "email",
|
||||
OrgId: 1,
|
||||
@ -78,7 +161,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
|
||||
Convey("and missing frequency", func() {
|
||||
err := CreateAlertNotificationCommand(cmd)
|
||||
So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound)
|
||||
So(err, ShouldEqual, models.ErrNotificationFrequencyNotFound)
|
||||
})
|
||||
|
||||
Convey("invalid frequency", func() {
|
||||
@ -90,7 +173,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Cannot update alert notifier with send reminder = false", func() {
|
||||
cmd := &m.CreateAlertNotificationCommand{
|
||||
cmd := &models.CreateAlertNotificationCommand{
|
||||
Name: "ops update",
|
||||
Type: "email",
|
||||
OrgId: 1,
|
||||
@ -101,14 +184,14 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
err := CreateAlertNotificationCommand(cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
updateCmd := &m.UpdateAlertNotificationCommand{
|
||||
updateCmd := &models.UpdateAlertNotificationCommand{
|
||||
Id: cmd.Result.Id,
|
||||
SendReminder: true,
|
||||
}
|
||||
|
||||
Convey("and missing frequency", func() {
|
||||
err := UpdateAlertNotification(updateCmd)
|
||||
So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound)
|
||||
So(err, ShouldEqual, models.ErrNotificationFrequencyNotFound)
|
||||
})
|
||||
|
||||
Convey("invalid frequency", func() {
|
||||
@ -121,7 +204,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Can save Alert Notification", func() {
|
||||
cmd := &m.CreateAlertNotificationCommand{
|
||||
cmd := &models.CreateAlertNotificationCommand{
|
||||
Name: "ops",
|
||||
Type: "email",
|
||||
OrgId: 1,
|
||||
@ -143,7 +226,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Can update alert notification", func() {
|
||||
newCmd := &m.UpdateAlertNotificationCommand{
|
||||
newCmd := &models.UpdateAlertNotificationCommand{
|
||||
Name: "NewName",
|
||||
Type: "webhook",
|
||||
OrgId: cmd.Result.OrgId,
|
||||
@ -159,7 +242,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Can update alert notification to disable sending of reminders", func() {
|
||||
newCmd := &m.UpdateAlertNotificationCommand{
|
||||
newCmd := &models.UpdateAlertNotificationCommand{
|
||||
Name: "NewName",
|
||||
Type: "webhook",
|
||||
OrgId: cmd.Result.OrgId,
|
||||
@ -174,12 +257,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Can search using an array of ids", func() {
|
||||
cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
cmd1 := models.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
cmd2 := models.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
cmd3 := models.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
cmd4 := models.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
|
||||
otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
otherOrg := models.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
|
||||
So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil)
|
||||
So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil)
|
||||
@ -188,7 +271,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil)
|
||||
|
||||
Convey("search", func() {
|
||||
query := &m.GetAlertNotificationsToSendQuery{
|
||||
query := &models.GetAlertNotificationsToSendQuery{
|
||||
Ids: []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231},
|
||||
OrgId: 1,
|
||||
}
|
||||
@ -199,7 +282,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("all", func() {
|
||||
query := &m.GetAllAlertNotificationsQuery{
|
||||
query := &models.GetAllAlertNotificationsQuery{
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
|
@ -932,29 +932,6 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
dashboardGuardianMock *guardian.FakeDashboardGuardian
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
||||
|
||||
func dashboardGuardianScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) {
|
||||
Convey(desc, func() {
|
||||
origNewDashboardGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(mock)
|
||||
|
||||
sc := &scenarioContext{
|
||||
dashboardGuardianMock: mock,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
guardian.New = origNewDashboardGuardian
|
||||
}()
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
type dashboardPermissionScenarioContext struct {
|
||||
dashboardGuardianMock *guardian.FakeDashboardGuardian
|
||||
}
|
||||
|
@ -107,4 +107,27 @@ func addAlertMigrations(mg *Migrator) {
|
||||
|
||||
mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal))
|
||||
mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0]))
|
||||
|
||||
mg.AddMigration("drop alert_notification_journal", NewDropTableMigration("alert_notification_journal"))
|
||||
|
||||
alert_notification_state := Table{
|
||||
Name: "alert_notification_state",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "alert_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "notifier_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "state", Type: DB_NVarchar, Length: 50, Nullable: false},
|
||||
{Name: "version", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "updated_at", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "alert_rule_state_updated_version", Type: DB_BigInt, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: UniqueIndex},
|
||||
},
|
||||
}
|
||||
|
||||
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]))
|
||||
}
|
||||
|
@ -44,6 +44,8 @@ type Dialect interface {
|
||||
|
||||
CleanDB() error
|
||||
NoOpSql() string
|
||||
|
||||
IsUniqueConstraintViolation(err error) bool
|
||||
}
|
||||
|
||||
func NewDialect(engine *xorm.Engine) Dialect {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user