mirror of https://github.com/grafana/grafana
commit
30fca2b181
@ -0,0 +1,9 @@ |
||||
apiVersion: 1 |
||||
|
||||
providers: |
||||
- name: 'Bulk alerting dashboards' |
||||
folder: 'Bulk alerting dashboards' |
||||
type: file |
||||
options: |
||||
path: devenv/bulk_alerting_dashboards |
||||
|
@ -0,0 +1,168 @@ |
||||
{ |
||||
"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": [] |
||||
}, |
||||
"aliasColors": {}, |
||||
"bars": false, |
||||
"dashLength": 10, |
||||
"dashes": false, |
||||
"datasource": "gdev-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 |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@ |
||||
grafana/provisioning/dashboards/alerts/alert-* |
@ -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 |
||||
``` |
@ -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 "$@" |
@ -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 |
@ -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 |
@ -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 |
@ -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>" |
||||
``` |
@ -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) |
||||
} |
@ -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") |
||||
}) |
||||
}) |
||||
} |
@ -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 |
||||
} |
@ -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") |
||||
}) |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue