mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into rebrand-docs
This commit is contained in:
commit
25318f5848
67
CHANGELOG.md
67
CHANGELOG.md
@ -1,22 +1,70 @@
|
||||
# 4.2.0 (unreleased)
|
||||
# 4.3.0 (unreleased)
|
||||
|
||||
## Minor Enchancements
|
||||
|
||||
# 4.2.0-beta2 (unreleased)
|
||||
## Minor Enhancements
|
||||
* **Templates**: Prevent use of the prefix `__` for templates in web UI [#7678](https://github.com/grafana/grafana/issues/7678)
|
||||
* **Threema**: Add emoji to Threema alert notifications [#7676](https://github.com/grafana/grafana/pull/7676) thx [@dbrgn](https://github.com/dbrgn)
|
||||
* **Panels**: Support dm3 unit [#7695](https://github.com/grafana/grafana/issues/7695) thx [@mitjaziv](https://github.com/mitjaziv)
|
||||
* **Docs**: Added some details about Sessions in Postgres [#7694](https://github.com/grafana/grafana/pull/7694) thx [@rickard-von-essen](https://github.com/rickard-von-essen)
|
||||
* **Influxdb**: Allow commas in template variables [#7681](https://github.com/grafana/grafana/issues/7681) thx [@thuck](https://github.com/thuck)
|
||||
* **Cloudwatch**: stop using deprecated session.New() [#7736](https://github.com/grafana/grafana/issues/7736) thx [@mtanda](https://github.com/mtanda)
|
||||
* **OpenTSDB**: Pass dropcounter rate option if no max counter and no reset value or reset value as 0 is specified [#7743](https://github.com/grafana/grafana/pull/7743) thx [@r4um](https://github.com/r4um)
|
||||
* **Templating**: support full resolution for $interval variable [#7696](https://github.com/grafana/grafana/pull/7696) thx [@mtanda](https://github.com/mtanda)
|
||||
* **Elasticsearch**: Unique Count on string fields in ElasticSearch [#3536](https://github.com/grafana/grafana/issues/3536), thx [@pyro2927](https://github.com/pyro2927)
|
||||
* **Templating**: Data source template variable that refers to other variable in regex filter [#6365](https://github.com/grafana/grafana/issues/6365) thx [@rlodge](https://github.com/rlodge)
|
||||
* **Admin**: Global User List: add search and pagination [#7469](https://github.com/grafana/grafana/issues/7469)
|
||||
|
||||
## Bugfixes
|
||||
* **Webhook**: Use proxy settings from environment variables [#7710](https://github.com/grafana/grafana/issues/7710)
|
||||
* **Panels**: Deleting a dashboard with unsaved changes raises an error message [#7591](https://github.com/grafana/grafana/issues/7591) thx [@thuck](https://github.com/thuck)
|
||||
* **Influxdb**: Query builder detects regex to easily for measurement [#7276](https://github.com/grafana/grafana/issues/7276) thx [@thuck](https://github.com/thuck)
|
||||
* **Docs**: router_logging not documented [#7723](https://github.com/grafana/grafana/issues/7723)
|
||||
* **Alerting**: Spelling mistake [#7739](https://github.com/grafana/grafana/pull/7739) thx [@woutersmit](https://github.com/woutersmit)
|
||||
* **Alerting**: Graph legend scrolls to top when an alias is toggled/clicked [#7680](https://github.com/grafana/grafana/issues/7680) thx [@p4ddy1](https://github.com/p4ddy1)
|
||||
|
||||
# 4.2.0-beta1 (2017-02-27)
|
||||
|
||||
## Enhancements
|
||||
* **Alerting**: Added Telegram alert notifier [#7098](https://github.com/grafana/grafana/pull/7098), thx [@leonoff](https://github.com/leonoff)
|
||||
* **Telegram**: Added Telegram alert notifier [#7098](https://github.com/grafana/grafana/pull/7098), thx [@leonoff](https://github.com/leonoff)
|
||||
* **Templating**: Make $__interval and $__interval_ms global built in variables that can be used in by any datasource (in panel queries), closes [#7190](https://github.com/grafana/grafana/issues/7190), closes [#6582](https://github.com/grafana/grafana/issues/6582)
|
||||
* **S3 Image Store**: External s3 image store (used in alert notifications) now support AWS IAM Roles, closes [#6985](https://github.com/grafana/grafana/issues/6985), [#7058](https://github.com/grafana/grafana/issues/7058) thx [@mtanda](https://github.com/mtanda)
|
||||
* **Optimzation**: Never issue refresh event when Grafana tab is not visible [#7218](https://github.com/grafana/grafana/issues/7218), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Browser History**: Browser back/forward now works time ranges / zoom, [#7259](https://github.com/grafana/grafana/issues/7259)
|
||||
* **SingleStat**: Implements diff aggregation method for singlestat [#7234](https://github.com/grafana/grafana/issues/7234), thx [@oliverpool](https://github.com/oliverpool)
|
||||
* **Dataproxy**: Added setting to enable more verbose logging in dataproxy [#7209](https://github.com/grafana/grafana/pull/7209), thx [@Ricky-N](https://github.com/Ricky-N)
|
||||
* **Alerting**: Better information about why an alert triggered [#7035](https://github.com/grafana/grafana/issues/7035)
|
||||
* **LINE**: Add LINE as alerting notification channel [#7301](https://github.com/grafana/grafana/pull/7301), thx [@huydx](https://github.com/huydx)
|
||||
* **LINE**: Adds image to notification message [#7417](https://github.com/grafana/grafana/pull/7417), thx [@Erliz](https://github.com/Erliz)
|
||||
* **Hipchat**: Adds support for sending alert notifications to hipchat [#6451](https://github.com/grafana/grafana/issues/6451), thx [@jregovic](https://github.com/jregovic)
|
||||
* **Alerting**: Uploading images for alert notifications is now optional [#7419](https://github.com/grafana/grafana/issues/7419)
|
||||
* **Dashboard**: Adds shortcut for collapsing/expanding all rows [#552](https://github.com/grafana/grafana/issues/552), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Alerting**: Adds de duping of alert notifications [#7632](https://github.com/grafana/grafana/pull/7632)
|
||||
* **Orgs**: Sharing dashboards using Grafana share feature will now redirect to correct org. [#1613](https://github.com/grafana/grafana/issues/1613)
|
||||
* **Pushover**: Add Pushover alert notifications [#7526](https://github.com/grafana/grafana/pull/7526) thx [@devkid](https://github.com/devkid)
|
||||
* **Threema**: Add Threema Gateway alert notification integration [#7482](https://github.com/grafana/grafana/pull/7482) thx [@dbrgn](https://github.com/dbrgn)
|
||||
|
||||
## Minor Enhancements
|
||||
* **Optimzation**: Never issue refresh event when Grafana tab is not visible [#7218](https://github.com/grafana/grafana/issues/7218), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Browser History**: Browser back/forward now works time ranges / zoom, [#7259](https://github.com/grafana/grafana/issues/7259)
|
||||
* **Elasticsearch**: Support for Min Doc Count options in Terms aggregation [#7324](https://github.com/grafana/grafana/pull/7324), thx [@lpic10](https://github.com/lpic10)
|
||||
* **Elasticsearch**: Term aggregation limit can now be changed in template queries [#7112](https://github.com/grafana/grafana/issues/7112), thx [@FFalcon](https://github.com/FFalcon)
|
||||
* **LINE**: Adds image to notification message [#7417](https://github.com/grafana/grafana/pull/7417), thx [@Erliz](https://github.com/Erliz)
|
||||
* **Elasticsearch**: Ad-hoc filters now support all operators [#7612](https://github.com/grafana/grafana/issues/7612), thx [@tamayika](https://github.com/tamayika)
|
||||
* **Graph**: Add full series name as title for legends. [#7493](https://github.com/grafana/grafana/pull/7493), thx [@kolobaev](https://github.com/kolobaev)
|
||||
* **Table**: Add a message when queries returns no data. [#6109](https://github.com/grafana/grafana/issues/6109), thx [@xginn8](https://github.com/xginn8)
|
||||
* **Graph**: Set max width for series names in legend tables. [#2385](https://github.com/grafana/grafana/issues/2385), thx [@kolobaev](https://github.com/kolobaev)
|
||||
* **Database**: Allow max db connection pool configuration [#7427](https://github.com/grafana/grafana/issues/7427), thx [@huydx](https://github.com/huydx)
|
||||
* **Datasources** Delete datsource by name [#7476](https://github.com/grafana/grafana/issues/7476), thx [@huydx](https://github.com/huydx)
|
||||
* **Dataproxy**: Only allow get that begins with api/ to access Prometheus [#7459](https://github.com/grafana/grafana/pull/7459), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Snapshot**: Make timeout for snapshot creation configurable [#7449](https://github.com/grafana/grafana/pull/7449) thx [@ryu1-sakai](https://github.com/ryu1-sakai)
|
||||
* **Panels**: Add more physics units [#7554](https://github.com/grafana/grafana/pull/7554) thx [@ryantxu](https://github.com/ryantxu)
|
||||
* **Email**: Add sender's name on email [#2131](https://github.com/grafana/grafana/issues/2131) thx [@jacobbednarz](https://github.com/jacobbednarz)
|
||||
* **HTTPS**: Set tls 1.2 as lowest tls version. [#7347](https://github.com/grafana/grafana/pull/7347) thx [@roman-vynar](https://github.com/roman-vynar)
|
||||
* **Table**: Added suppressing of empty results to table plugin. [#7602](https://github.com/grafana/grafana/pull/7602) thx [@LLIyRiK](https://github.com/LLIyRiK)
|
||||
|
||||
## Tech
|
||||
|
||||
* **Library Upgrade**: Upgraded angularjs from 1.5.8 to 1.6.1 [#7274](https://github.com/grafana/grafana/issues/7274)
|
||||
* **Backend**: Grafana is now built using golang 1.8
|
||||
|
||||
## Bugfixes
|
||||
* **Alerting**: Fixes missing support for no_data and execution error when testing alerts [#7149](https://github.com/grafana/grafana/issues/7149)
|
||||
@ -24,11 +72,18 @@
|
||||
* **Alertlist**: Only show scrollbar when required [#7269](https://github.com/grafana/grafana/issues/7269)
|
||||
* **SMTP**: Set LocalName to hostname [#7223](https://github.com/grafana/grafana/issues/7223)
|
||||
* **Sidemenu**: Disable sign out in sidemenu for AuthProxyEnabled [#7377](https://github.com/grafana/grafana/pull/7377), thx [@solugebefola](https://github.com/solugebefola)
|
||||
* **Prometheus**: Add support for basic auth in Prometheus tsdb package [#6799](https://github.com/grafana/grafana/issues/6799), thx [@hagen1778](https://github.com/hagen1778)
|
||||
* **OAuth**: Redirect to original page when logging in with OAuth [#7513](https://github.com/grafana/grafana/issues/7513)
|
||||
* **Annotations**: Wrap text in annotations tooltip [#7542](https://github.com/grafana/grafana/pull/7542), thx [@xginn8](https://github.com/xginn8)
|
||||
* **Templating**: Fixes error when using numeric sort on empty strings [#7382](https://github.com/grafana/grafana/issues/7382)
|
||||
* **Templating**: Fixed issue detecting template variable dependency [#7354](https://github.com/grafana/grafana/issues/7354)
|
||||
|
||||
# 4.1.2 (unreleased)
|
||||
# 4.1.2 (2017-02-13)
|
||||
|
||||
### Bugfixes
|
||||
* **Table**: Fixes broken annotation rendering mode in the table panel [#7268](https://github.com/grafana/grafana/issues/7268)
|
||||
* **Data Sources**: Sorting for lists of data sources in UI is now case insensitive [#7491](https://github.com/grafana/grafana/issues/7491)
|
||||
* **Admin**: Support more then 1000 users in global users list [#7469](https://github.com/grafana/grafana/issues/7469)
|
||||
|
||||
# 4.1.1 (2017-01-11)
|
||||
|
||||
|
4
Makefile
4
Makefile
@ -4,9 +4,9 @@ deps-go:
|
||||
go run build.go setup
|
||||
|
||||
deps-js:
|
||||
yarn install --pure-lockfile
|
||||
yarn install --pure-lockfile --no-progress
|
||||
|
||||
deps: deps-go deps-js
|
||||
deps: deps-js
|
||||
|
||||
build-go:
|
||||
go run build.go build
|
||||
|
@ -80,7 +80,7 @@ the latest master builds [here](http://grafana.org/builds)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Go 1.7.3
|
||||
- Go 1.8
|
||||
- NodeJS v4+
|
||||
|
||||
### Get Code
|
||||
|
11
appveyor.yml
11
appveyor.yml
@ -7,13 +7,14 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
|
||||
environment:
|
||||
nodejs_version: "6"
|
||||
GOPATH: c:\gopath
|
||||
GOVERSION: 1.7.4
|
||||
GOVERSION: 1.8
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
# install nodejs and npm
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- npm install -g yarn
|
||||
- yarn install --pure-lockfile
|
||||
- npm install -g yarn --silent
|
||||
- yarn install --pure-lockfile --no-progress
|
||||
- npm install -g grunt-cli
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.zip
|
||||
- 7z x go%GOVERSION%.windows-amd64.zip -y -oC:\ > NUL
|
||||
@ -24,12 +25,12 @@ install:
|
||||
- echo %GOPATH%
|
||||
- go version
|
||||
- go env
|
||||
- go run build.go setup
|
||||
# - go run build.go setup
|
||||
|
||||
build_script:
|
||||
- go run build.go build
|
||||
- grunt release
|
||||
- go run build.go sha1-dist
|
||||
- go run build.go sha-dist
|
||||
- cp dist/* .
|
||||
|
||||
artifacts:
|
||||
|
47
build.go
47
build.go
@ -5,7 +5,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
@ -20,6 +20,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"path"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -42,7 +43,7 @@ var (
|
||||
binaries []string = []string{"grafana-server", "grafana-cli"}
|
||||
)
|
||||
|
||||
const minGoVersion = 1.7
|
||||
const minGoVersion = 1.8
|
||||
|
||||
func main() {
|
||||
log.SetOutput(os.Stdout)
|
||||
@ -104,8 +105,8 @@ func main() {
|
||||
grunt(gruntBuildArg("release")...)
|
||||
createDebPackages()
|
||||
|
||||
case "sha1-dist":
|
||||
sha1FilesInDist()
|
||||
case "sha-dist":
|
||||
shaFilesInDist()
|
||||
|
||||
case "latest":
|
||||
makeLatestDistCopies()
|
||||
@ -120,14 +121,24 @@ func main() {
|
||||
}
|
||||
|
||||
func makeLatestDistCopies() {
|
||||
rpmIteration := "-1"
|
||||
if linuxPackageIteration != "" {
|
||||
rpmIteration = linuxPackageIteration
|
||||
files, err := ioutil.ReadDir("dist")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create latest copies. Cannot read from /dist")
|
||||
}
|
||||
|
||||
runError("cp", fmt.Sprintf("dist/grafana_%v-%v_amd64.deb", linuxPackageVersion, linuxPackageIteration), "dist/grafana_latest_amd64.deb")
|
||||
runError("cp", fmt.Sprintf("dist/grafana-%v-%v.x86_64.rpm", linuxPackageVersion, rpmIteration), "dist/grafana-latest-1.x86_64.rpm")
|
||||
runError("cp", fmt.Sprintf("dist/grafana-%v-%v.linux-x64.tar.gz", linuxPackageVersion, linuxPackageIteration), "dist/grafana-latest.linux-x64.tar.gz")
|
||||
latestMapping := map[string]string {
|
||||
".deb": "dist/grafana_latest_amd64.deb",
|
||||
".rpm": "dist/grafana-latest-1.x86_64.rpm",
|
||||
".tar.gz": "dist/grafana-latest.linux-x64.tar.gz",
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
for extension, fullName := range latestMapping {
|
||||
if strings.HasSuffix(file.Name(), extension) {
|
||||
runError("cp", path.Join("dist", file.Name()), fullName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readVersionFromPackageJson() {
|
||||
@ -332,9 +343,9 @@ func grunt(params ...string) {
|
||||
func gruntBuildArg(task string) []string {
|
||||
args := []string{task}
|
||||
if includeBuildNumber {
|
||||
args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
|
||||
args = append(args, fmt.Sprintf("--pkgVer=%v-%v", version, linuxPackageIteration))
|
||||
} else {
|
||||
args = append(args, fmt.Sprintf("--pkgVer=%v", linuxPackageVersion))
|
||||
args = append(args, fmt.Sprintf("--pkgVer=%v", version))
|
||||
}
|
||||
if pkgArch != "" {
|
||||
args = append(args, fmt.Sprintf("--arch=%v", pkgArch))
|
||||
@ -511,14 +522,14 @@ func md5File(file string) error {
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
func sha1FilesInDist() {
|
||||
func shaFilesInDist() {
|
||||
filepath.Walk("./dist", func(path string, f os.FileInfo, err error) error {
|
||||
if path == "./dist" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(path, ".sha1") == false {
|
||||
err := sha1File(path)
|
||||
if strings.Contains(path, ".sha256") == false {
|
||||
err := shaFile(path)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create sha file. error: %v\n", err)
|
||||
}
|
||||
@ -527,20 +538,20 @@ func sha1FilesInDist() {
|
||||
})
|
||||
}
|
||||
|
||||
func sha1File(file string) error {
|
||||
func shaFile(file string) error {
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
h := sha1.New()
|
||||
h := sha256.New()
|
||||
_, err = io.Copy(h, fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(file + ".sha1")
|
||||
out, err := os.Create(file + ".sha256")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
10
circle.yml
10
circle.yml
@ -9,7 +9,7 @@ machine:
|
||||
GOPATH: "/home/ubuntu/.go_workspace"
|
||||
ORG_PATH: "github.com/grafana"
|
||||
REPO_PATH: "${ORG_PATH}/grafana"
|
||||
GODIST: "go1.7.4.linux-amd64.tar.gz"
|
||||
GODIST: "go1.8.linux-amd64.tar.gz"
|
||||
post:
|
||||
- mkdir -p ~/download
|
||||
- mkdir -p ~/docker
|
||||
@ -33,7 +33,7 @@ dependencies:
|
||||
|
||||
test:
|
||||
override:
|
||||
- bash scripts/circle-test.sh
|
||||
- bash scripts/circle-test.sh
|
||||
|
||||
deployment:
|
||||
gh_branch:
|
||||
@ -41,7 +41,7 @@ deployment:
|
||||
commands:
|
||||
- ./scripts/build/deploy.sh
|
||||
- ./scripts/build/sign_packages.sh
|
||||
- go run build.go sha1-dist
|
||||
- go run build.go sha-dist
|
||||
- aws s3 sync ./dist s3://$BUCKET_NAME/master
|
||||
- ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master
|
||||
- ./scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
|
||||
@ -50,7 +50,7 @@ deployment:
|
||||
commands:
|
||||
- ./scripts/build/deploy.sh
|
||||
- ./scripts/build/sign_packages.sh
|
||||
- go run build.go sha1-dist
|
||||
- go run build.go sha-dist
|
||||
- aws s3 sync ./dist s3://$BUCKET_NAME/release
|
||||
- ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release
|
||||
|
||||
- ./scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} ${CIRCLE_TAG}
|
||||
|
@ -73,6 +73,11 @@ password =
|
||||
# Example: mysql://user:secret@host:port/database
|
||||
url =
|
||||
|
||||
# Max conn setting default is 0 (mean not set)
|
||||
max_conn =
|
||||
max_idle_conn =
|
||||
max_open_conn =
|
||||
|
||||
# For "postgres", use either "disable", "require" or "verify-full"
|
||||
# For "mysql", use either "true", "false", or "skip-verify".
|
||||
ssl_mode = disable
|
||||
@ -291,6 +296,7 @@ cert_file =
|
||||
key_file =
|
||||
skip_verify = false
|
||||
from_address = admin@grafana.localhost
|
||||
from_name = Grafana
|
||||
|
||||
[emails]
|
||||
welcome_email_on_sign_up = false
|
||||
|
@ -82,6 +82,12 @@
|
||||
# For "sqlite3" only, path relative to data_path setting
|
||||
;path = grafana.db
|
||||
|
||||
# Max conn setting default is 0 (mean not set)
|
||||
;max_conn =
|
||||
;max_idle_conn =
|
||||
;max_open_conn =
|
||||
|
||||
|
||||
#################################### Session ####################################
|
||||
[session]
|
||||
# Either "memory", "file", "redis", "mysql", "postgres", default is "file"
|
||||
@ -276,6 +282,7 @@
|
||||
;key_file =
|
||||
;skip_verify = false
|
||||
;from_address = admin@grafana.localhost
|
||||
;from_name = Grafana
|
||||
|
||||
[emails]
|
||||
;welcome_email_on_sign_up = false
|
||||
|
@ -3,8 +3,6 @@ prometheus:
|
||||
net: host
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- /var/docker/prometheus:/prometheus-data
|
||||
|
||||
node_exporter:
|
||||
image: prom/node-exporter
|
||||
|
@ -10,8 +10,7 @@ weight = 8
|
||||
|
||||
# Grafana CLI
|
||||
|
||||
Grafana cli is a small executable that is bundled with grafana server and is suppose to be
|
||||
executed on the same machine as grafana runs.
|
||||
Grafana cli is a small executable that is bundled with grafana server and is suppose to be executed on the same machine as grafana runs.
|
||||
|
||||
## Plugins
|
||||
|
||||
|
@ -13,7 +13,7 @@ weight = 2
|
||||
|
||||
> Alerting is only available in Grafana v4.0 and above.
|
||||
|
||||
The alert engine publish some internal metrics about itself. You can read more about how Grafana published [interal metrics](/installation/configuration/#metrics)
|
||||
The alert engine publishes some internal metrics about itself. You can read more about how Grafana published [internal metrics](/installation/configuration/#metrics).
|
||||
|
||||
Description | Type | Metric name
|
||||
---------- | ----------- | ----------
|
||||
|
@ -101,4 +101,9 @@ config file.
|
||||
|
||||
This is an optional requirement, you can get slack and email notifications without setting this up.
|
||||
|
||||
# Configure the link back to Grafana from alert notifications
|
||||
|
||||
All alert notifications contains a link back to the triggered alert in the Grafana instance.
|
||||
This url is based on the [domain](/installation/configuration/#domain) setting in Grafana.
|
||||
|
||||
|
||||
|
104
docs/sources/contribute/cla.md
Normal file
104
docs/sources/contribute/cla.md
Normal file
@ -0,0 +1,104 @@
|
||||
+++
|
||||
title = "Contributor Licence Agreement (CLA)"
|
||||
description = "Contributer Licence Agreement (CLA)"
|
||||
type = "docs"
|
||||
aliases = ["/project/cla"]
|
||||
[menu.docs]
|
||||
parent = "contribute"
|
||||
weight = 1
|
||||
+++
|
||||
|
||||
# Grafana Labs Contributor License Agreement
|
||||
|
||||
Thank you for your interest in contributing to Grafana Labs ("We" or "Us").
|
||||
|
||||
This contributor agreement ("Agreement") documents the rights granted by contributors to Us.
|
||||
To make this document effective, please sign it following the instructions at
|
||||
[cla-assistant](https://cla-assistant.io/grafana/grafana). This is a legally binding document,
|
||||
so please read it carefully before agreeing to it. The Agreement may cover more than
|
||||
one software project managed by Us.
|
||||
|
||||
## 1. Definitions
|
||||
|
||||
"You" (Individual) means the individual who Submits a Contribution to Us.
|
||||
|
||||
"You" (Entity) means any Legal Entity on behalf of whom a Contribution has been received by Us. "Legal Entity" means an entity which is not a natural person. "Affiliates" means other Legal Entities that control, are controlled by, or under common control with that Legal Entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such Legal Entity, whether by contract or otherwise, (ii) ownership of fifty percent (50%) or more of the outstanding shares or securities which vote to elect the management or other persons who direct such Legal Entity or (iii) beneficial ownership of such entity.
|
||||
|
||||
"Contribution" means any work of authorship that is Submitted by You to Us in which You own or assert ownership of the Copyright. If You do not own the Copyright in the entire work of authorship, you must notify us at contact@grafana.com before Submitting such a Contribution.
|
||||
|
||||
"Copyright" means all rights protecting works of authorship owned or controlled by You [or Your Affiliates], including copyright, moral and neighboring rights, as appropriate, for the full term of their existence including any extensions by You.
|
||||
|
||||
"Material" means the work of authorship which is made available by Us to third parties. When this Agreement covers more than one software project, the Material means the work of authorship to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material.
|
||||
|
||||
"Submit" means any form of electronic, verbal, or written communication sent to Us or our representatives, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Material, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
|
||||
|
||||
"Submission Date" means the date on which You Submit a Contribution to Us.
|
||||
|
||||
"Effective Date" means the date You execute this Agreement or the date You first Submit a Contribution to Us, whichever is earlier.
|
||||
|
||||
"Media" means any portion of a Contribution which is not software.
|
||||
|
||||
## 2. Grant of Rights
|
||||
|
||||
### 2.1 Copyright License
|
||||
|
||||
(a) You retain ownership of the Copyright in Your Contribution and have the same rights to use or license the Contribution which You would have had without entering into the Agreement.
|
||||
|
||||
(b) To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable license under the Copyright covering the Contribution, with the right to sublicense such rights through multiple tiers of sublicensees, to reproduce, modify, display, perform and distribute the Contribution as part of the Material; provided that this license is conditioned upon compliance with Section 2.3.
|
||||
|
||||
### 2.2 Patent License
|
||||
|
||||
For patent claims including, without limitation, method, process, and apparatus claims which You [or Your Affiliates] own, control or have the right to grant, now or in the future, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable patent license, with the right to sublicense these rights to multiple tiers of sublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with the Material (and portions of such combination). This license is granted only to the extent that the exercise of the licensed rights infringes such patent claims; and provided that this license is conditioned upon compliance with Section 2.3.
|
||||
|
||||
### 2.3 Outbound License
|
||||
|
||||
As a condition on the grant of rights in Sections 2.1 and 2.2, We agree to license the Contribution only under the terms of the license or licenses which We are using on the Submission Date for the Material or any licenses which are approved by the Open Source Initiative on or after the Effective Date, including both permissive and copyleft licenses, whether or not such licenses are subsequently disapproved (including any right to adopt any future version of a license if permitted).
|
||||
|
||||
In addition, We may use the following licenses for Media in the Contribution: GNU Free Documentation License v1.3, Creative Commons Attribution 3.0, or Creative Commons Attribution No Derivatives 3.0 (including any right to adopt any future version of a license if permitted).
|
||||
|
||||
2.4 Moral Rights. If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against Us or our successors in interest, or any of our licensees, either direct or indirect.
|
||||
|
||||
2.5 Our Rights. You acknowledge that We are not obligated to use Your Contribution as part of the Material and may decide to include any Contribution We consider appropriate.
|
||||
|
||||
2.6 Reservation of Rights. Any rights not expressly [assigned or] licensed under this section are expressly reserved by You.
|
||||
|
||||
## 3. Agreement
|
||||
|
||||
You confirm that:
|
||||
|
||||
(a) You have the legal authority to enter into this Agreement.
|
||||
|
||||
(b) You [or Your Affiliates] own the Copyright and patent claims covering the Contribution which are required to grant the rights under Section 2.
|
||||
|
||||
\(c)(Individual) The grant of rights under Section 2 does not violate any grant of rights which You have made to third parties, including Your employer. If You are an employee, You have had Your employer approve this Agreement or sign the Entity version of this document. If You are less than eighteen years old, please have Your parents or guardian sign the Agreement.
|
||||
|
||||
\(c)(Entity) The grant of rights under Section 2 does not violate any grant of rights which You or Your Affiliates have made to third parties.
|
||||
|
||||
## 4. Disclaimer
|
||||
|
||||
EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US [AND BY US TO YOU]. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD PERMITTED BY LAW.
|
||||
|
||||
## 5. Consequential Damage Waiver
|
||||
|
||||
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU [OR US] BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED.
|
||||
|
||||
## 6. Miscellaneous
|
||||
|
||||
6.1 This Agreement will be governed by and construed in accordance with the laws of New York State, United States excluding its conflicts of law provisions. Under certain circumstances, the governing law in this section might be superseded by the United Nations Convention on Contracts for the International Sale of Goods ("UN Convention") and the parties intend to avoid the application of the UN Convention to this Agreement and, thus, exclude the application of the UN Convention in its entirety to this Agreement.
|
||||
|
||||
6.2 This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings.
|
||||
|
||||
6.3 If You or We assign the rights or obligations received through this Agreement to a third party, as a condition of the assignment, that third party must agree in writing to abide by all the rights and obligations in the Agreement.
|
||||
|
||||
6.4 The failure of either party to require performance by the other party of any provision of this Agreement in one situation shall not affect the right of a party to require such performance at any time in the future. A waiver of performance under a provision in one situation shall not be considered a waiver of the performance of the provision in the future or a waiver of the provision in its entirety.
|
||||
|
||||
6.5 If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and which is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law.
|
||||
|
||||
### Sign using CLA-Assistant
|
||||
|
||||
[https://cla-assistant.io/grafana/grafana](https://cla-assistant.io/grafana/grafana)
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
This CLA aggreement 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/)
|
12
docs/sources/contribute/index.md
Normal file
12
docs/sources/contribute/index.md
Normal file
@ -0,0 +1,12 @@
|
||||
+++
|
||||
title = "Contribute"
|
||||
description = "Contribute"
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Contribute"
|
||||
identifier = "contribute"
|
||||
weight = 20
|
||||
+++
|
||||
|
||||
### Contribute info
|
||||
|
@ -9,13 +9,16 @@ weight = 5
|
||||
+++
|
||||
|
||||
# Data Source Overview
|
||||
|
||||
Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes.
|
||||
|
||||
|
||||
## Querying
|
||||
|
||||
The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization.
|
||||
|
||||
## Supported Data Sources
|
||||
|
||||
The following datasources are officially supported:
|
||||
|
||||
* [Graphite]({{< relref "graphite.md" >}})
|
||||
|
@ -23,7 +23,6 @@ Grafana ships with very feature rich data source plugin for InfluxDB. Supporting
|
||||
> NOTE: If this link is missing in the side menu it means that your current user does not have the `Admin` role for the current organization.
|
||||
|
||||
3. Click the `Add new` link in the top header.
|
||||
4. Select `InfluxDB 0.9.x` or `InfluxDB 0.8.x` from the dropdown.
|
||||
|
||||
Name | Description
|
||||
------------ | -------------
|
||||
|
@ -16,7 +16,8 @@ weight = 2
|
||||
Grafana includes support for Prometheus Datasources. While the process of adding the datasource is similar to adding a Graphite or OpenTSDB datasource type, Prometheus does have a few different options for building queries.
|
||||
|
||||
## Adding the data source to Grafana
|
||||

|
||||
|
||||

|
||||
|
||||
1. Open the side menu by clicking the the Grafana icon in the top header.
|
||||
2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
|
||||
@ -49,6 +50,7 @@ For details on Prometheus metric queries check out the Prometheus documentation
|
||||
- [Query Metrics - Prometheus documentation](http://prometheus.io/docs/querying/basics/).
|
||||
|
||||
## Templated queries
|
||||
|
||||
Prometheus Datasource Plugin provides the following functions in `Variables values query` field in Templating Editor to query `metric names` and `labels names` on the Prometheus server.
|
||||
|
||||
Name | Description
|
||||
@ -65,8 +67,8 @@ For details of `metric names` & `label names`, and `label values`, please refer
|
||||
You can create a template variable in Grafana and have that variable filled with values from any Prometheus metric exploration query.
|
||||
You can then use this variable in your Prometheus metric queries.
|
||||
|
||||
For example you can have a variable that contains all values for label `hostname` if you specify a query like this
|
||||
in the templating edit view.
|
||||
For example you can have a variable that contains all values for label `hostname` if you specify a query like this in the templating edit view.
|
||||
|
||||
```sql
|
||||
label_values(hostname)
|
||||
```
|
||||
@ -78,4 +80,4 @@ You can also use raw queries & regular expressions to extract anything you might
|
||||
When the `Include All` option or `Multi-Value` option is enabled, Grafana converts the labels from plain text to a regex compatible string.
|
||||
Which means you have to use `=~` instead of `=` in your Prometheus queries. For example `ALERTS{instance=~$instance}` instead of `ALERTS{instance=$instance}`.
|
||||
|
||||

|
||||

|
||||
|
@ -14,6 +14,7 @@ parent = "guides"
|
||||
This document is a “bottom up” introduction to basic concepts in Grafana, and can be used as a starting point to get familiar with core features.
|
||||
|
||||
### Data Source
|
||||
|
||||
Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes.
|
||||
|
||||
The following datasources are officially supported: [Graphite]({{< relref "features/datasources/graphite.md" >}}), [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}), [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}), [Prometheus]({{< relref "features/datasources/prometheus.md" >}}), [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}), [CloudWatch]({{< relref "features/datasources/cloudwatch.md" >}}).
|
||||
@ -21,6 +22,7 @@ The following datasources are officially supported: [Graphite]({{< relref "featu
|
||||
The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization.
|
||||
|
||||
### Organization
|
||||
|
||||
Grafana supports multiple organizations in order to support a wide variety of deployment models, including using a single Grafana instance to provide service to multiple potentially untrusted Organizations.
|
||||
|
||||
In many cases, Grafana will be deployed with a single Organization.
|
||||
@ -34,6 +36,7 @@ All Dashboards are owned by a particular Organization.
|
||||
For more details on the user model for Grafana, please refer to [Admin](/reference/admin/)
|
||||
|
||||
### User
|
||||
|
||||
A User is a named account in Grafana. A user can belong to one or more Organizations, and can be assigned different levels of privileges through roles.
|
||||
|
||||
Grafana supports a wide variety of internal and external ways for Users to authenticate themselves. These include from its own integrated database, from an external SQL server, or from an external LDAP server.
|
||||
|
@ -10,12 +10,15 @@ parent = "guides"
|
||||
+++
|
||||
|
||||
# Getting started
|
||||
This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running and have added at least one [Data Source](/datasources/overview).
|
||||
|
||||
This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running and have added at least one [Data Source](/features/datasources/).
|
||||
|
||||
## Beginner guides
|
||||
|
||||
Watch the 10min [beginners guide to building dashboards](https://www.youtube.com/watch?v=sKNZMtoSHN4&index=7&list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2) to get a quick intro to setting up Dashboards and Panels.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
Read the [Basic Concepts](/guides/basic_concepts) document to get a crash course in key Grafana concepts.
|
||||
|
||||
### Top header
|
||||
@ -34,7 +37,8 @@ The image above shows you the top header for a Dashboard.
|
||||
6. Settings: Manage Dashboard settings and features such as Templating and Annotations.
|
||||
|
||||
## Dashboards, Panels, Rows, the building blocks of Grafana...
|
||||
Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail.
|
||||
|
||||
Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, Prometheus and Cloudwatch). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail.
|
||||
|
||||
|
||||
## Adding & Editing Graphs and Panels
|
||||
|
@ -7,7 +7,7 @@ type = "docs"
|
||||
name = "Version 4.1"
|
||||
identifier = "v4.1"
|
||||
parent = "whatsnew"
|
||||
weight = -1
|
||||
weight = 3
|
||||
+++
|
||||
|
||||
|
||||
|
88
docs/sources/guides/whats-new-in-v4-2.md
Normal file
88
docs/sources/guides/whats-new-in-v4-2.md
Normal file
@ -0,0 +1,88 @@
|
||||
+++
|
||||
title = "What's New in Grafana v4.2"
|
||||
description = "Feature & improvement highlights for Grafana v4.2"
|
||||
keywords = ["grafana", "new", "documentation", "4.2.0"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Version 4.2"
|
||||
identifier = "v4.2"
|
||||
parent = "whatsnew"
|
||||
weight = -1
|
||||
+++
|
||||
|
||||
## Whats new in Grafana v4.2
|
||||
|
||||
Grafana v4.2 Beta is now [available for download](/download/4_2_0/).
|
||||
Just like the last release this one contains lots bug fixes and minor improvements.
|
||||
We are very happy to say that 27 of 40 issues was closed by pull requests from the community.
|
||||
Big thumbs up!
|
||||
|
||||
## Release Highlights
|
||||
|
||||
- **Hipchat**: Adds support for sending alert notifications to hipchat [#6451](https://github.com/grafana/grafana/issues/6451), thx [@jregovic](https://github.com/jregovic)
|
||||
- **Telegram**: Added Telegram alert notifier [#7098](https://github.com/grafana/grafana/pull/7098), thx [@leonoff](https://github.com/leonoff)
|
||||
- **LINE**: Add LINE as alerting notification channel [#7301](https://github.com/grafana/grafana/pull/7301), thx [@huydx](https://github.com/huydx)
|
||||
- **Templating**: Make $__interval and $__interval_ms global built in variables that can be used in by any datasource (in panel queries), closes [#7190](https://github.com/grafana/grafana/issues/7190), closes [#6582](https://github.com/grafana/grafana/issues/6582)
|
||||
- **Alerting**: Adds deduping of alert notifications [#7632](https://github.com/grafana/grafana/pull/7632)
|
||||
- **Alerting**: Better information about why an alert triggered [#7035](https://github.com/grafana/grafana/issues/7035)
|
||||
- **Orgs**: Sharing dashboards using Grafana share feature will now redirect to correct org. [#6948](https://github.com/grafana/grafana/issues/6948)
|
||||
- [Full changelog](https://github.com/grafana/grafana/blob/master/CHANGELOG.md)
|
||||
|
||||
### New alert notification channels
|
||||
|
||||
This release adds **five** new alert notifications channels, all of them contributed by the community.
|
||||
|
||||
* Hipchat
|
||||
* Telegram
|
||||
* LINE
|
||||
* Pushover
|
||||
* Threema
|
||||
|
||||
### Templating
|
||||
|
||||
We added two new global built in variables in grafana. `$__interval` and `$__interval_ms` are now reserved template names in grafana and can be used by any datasource.
|
||||
We might add more global built in variables in the future and if we do we will prefix them with `$__`. So please avoid using that in your template variables.
|
||||
|
||||
### Dedupe alert notifications when running multiple servers
|
||||
|
||||
In this release we will dedupe alert notificiations 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.
|
||||
3-5 servers should not be a problem but as always, it depends on how many alerts you have and how frequently they execute.
|
||||
|
||||
Next up for a better HA situation is to add support for workload balancing between Grafana servers.
|
||||
|
||||
### Alerting more info
|
||||
|
||||
You can now see the reason why an alert triggered in the alert history. Its also easier to detect when an alert is set to `alerting` due to the `no_data` option.
|
||||
|
||||
### Improved support for multi-org setup
|
||||
|
||||
When loading dashboards we now set an query parameter called orgId. So we can detect from which org an user shared a dashboard.
|
||||
This makes it possible for users to share dashboards between orgs without changing org first.
|
||||
|
||||
We aim to introduce [dashboard groups](https://github.com/grafana/grafana/issues/1611) sometime in the future which will introduce access control and user groups within one org.
|
||||
Making it possible to have users in multiple groups and have detailed access control.
|
||||
|
||||
## Upgrade & Breaking changes
|
||||
|
||||
If your using https in grafana we now force you to use tls 1.2 and the most secure ciphers.
|
||||
We think its better to be secure by default rather then making it configurable.
|
||||
If you want to run https with lower versions of tls we suggest you put a reserve proxy in front of grafana.
|
||||
|
||||
If you have template variables name `$__interval` or `$__interval_ms` they will no longer work since these keywords
|
||||
are reserved as global built in variables. We might add more global built in variables in the future and if we do, we will prefix them with `$__`. So please avoid using that in your template variables.
|
||||
|
||||
## Changelog
|
||||
|
||||
Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list
|
||||
of new features, changes, and bug fixes.
|
||||
|
||||
## Download
|
||||
|
||||
Head to [v4.2-beta download page](/download/4_2_0/) for download links & instructions.
|
||||
|
||||
## Thanks
|
||||
|
||||
A big thanks to all the Grafana users who contribute by submitting PRs, bug reports & feedback!
|
@ -158,6 +158,7 @@ with Grafana admin permission.
|
||||
"cert_file":"",
|
||||
"enabled":"false",
|
||||
"from_address":"admin@grafana.localhost",
|
||||
"from_name":"Grafana",
|
||||
"host":"localhost:25",
|
||||
"key_file":"",
|
||||
"password":"************",
|
||||
@ -292,4 +293,4 @@ Change password for specific user
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100}
|
||||
{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100}
|
||||
|
@ -30,7 +30,7 @@ curl example:
|
||||
|
||||
Open the sidemenu and click the organization dropdown and select the `API Keys` option.
|
||||
|
||||

|
||||

|
||||
|
||||
You use the token in all requests in the `Authorization` header, like this:
|
||||
|
||||
|
@ -195,7 +195,7 @@ parent = "http_api"
|
||||
|
||||
{"message":"Datasource updated", "id": 1, "name": "test_datasource"}
|
||||
|
||||
## Delete an existing data source
|
||||
## Delete an existing data source by id
|
||||
|
||||
`DELETE /api/datasources/:datasourceId`
|
||||
|
||||
@ -213,6 +213,24 @@ parent = "http_api"
|
||||
|
||||
{"message":"Data source deleted"}
|
||||
|
||||
## Delete an existing data source by name
|
||||
|
||||
`DELETE /api/datasources/name/:datasourceName`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
DELETE /api/datasources/name/test_datasource HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Data source deleted"}
|
||||
|
||||
## Data source proxy calls
|
||||
|
||||
`GET /api/datasources/proxy/:datasourceId/*`
|
||||
|
@ -13,7 +13,7 @@ parent = "http_api"
|
||||
|
||||
## Search Users
|
||||
|
||||
`GET /api/users`
|
||||
`GET /api/users?perpage=10&page=1`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
@ -22,6 +22,8 @@ parent = "http_api"
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
@ -44,6 +46,45 @@ parent = "http_api"
|
||||
}
|
||||
]
|
||||
|
||||
## Search Users with Paging
|
||||
|
||||
`GET /api/users/search?perpage=10&page=1`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
GET /api/users/search?perpage=10&page=1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. The `totalCount` field in the response can be used for pagination of the user list E.g. if `totalCount` is equal to 100 users and the `perpage` parameter is set to 10 then there are 10 pages of users.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"totalCount": 2,
|
||||
"users": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Admin",
|
||||
"login": "admin",
|
||||
"email": "admin@mygraf.com",
|
||||
"isAdmin": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "User",
|
||||
"login": "user",
|
||||
"email": "user@mygraf.com",
|
||||
"isAdmin": false
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"perPage": 10
|
||||
}
|
||||
|
||||
## Get single user by Id
|
||||
|
||||
`GET /api/users/:id`
|
||||
@ -71,36 +112,35 @@ parent = "http_api"
|
||||
|
||||
## Get single user by Username(login) or Email
|
||||
|
||||
`GET /api/users/lookup`
|
||||
`GET /api/users/lookup?loginOrEmail=user@mygraf.com`
|
||||
|
||||
**Parameter:** `loginOrEmail`
|
||||
**Example Request using the email as option**:
|
||||
|
||||
**Example Request using the email as option**:
|
||||
GET /api/users/lookup?loginOrEmail=user@mygraf.com HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
GET /api/users/lookup?loginOrEmail=user@mygraf.com HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
**Example Request using the username as option**:
|
||||
|
||||
GET /api/users/lookup?loginOrEmail=admin HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Request using the username as option**:
|
||||
GET /api/users/lookup?loginOrEmail=admin HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
**Example Response**:
|
||||
|
||||
**Example Response**:
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "user@mygraf.com"
|
||||
"name": "admin",
|
||||
"login": "admin",
|
||||
"theme": "light",
|
||||
"orgId": 1,
|
||||
"isGrafanaAdmin": true
|
||||
}
|
||||
{
|
||||
"email": "user@mygraf.com"
|
||||
"name": "admin",
|
||||
"login": "admin",
|
||||
"theme": "light",
|
||||
"orgId": 1,
|
||||
"isGrafanaAdmin": true
|
||||
}
|
||||
|
||||
|
||||
## User Update
|
||||
|
@ -55,6 +55,7 @@ Then you can override them using:
|
||||
<hr />
|
||||
|
||||
## instance_name
|
||||
|
||||
Set the name of the grafana-server instance. Used in logging and internal metrics and in
|
||||
clustering info. Defaults to: `${HOSTNAME}`, which will be replaced with
|
||||
environment variable `HOSTNAME`, if that is empty or does not exist Grafana will try to use
|
||||
@ -134,6 +135,10 @@ Path to the certificate file (if `protocol` is set to `https`).
|
||||
|
||||
Path to the certificate key file (if `protocol` is set to `https`).
|
||||
|
||||
### router_logging
|
||||
|
||||
Set to true for Grafana to log all HTTP requests (not just errors). These are logged as Info level events
|
||||
to grafana log.
|
||||
<hr />
|
||||
|
||||
<hr />
|
||||
@ -145,6 +150,7 @@ things). By default it is configured to use `sqlite3` which is an
|
||||
embedded database (included in the main Grafana binary).
|
||||
|
||||
### url
|
||||
|
||||
Use either URL or or the other fields below to configure the database
|
||||
Example: `mysql://user:secret@host:port/database`
|
||||
|
||||
@ -231,7 +237,7 @@ Default is `false`.
|
||||
|
||||
Set to `false` to prohibit users from being able to sign up / create
|
||||
user accounts. Defaults to `true`. The admin user can still create
|
||||
users from the [Grafana Admin Pages](../reference/admin.md)
|
||||
users from the [Grafana Admin Pages](../../reference/admin)
|
||||
|
||||
### allow_org_create
|
||||
|
||||
@ -455,7 +461,7 @@ session provider you have configured.
|
||||
|
||||
- **file:** session file path, e.g. `data/sessions`
|
||||
- **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
|
||||
- **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=disable
|
||||
- **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=require
|
||||
- **memcache:** ex: 127.0.0.1:11211
|
||||
- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana`
|
||||
|
||||
@ -471,6 +477,17 @@ Mysql Example:
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
|
||||
|
||||
Postgres Example:
|
||||
|
||||
CREATE TABLE session (
|
||||
key CHAR(16) NOT NULL,
|
||||
data BYTEA,
|
||||
expiry INTEGER NOT NULL,
|
||||
PRIMARY KEY (key)
|
||||
);
|
||||
|
||||
Postgres valid `sslmode` are `disable`, `require` (default), `verify-ca`, and `verify-full`.
|
||||
|
||||
### cookie_name
|
||||
|
||||
The name of the Grafana session cookie.
|
||||
@ -540,6 +557,9 @@ Verify SSL for smtp server? defaults to `false`
|
||||
### from_address
|
||||
Address used when sending out emails, defaults to `admin@grafana.localhost`
|
||||
|
||||
### from_name
|
||||
Name to be used when sending out emails, defaults to `Grafana`
|
||||
|
||||
## [log]
|
||||
|
||||
### mode
|
||||
@ -597,13 +617,18 @@ You can choose between (s3, webdav). If left empty Grafana will ignore the uploa
|
||||
## [external_image_storage.s3]
|
||||
|
||||
### bucket_url
|
||||
bucket url for s3. ex http://grafana.s3.amazonaws.com/
|
||||
Bucket URL for S3. AWS region can be specified within URL or defaults to 'us-east-1', e.g.
|
||||
- http://grafana.s3.amazonaws.com/
|
||||
- https://grafana.s3-ap-southeast-2.amazonaws.com/
|
||||
- https://grafana.s3-cn-north-1.amazonaws.com.cn
|
||||
|
||||
### access_key
|
||||
access key. ex AAAAAAAAAAAAAAAAAAAA
|
||||
Access key. e.g. AAAAAAAAAAAAAAAAAAAA
|
||||
|
||||
Access key requires permissions to the S3 bucket for the 's3:PutObject' and 's3:PutObjectAcl' actions.
|
||||
|
||||
### secret_key
|
||||
secret key. ex AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
Secret key. e.g. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
|
||||
## [external_image_storage.webdav]
|
||||
|
||||
|
@ -15,14 +15,23 @@ weight = 1
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Stable for Debian-based Linux | [4.1.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.1-1484211277_amd64.deb)
|
||||
Stable for Debian-based Linux | [4.1.2 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.2-1486989747_amd64.deb)
|
||||
Beta for Debian-based Linux | [4.2.0-beta1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.2.0-beta1_amd64.deb)
|
||||
|
||||
## Install Stable
|
||||
|
||||
```
|
||||
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.1-1484211277_amd64.deb
|
||||
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.2-1486989747_amd64.deb
|
||||
$ sudo apt-get install -y adduser libfontconfig
|
||||
$ sudo dpkg -i grafana_4.1.1-1484211277_amd64.deb
|
||||
$ sudo dpkg -i grafana_4.1.2-1486989747_amd64.deb
|
||||
```
|
||||
|
||||
## Install Beta
|
||||
|
||||
```
|
||||
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.2.0-beta1_amd64.deb
|
||||
$ sudo apt-get install -y adduser libfontconfig
|
||||
$ sudo dpkg -i grafana_4.2.0-beta1_amd64.deb
|
||||
```
|
||||
|
||||
## APT Repository
|
||||
|
@ -15,24 +15,25 @@ weight = 2
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.1.1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.1-1484211277.x86_64.rpm)
|
||||
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.1.2 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.2-1486989747.x86_64.rpm)
|
||||
Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [4.2.0-beta1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.2.0-beta1.x86_64.rpm)
|
||||
|
||||
## Install Stable
|
||||
|
||||
You can install Grafana using Yum directly.
|
||||
|
||||
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.1-1484211277.x86_64.rpm
|
||||
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.2-1486989747.x86_64.rpm
|
||||
|
||||
Or install manually using `rpm`.
|
||||
|
||||
#### On CentOS / Fedora / Redhat:
|
||||
|
||||
$ sudo yum install initscripts fontconfig
|
||||
$ sudo rpm -Uvh grafana-4.1.1-1484211277.x86_64.rpm
|
||||
$ sudo rpm -Uvh grafana-4.1.2-1486989747.x86_64.rpm
|
||||
|
||||
#### On OpenSuse:
|
||||
|
||||
$ sudo rpm -i --nodeps grafana-4.1.1-1484211277.x86_64.rpm
|
||||
$ sudo rpm -i --nodeps grafana-4.1.2-1486989747.x86_64.rpm
|
||||
|
||||
## Install via YUM Repository
|
||||
|
||||
|
@ -13,7 +13,8 @@ weight = 3
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Latest stable package for Windows | [grafana.4.1.1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.1.windows-x64.zip)
|
||||
Latest stable package for Windows | [grafana.4.1.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.2.windows-x64.zip)
|
||||
Latest beta package for Windows | [grafana-4.2.0-beta1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.2.0-beta1.windows-x64.zip)
|
||||
|
||||
## Configure
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
---
|
||||
page_title: App plugin
|
||||
page_description: App plugin for Grafana
|
||||
page_keywords: grafana, plugins, documentation
|
||||
---
|
||||
|
||||
|
||||
# Apps
|
||||
|
||||
App plugins is a new kind of grafana plugin that can bundle datasource and panel plugins within one package. It also enable the plugin author to create custom pages within grafana. The custom pages enables the plugin author to include things like documentation, sign up forms or controlling other services using HTTP requests.
|
||||
|
||||
Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu.
|
||||
|
||||
<img class="no-shadow" src="/img/v3/app-in-main-menu.png">
|
||||
|
||||
## Enabling app plugins
|
||||
After installing an app it have to be enabled before it show up as an datasource or panel. You can do that on the app page in the config tab.
|
||||
|
||||
### Develop your own App
|
||||
|
||||
> Our goal is not to have a very extensive documentation but rather have actual
|
||||
> code that people can look at. An example implementation of an app can be found
|
||||
> in this [example app repo](https://github.com/grafana/example-app)
|
||||
|
63
docs/sources/plugins/developing/apps.md
Normal file
63
docs/sources/plugins/developing/apps.md
Normal file
@ -0,0 +1,63 @@
|
||||
+++
|
||||
title = "Developing App Plugins"
|
||||
keywords = ["grafana", "plugins", "documentation"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Developing App Plugins"
|
||||
parent = "developing"
|
||||
weight = 6
|
||||
+++
|
||||
|
||||
# Grafana Apps
|
||||
|
||||
App plugins are a new kind of grafana plugin that can bundle datasource and panel plugins within one package. It also enable the plugin author to create custom pages within grafana. The custom pages enable the plugin author to include things like documentation, sign up forms or controlling other services using HTTP requests.
|
||||
|
||||
Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu.
|
||||
|
||||
{{< imgbox img="/img/docs/v3/app-in-main-menu.png" caption="App in Main Menu" >}}
|
||||
|
||||
## Enabling app plugins
|
||||
|
||||
After installing an app, it has to be enabled before it shows up as a datasource or panel. You can do that on the app page in the config tab.
|
||||
|
||||
## Developing an App Plugin
|
||||
|
||||
An App is a bundle of panels, dashboards and/or data source(s). There is nothing different about developing panels and data sources for an app.
|
||||
|
||||
Apps have to be enabled in Grafana and should import any included dashboards when the user enables it. A ConfigCtrl class should be created and the dashboards imported in the postUpdate hook. See example below:
|
||||
|
||||
```javascript
|
||||
export class ConfigCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, $q) {
|
||||
this.$q = $q;
|
||||
this.enabled = false;
|
||||
this.appEditCtrl.setPostUpdateHook(this.postUpdate.bind(this));
|
||||
}
|
||||
|
||||
postUpdate() {
|
||||
if (!this.appModel.enabled) {
|
||||
return this.$q.resolve();
|
||||
}
|
||||
return this.appEditCtrl.importDashboards().then(() => {
|
||||
this.enabled = true;
|
||||
return {
|
||||
url: "plugins/raintank-kubernetes-app/page/clusters",
|
||||
message: "Kubernetes App enabled!"
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
ConfigCtrl.templateUrl = 'components/config/config.html';
|
||||
```
|
||||
|
||||
If possible a link to a dashboard or custom page should be shown after enabling the app to guide the user to the appropriate place.
|
||||
|
||||
{{< imgbox img="/img/docs/app_plugin_after_enable.png" caption="After enabling" >}}
|
||||
|
||||
### Develop your own App
|
||||
|
||||
> Our goal is not to have a very extensive documentation but rather have actual
|
||||
> code that people can look at. An example implementation of an app can be found
|
||||
> in this [example app repo](https://github.com/grafana/example-app)
|
||||
|
182
docs/sources/plugins/developing/code-styleguide.md
Normal file
182
docs/sources/plugins/developing/code-styleguide.md
Normal file
@ -0,0 +1,182 @@
|
||||
+++
|
||||
title = "Plugin Code Styleguide"
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Plugin Code Styleguide"
|
||||
parent = "developing"
|
||||
weight = 2
|
||||
+++
|
||||
|
||||
# Grafana Plugin Code Styleguide
|
||||
|
||||
This guide has two parts. The first part describes the metadata and the second part is a styleguide for HTML/CSS and JavaScript in Grafana plugins and applies if you are using ES6 in your plugin. If using TypeScript then the [Angular TypeScript styleguide](https://angular.io/styleguide) is recommended.
|
||||
|
||||
## Metadata
|
||||
|
||||
The plugin metadata consists of a plugin.json file and the README.md file. These two files are used by Grafana and Grafana.net.
|
||||
|
||||
### Plugin.json (mandatory)
|
||||
|
||||
The plugin.json file is the same concept as the package.json file for an npm package. When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless the folder contains a subfolder named `dist`. In that case grafana will mount the `dist` folder instead.
|
||||
|
||||
The most important fields are the first three, especially the id. The convention for the plugin id is **[github username/org]-[plugin name]-[datasource|app|panel]** and it has to be unique.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
raintank-worldping-app
|
||||
grafana-simple-json-datasource
|
||||
grafana-piechart-panel
|
||||
mtanda-histogram-panel
|
||||
```
|
||||
|
||||
The full file format for plugin.json is described [here]({{< relref "plugin.json.md" >}}).
|
||||
|
||||
Minimal plugin.json:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Clock",
|
||||
"id": "yourorg-clock-panel",
|
||||
|
||||
"info": {
|
||||
"description": "Clock panel for grafana",
|
||||
"author": {
|
||||
"name": "Raintank Inc.",
|
||||
"url": "http://raintank.io"
|
||||
},
|
||||
"keywords": ["clock", "panel"],
|
||||
"version": "1.0.0",
|
||||
"updated": "2015-03-24"
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"grafanaVersion": "3.x.x",
|
||||
"plugins": [ ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### README.md
|
||||
|
||||
The README.md file is rendered both on Grafana.net and in the plugins section in Grafana. The only difference from how GitHub renders markdown is that html is not allowed.
|
||||
|
||||
## File and Directory Structure Conventions
|
||||
|
||||
Here is a typical directory structure for a plugin.
|
||||
|
||||
```
|
||||
johnnyb-awesome-datasource
|
||||
|-- dist
|
||||
|-- spec
|
||||
| |-- datasource_spec.js
|
||||
| |-- query_ctrl_spec.js
|
||||
| |-- test-main.js
|
||||
|-- src
|
||||
| |-- img
|
||||
| | |-- logo.svg
|
||||
| |-- partials
|
||||
| | |-- annotations.editor.html
|
||||
| | |-- config.html
|
||||
| | |-- query.editor.html
|
||||
| |-- datasource.js
|
||||
| |-- module.js
|
||||
| |-- plugin.json
|
||||
| |-- query_ctrl.js
|
||||
|-- Gruntfile.js
|
||||
|-- LICENSE
|
||||
|-- package.json
|
||||
|-- README.md
|
||||
```
|
||||
|
||||
Most JavaScript projects have a build step and most Grafana plugins are built using Babel and ES6. The generated JavaScript should be placed in the `dist` directory and the source code in the `src` directory. We recommend that the plugin.json file be placed in the src directory and then copied over to the dist directory when building. The `README.md` can be placed in the root or in the dist directory.
|
||||
|
||||
Directories:
|
||||
|
||||
- `src/` contains plugin source files.
|
||||
- `src/partials` contains html templates.
|
||||
- `src/img` contains plugin logos and other images.
|
||||
- `spec/` contains tests (optional).
|
||||
- `dist/` contains built content.
|
||||
|
||||
## HTML and CSS
|
||||
|
||||
For the HTML on editor tabs, we recommend using the inbuilt Grafana styles rather than defining your own. This makes plugins feel like a more natural part of Grafana. If done correctly, the html will also be responsive and adapt to smaller screens. The `gf-form` css classes should be used for labels and inputs.
|
||||
|
||||
Below is a minimal example of an editor row with one form group and two fields, a dropdown and a text input:
|
||||
|
||||
```html
|
||||
<div class="editor-row">
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">My Plugin Options</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Label1</label>
|
||||
<div class="gf-form-select-wrapper max-width-10">
|
||||
<select class="input-small gf-form-input" ng-model="ctrl.panel.mySelectProperty" ng-options="t for t in ['option1', 'option2', 'option3']" ng-change="ctrl.onSelectChange()"></select>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Label2</label>
|
||||
<input type="text" class="input-small gf-form-input width-10" ng-model="ctrl.panel.myProperty" ng-change="ctrl.onFieldChange()" placeholder="suggestion for user" ng-model-onblur />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Use the `width-x` and `max-width-x` classes to control the width of your labels and input fields. Try to get labels and input fields to line up neatly by having the same width for all the labels in a group and the same width for all inputs in a group if possible.
|
||||
|
||||
## Build Scripts
|
||||
|
||||
Our recommendation is to use whatever you usually use - Grunt, Gulp or npm scripts. Most plugins seems to use Grunt so that is probably the easiest to get started with if you do not have a preferred build system. The only requirement is that it supports systemjs which is required by Grafana to load plugins.
|
||||
|
||||
## Linting
|
||||
|
||||
We recommend that you use a linter for your JavaScript. For ES6, the standard linter is [eslint](http://eslint.org/). Rules for linting are described in an .eslintrc that is placed in the root directory. [Here is an example](https://github.com/grafana/worldmap-panel/blob/master/.eslintrc) of linting rules in a plugin.
|
||||
|
||||
### ES6 features
|
||||
|
||||
1. Use `const` if a variable is not going to be reassigned.
|
||||
2. Prefer to use `let` instead `var` ([Exploring ES6](http://exploringjs.com/es6/ch_core-features.html#_from-var-to-letconst))
|
||||
3. Use arrow functions, which don’t shadow `this` ([Exploring ES6](http://exploringjs.com/es6/ch_core-features.html#_from-function-expressions-to-arrow-functions)):
|
||||
|
||||
```js
|
||||
testDatasource() {
|
||||
return this.getServerStatus()
|
||||
.then(status => {
|
||||
return this.doSomething(status);
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
better than
|
||||
|
||||
```js
|
||||
testDatasource() {
|
||||
var self = this;
|
||||
return this.getServerStatus()
|
||||
.then(function(status) {
|
||||
return self.doSomething(status);
|
||||
})
|
||||
}
|
||||
```
|
||||
4. Use native _Promise_ object:
|
||||
|
||||
```js
|
||||
metricFindQuery(query) {
|
||||
if (!query) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
better than
|
||||
|
||||
```js
|
||||
metricFindQuery(query) {
|
||||
if (!query) {
|
||||
return this.$q.when([]);
|
||||
}
|
||||
}
|
||||
```
|
||||
5. If using Lodash, then be consequent and prefer that to the native ES6 array functions.
|
@ -1,3 +1,12 @@
|
||||
+++
|
||||
title = "Developing Datasource Plugins"
|
||||
keywords = ["grafana", "plugins", "documentation"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Developing Datasource Plugins"
|
||||
parent = "developing"
|
||||
weight = 6
|
||||
+++
|
||||
|
||||
# Datasources
|
||||
|
||||
@ -31,9 +40,11 @@ There are two datasource specific settings for the plugin.json
|
||||
These settings indicates what kind of data the plugin can deliver. At least one of them have to be true
|
||||
|
||||
## Datasource
|
||||
|
||||
The javascript object that communicates with the database and transforms data to times series.
|
||||
|
||||
The Datasource should contain the following functions.
|
||||
The Datasource should contain the following functions:
|
||||
|
||||
```
|
||||
query(options) //used by panels to get data
|
||||
testDatasource() //used by datasource configuration page to make sure the connection is working
|
||||
@ -41,9 +52,14 @@ annotationQuery(options) // used by dashboards to get annotations
|
||||
metricFindQuery(options) // used by query editor to get metric suggestions.
|
||||
```
|
||||
|
||||
### testDatasource
|
||||
|
||||
When a user clicks on the *Save & Test* button when adding a new data source, the details are first saved to the database and then the `testDatasource` function that is defined in your data source plugin will be called. It is recommended that this function makes a query to the data source that will also test that the authentication details are correct. This is so the data source is correctly configured when the user tries to write a query in a new dashboard.
|
||||
|
||||
### Query
|
||||
|
||||
Request object passed to datasource.query function
|
||||
Request object passed to datasource.query function:
|
||||
|
||||
```json
|
||||
{
|
||||
"range": { "from": "2015-12-22T03:06:13.851Z", "to": "2015-12-22T06:48:24.137Z" },
|
||||
@ -57,11 +73,12 @@ Request object passed to datasource.query function
|
||||
}
|
||||
```
|
||||
|
||||
There are two different kind of results for datasources.
|
||||
Time series and table. Time series is the most common format and is supported by all datasources and panels. Table format is only support by the Influxdb datasource and table panel. But we might see more of this in the future.
|
||||
There are two different kinds of results for datasources;
|
||||
time series and table. Time series is the most common format and is supported by all datasources and panels. Table format is only supported by the InfluxDB datasource and table panel. But we might see more of this in the future.
|
||||
|
||||
Time series response from datasource.query.
|
||||
An array of:
|
||||
|
||||
Time series response from datasource.query
|
||||
An array of
|
||||
```json
|
||||
[
|
||||
{
|
||||
@ -81,8 +98,9 @@ An array of
|
||||
]
|
||||
```
|
||||
|
||||
Table response from datasource.query
|
||||
An array of
|
||||
Table response from datasource.query.
|
||||
An array of:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
@ -119,7 +137,8 @@ An array of
|
||||
|
||||
### Annotation Query
|
||||
|
||||
Request object passed to datasource.annotationQuery function
|
||||
Request object passed to datasource.annotationQuery function:
|
||||
|
||||
```json
|
||||
{
|
||||
"range": { "from": "2016-03-04T04:07:55.144Z", "to": "2016-03-04T07:07:55.144Z" },
|
||||
@ -132,7 +151,8 @@ Request object passed to datasource.annotationQuery function
|
||||
}
|
||||
```
|
||||
|
||||
Expected result from datasource.annotationQuery
|
||||
Expected result from datasource.annotationQuery:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
@ -152,24 +172,24 @@ Expected result from datasource.annotationQuery
|
||||
|
||||
## QueryCtrl
|
||||
|
||||
A javascript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This class have to inherit from the app/plugins/sdk.QueryCtrl class.
|
||||
A JavaScript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This class has to inherit from the app/plugins/sdk.QueryCtrl class.
|
||||
|
||||
Requires a static template or templateUrl variable which will be rendered as the view for this controller.
|
||||
|
||||
## ConfigCtrl
|
||||
|
||||
A javascript class that will be instantiated and treated as an Angular controller when a user tries to edit or create a new datasource of this type.
|
||||
A JavaScript class that will be instantiated and treated as an Angular controller when a user tries to edit or create a new datasource of this type.
|
||||
|
||||
Requires a static template or templateUrl variable which will be rendered as the view for this controller.
|
||||
|
||||
## QueryOptionsCtrl
|
||||
|
||||
A javascript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This controller is responsible for handling panel wide settings for the datasource. Such as interval, rate and aggregations if needed.
|
||||
A JavaScript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This controller is responsible for handling panel wide settings for the datasource, such as interval, rate and aggregations if needed.
|
||||
|
||||
Requires a static template or templateUrl variable which will be rendered as the view for this controller.
|
||||
|
||||
## AnnotationsQueryCtrl
|
||||
|
||||
A javascript class that will be instantiated and treated as an Angular controller when the user choose this type of datasource in the templating menu in the dashboard.
|
||||
A JavaScript class that will be instantiated and treated as an Angular controller when the user choose this type of datasource in the templating menu in the dashboard.
|
||||
|
||||
Requires a static template or templateUrl variable which will be rendered as the view for this controller. The fields that are bound to this controller is then sent to the Database objects annotationQuery function.
|
||||
Requires a static template or templateUrl variable which will be rendered as the view for this controller. The fields that are bound to this controller are then sent to the Database objects annotationQuery function.
|
128
docs/sources/plugins/developing/defaults-and-editor-mode.md
Normal file
128
docs/sources/plugins/developing/defaults-and-editor-mode.md
Normal file
@ -0,0 +1,128 @@
|
||||
+++
|
||||
title = "Plugin Defaults and Editor Mode"
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Plugin Defaults and Editor Mode"
|
||||
parent = "developing"
|
||||
weight = 3
|
||||
+++
|
||||
|
||||
# Plugin Defaults and Editor Mode
|
||||
|
||||
Most plugins allow users to customize the behavior by changing settings on an editor tab. These setting fields are saved in the dashboard json.
|
||||
|
||||
## Defaults
|
||||
|
||||
We define fields to be saved in Grafana by creating values on the panel object of the controller. You can see these values for any panel by choosing View JSON from the settings menu in Grafana. Here is an excerpt from the clock panel json (with some fields removed), the panel data is saved in the panels array:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Clock",
|
||||
...
|
||||
"rows": [
|
||||
{
|
||||
...
|
||||
"panels": [
|
||||
{
|
||||
"bgColor": "rgb(132, 151, 130)",
|
||||
"clockType": "24 hour",
|
||||
```
|
||||
|
||||
You can define panel data by first creating a variable with default values for the fields and then setting them on the panel object:
|
||||
|
||||
```javascript
|
||||
const panelDefaults = {
|
||||
clockType: '24 hour',
|
||||
fontSize: '60px',
|
||||
fontWeight: 'normal',
|
||||
bgColor: null
|
||||
};
|
||||
|
||||
constructor($scope, $injector) {
|
||||
super($scope, $injector);
|
||||
_.defaults(this.panel, panelDefaults);
|
||||
|
||||
this.updateClock();
|
||||
}
|
||||
```
|
||||
|
||||
The Lodash function [defaults](https://lodash.com/docs/4.17.4#defaults), which is called in the code above: `_.defaults`, sets a default value only if the value is not already set. This way values that have been changed by the user will not be overwritten.
|
||||
|
||||
These panel fields can be used in the controller or module.html template:
|
||||
|
||||
```html
|
||||
<h2 style="font-size: {{ctrl.panel.fontSize}};">{{ctrl.time}}</h2>
|
||||
```
|
||||
|
||||
If you want your users to be able to change these panel values then you need to expose them in the Grafana editor.
|
||||
|
||||
## Editor Mode
|
||||
|
||||
Editor mode is when a user clicks Edit on a panel. Every panel has a general tab where you change the title and width and some panels have more inbuilt tabs like the Metrics tab or Time Range tab. A panel plugin can add its own tab(s) so that a user can customize the panel.
|
||||
|
||||
Grafana conventions mean all you need to do is to hook up an Angular template with input fields and Grafana will automatically save the values to the dashboard json and load them on dashboard load.
|
||||
|
||||
## Using Events
|
||||
|
||||
To add an editor tab you need to hook into the event model so that the tab is added when the *init-edit-mode* event is triggered. The following code should be added to the constructor of the plugin Ctrl class:
|
||||
|
||||
```javascript
|
||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
||||
```
|
||||
|
||||
Then you need to create a handler function that is bound to the event. In the example above, the handler is called onInitEditMode. The tab is added by calling the controller function, *addEditorTab*. This function has three parameters; the tab name, the path to a html template for the new editor tab and the tab number. It can be a bit tricky to figure out the path, the path name will be based on the id that is specified in the plugin.json file - for example **grafana-clock-panel**. The code below hooks up an Angular template called editor.html that is located in the `src/partials` directory.
|
||||
|
||||
```javascript
|
||||
onInitEditMode() {
|
||||
this.addEditorTab('Options', 'public/plugins/grafana-clock-panel/editor.html', 2);
|
||||
}
|
||||
```
|
||||
|
||||
## Editor HTML and CSS
|
||||
|
||||
For editor tabs html, it is best to use Grafana css styles rather than custom styles. This is to preserve the look and feel of other tabs in Grafana.
|
||||
|
||||
Most editor tabs should use the [gf-form css class](https://github.com/grafana/grafana/blob/master/public/sass/components/_gf-form.scss) from Grafana. The example below has one row with a couple of columns and each column is wrapped in a div like this:
|
||||
|
||||
```html
|
||||
<div class="section gf-form-group">
|
||||
```
|
||||
|
||||
Then each pair, label and field is wrapped in a div with a gf-form class.
|
||||
|
||||
```html
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Font Size</label>
|
||||
<input type="text" class="gf-form-input width-4" ng-model="ctrl.panel.fontSize" ng-change="ctrl.render()" ng-model-onblur>
|
||||
</div>
|
||||
```
|
||||
|
||||
Note that there are some Angular attributes here. *ng-model* will update the panel data. *ng-change* will render the panel when you change the value. This change will occur on the onblur event due to the *ng-model-onblur* attribute. This means you can see the effect of your changes on the panel while editing.
|
||||
|
||||
{{< imgbox img="/assets/img/blog/clock-panel-editor.png" caption="Panel Editor" >}}
|
||||
|
||||
On the editor tab we use a drop down for 12/24 hour clock, an input field for font size and a color picker for the background color.
|
||||
|
||||
The drop down/select has its own *gf-form-select-wrapper* css class and looks like this:
|
||||
|
||||
```html
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">12 or 24 hour</label>
|
||||
<div class="gf-form-select-wrapper max-width-9">
|
||||
<select class="input-small gf-form-input" ng-model="ctrl.panel.clockType" ng-options="t for t in ['12 hour', '24 hour', 'custom']" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
The color picker (or spectrum picker) is a component that already exists in Grafana. We use it like this for the background color:
|
||||
|
||||
```html
|
||||
<spectrum-picker class="gf-form-input" ng-model="ctrl.panel.bgColor" ng-change="ctrl.render()" ></spectrum-picker>
|
||||
```
|
||||
|
||||
## Editor Tab Finished
|
||||
|
||||
To reiterate, this all ties together quite neatly. We specify properties and panel defaults in the constructor for the panel controller and these can then be changed in the editor. Grafana takes care of saving the changes.
|
||||
|
||||
One thing to be aware of is that panel defaults are used the first time a panel is created to set the initial values of the panel properties. After the panel is saved then the saved value will be used instead. So beware if you update panel defaults they will not automatically update the property in an existing panel. For example, if you set the default font size to 60px first and then in version 2 of the plugin change it to 50px, existing panels will still have 60px and only new panels will get the new 50px value.
|
120
docs/sources/plugins/developing/development.md
Normal file
120
docs/sources/plugins/developing/development.md
Normal file
@ -0,0 +1,120 @@
|
||||
+++
|
||||
title = "Developer Guide"
|
||||
type = "docs"
|
||||
aliases = ["/plugins/development/", "/plugins/datasources/", "/plugins/apps/", "/plugins/panels/"]
|
||||
[menu.docs]
|
||||
name = "Developer Guide"
|
||||
parent = "developing"
|
||||
weight = 1
|
||||
+++
|
||||
|
||||
# Developer Guide
|
||||
|
||||
From grafana 3.0 it's very easy to develop your own plugins and share them with other grafana users.
|
||||
|
||||
There are two blog posts about authoring a plugin that might also be of interest to any plugin authors, [Timing is Everything. Writing the Clock Panel Plugin for Grafana 3.0- part 1](http://grafana.org/blog/2016/04/08/timing-is-everything.-writing-the-clock-panel-plugin-for-grafana-3.0/) and [Timing is Everything. Editor Mode in Grafana 3.0 for the Clock Panel Plugin](http://grafana.org/blog/2016/04/15/timing-is-everything.-editor-mode-in-grafana-3.0-for-the-clock-panel-plugin/).
|
||||
|
||||
## Short version
|
||||
|
||||
1. [Setup grafana](http://docs.grafana.org/project/building_from_source/)
|
||||
2. Clone an example plugin into ```/var/lib/grafana/plugins``` or `data/plugins` (relative to grafana git repo if your running development version from source dir)
|
||||
3. Code away!
|
||||
|
||||
## What languages?
|
||||
|
||||
Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages.
|
||||
|
||||
## Buildscript
|
||||
|
||||
You can use any build system you like that support systemjs. All the built content should end up in a folder named ```dist``` and committed to the repository.By committing the dist folder the person who installs your plugin does not have to run any buildscript.
|
||||
|
||||
All our example plugins have build scripted configured.
|
||||
|
||||
## Metadata
|
||||
|
||||
See the [coding styleguide]({{< relref "code-styleguide.md" >}}) for details on the metadata.
|
||||
|
||||
## module.(js|ts)
|
||||
|
||||
This is the entry point for every plugin. This is the place where you should export
|
||||
your plugin implementation. Depending on what kind of plugin you are developing you
|
||||
will be expected to export different things. You can find what's expected for [datasource]({{< relref "datasources.md" >}}), [panels]({{< relref "panels.md" >}})
|
||||
and [apps]({{< relref "apps.md" >}}) plugins in the documentation.
|
||||
|
||||
The Grafana SDK is quite small so far and can be found here:
|
||||
|
||||
- [SDK file in Grafana](https://github.com/grafana/grafana/blob/master/public/app/plugins/sdk.ts)
|
||||
- [SDK Readme](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md)
|
||||
|
||||
The SDK contains three different plugin classes: PanelCtrl, MetricsPanelCtrl and QueryCtrl. For plugins of the panel type, the module.js file should export one of these. There are some extra classes for [data sources]({{< relref "datasources.md" >}}).
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
import {ClockCtrl} from './clock_ctrl';
|
||||
|
||||
export {
|
||||
ClockCtrl as PanelCtrl
|
||||
};
|
||||
```
|
||||
|
||||
The module class is also where css for the dark and light themes is imported:
|
||||
|
||||
```javascript
|
||||
import {loadPluginCss} from 'app/plugins/sdk';
|
||||
import WorldmapCtrl from './worldmap_ctrl';
|
||||
|
||||
loadPluginCss({
|
||||
dark: 'plugins/grafana-worldmap-panel/css/worldmap.dark.css',
|
||||
light: 'plugins/grafana-worldmap-panel/css/worldmap.light.css'
|
||||
});
|
||||
|
||||
export {
|
||||
WorldmapCtrl as PanelCtrl
|
||||
};
|
||||
```
|
||||
|
||||
## Start developing your plugin
|
||||
|
||||
There are three ways that you can start developing a Grafana plugin.
|
||||
|
||||
1. Setup a Grafana development environment. [(described here)](http://docs.grafana.org/project/building_from_source/) and place your plugin in the ```data/plugins``` folder.
|
||||
2. Install Grafana and place your plugin in the plugins directory which is set in your [config file](/installation/configuration). By default this is `/var/lib/grafana/plugins` on Linux systems.
|
||||
3. Place your plugin directory anywhere you like and specify it grafana.ini.
|
||||
|
||||
We encourage people to setup the full Grafana environment so that you can get inspiration from the rest of grafana code base.
|
||||
|
||||
When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless
|
||||
the folder contains a subfolder named dist. In that case grafana will mount the dist folder instead.
|
||||
This makes it possible to have both built and src content in the same plugin git repo.
|
||||
|
||||
## Grafana Events
|
||||
|
||||
There are a number of Grafana events that a plugin can hook into:
|
||||
|
||||
- `init-edit-mode` can be used to add tabs when editing a panel
|
||||
- `panel-teardown` can be used for clean up
|
||||
- `data-received` is an event in that is triggered on data refresh and can be hooked into
|
||||
- `data-snapshot-load` is an event triggered to load data when in snapshot mode.
|
||||
- `data-error` is used to handle errors on dashboard refresh.
|
||||
|
||||
If a panel receives data and hooks into the `data-received` event then it should handle snapshot mode too. Otherwise the panel will not work if saved as a snapshot. [Getting Plugins to work in Snapshot Mode]({{< relref "snapshot-mode.md" >}}) describes how to add support for this.
|
||||
|
||||
## Examples
|
||||
|
||||
We currently have three different examples that you can fork/download to get started developing your grafana plugin.
|
||||
|
||||
- [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for querying json data from backends)
|
||||
- [example-app](https://github.com/grafana/example-app)
|
||||
- [clock-panel](https://github.com/grafana/clock-panel)
|
||||
- [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts)
|
||||
- [piechart-panel](https://github.com/grafana/piechart-panel)
|
||||
|
||||
## Other Articles
|
||||
|
||||
- [Getting Plugins to work in Snapshot Mode]({{< relref "snapshot-mode.md" >}})
|
||||
- [Plugin Defaults and Editor Mode]({{< relref "defaults-and-editor-mode.md" >}})
|
||||
- [Grafana Plugin Code Styleguide]({{< relref "code-styleguide.md" >}})
|
||||
- [Grafana Apps]({{< relref "apps.md" >}})
|
||||
- [Grafana Datasources]({{< relref "datasources.md" >}})
|
||||
- [plugin.json Schema]({{< relref "plugin.json.md" >}})
|
8
docs/sources/plugins/developing/index.md
Normal file
8
docs/sources/plugins/developing/index.md
Normal file
@ -0,0 +1,8 @@
|
||||
+++
|
||||
title = "Developing Plugins"
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
parent = "plugins"
|
||||
identifier = "developing"
|
||||
weight = 3
|
||||
+++
|
@ -9,7 +9,7 @@ page_keywords: grafana, plugins, documentation
|
||||
title = "Installing Plugins"
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
parent = "plugins"
|
||||
parent = "developing"
|
||||
weight = 1
|
||||
+++
|
||||
|
28
docs/sources/plugins/developing/plugin.json.md
Normal file
28
docs/sources/plugins/developing/plugin.json.md
Normal file
@ -0,0 +1,28 @@
|
||||
+++
|
||||
title = "plugin.json Schema"
|
||||
keywords = ["grafana", "plugins", "documentation"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "plugin.json Schema"
|
||||
parent = "developing"
|
||||
weight = 6
|
||||
+++
|
||||
|
||||
# Plugin.json
|
||||
|
||||
The plugin.json file is mandatory for all plugins. When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless the folder contains a subfolder named `dist`. In that case grafana will mount the `dist` folder instead.
|
||||
|
||||
## Plugin JSON Schema
|
||||
|
||||
| Property | Description |
|
||||
| ------------- |-------------|
|
||||
| id | unique name of the plugin - [conventions described in styleguide]({{< relref "code-styleguide.md" >}}) |
|
||||
| type | panel/datasource/app |
|
||||
| name | Human readable name of the plugin |
|
||||
| info.description | Description of plugin. Used for searching grafana net plugins |
|
||||
| info.author | |
|
||||
| info.keywords | plugin keywords. Used for search on grafana net|
|
||||
| info.logos | link to project logos |
|
||||
| info.version | project version of this commit. Must be semver |
|
||||
| dependencies.grafanaVersion | Required grafana backend version for this plugin |
|
||||
| dependencies.plugins | required plugins for this plugin. |
|
79
docs/sources/plugins/developing/snapshot-mode.md
Normal file
79
docs/sources/plugins/developing/snapshot-mode.md
Normal file
@ -0,0 +1,79 @@
|
||||
+++
|
||||
title = "Snapshot Mode"
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Snapshot Mode"
|
||||
parent = "developing"
|
||||
weight = 6
|
||||
+++
|
||||
|
||||
# Getting Plugins to work in Snapshot Mode
|
||||
|
||||
{{< imgbox img="/img/docs/Grafana-snapshot-example.png" caption="A dashboard using snapshot data and not live data." >}}
|
||||
|
||||
Grafana has this great feature where you can [save a snapshot of your dashboard](http://docs.grafana.org/reference/sharing/). Instead of sending a screenshot of a dashboard to someone, you can send them a working, interactive Grafana dashboard with the snapshot data embedded inside it. The snapshot can be saved on your Grafana server and is available to all your co-workers. Raintank also hosts a [snapshot server](http://snapshot.raintank.io/) if you want to send the snapshot to someone who does not have access to your Grafana server.
|
||||
|
||||
{{< imgbox img="/img/docs/animated_gifs/snapshots.gif" caption="Selecting a snapshot" >}}
|
||||
|
||||
This all works because Grafana saves a snapshot of the current data in the dashboard json instead of fetching the data from a data source. However, if you are building a custom panel plugin then this will not work straight out of the box. You will need to make some small (and easy!) changes first.
|
||||
|
||||
## Enabling support for loading snapshot data
|
||||
|
||||
Grafana automatically saves data from data sources in the dashboard json when the snapshot is created so we do not have to write any code for that. Enabling snapshot support for reading time series data is very simple. First in the constructor, we need to add an event handler for `data-snapshot-load`. This event is triggered by Grafana when the snapshot data is loaded from the dashboard json.
|
||||
|
||||
```javascript
|
||||
constructor($scope, $injector, contextSrv) {
|
||||
super($scope, $injector);
|
||||
...
|
||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
||||
this.events.on('data-received', this.onDataReceived.bind(this));
|
||||
this.events.on('panel-teardown', this.onPanelTeardown.bind(this));
|
||||
this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this));
|
||||
```
|
||||
|
||||
Then we need to create a simple event handler that just forwards the data on to our regular `data-received` handler:
|
||||
|
||||
```javascript
|
||||
onDataSnapshotLoad(snapshotData) {
|
||||
this.onDataReceived(snapshotData);
|
||||
}
|
||||
```
|
||||
|
||||
This will cover most use cases for snapshot support. Sometimes you will want to save data that is not time series data from a Grafana data source and then you have to do a bit more work to get snapshot support.
|
||||
|
||||
## Saving custom data for snapshots
|
||||
|
||||
Data that is not time series data from a Grafana data source is not saved automatically by Grafana. Saving custom data for snapshot mode has to be done manually.
|
||||
|
||||
{{< imgbox img="/img/docs/Grafana-save-snapshot.png" caption="Save snapshot" >}}
|
||||
|
||||
Grafana gives us a chance to save data to the dashboard json when it is creating a snapshot. In the 'data-received' event handler, you can check the snapshot flag on the dashboard object. If this is true, then Grafana is creating a snapshot and you can manually save custom data to the panel json. In the example, a new field called snapshotLocationData in the panel json is initialized with a snapshot of the custom data.
|
||||
|
||||
```javascript
|
||||
onDataReceived(dataList) {
|
||||
if (!dataList) return;
|
||||
|
||||
if (this.dashboard.snapshot && this.locations) {
|
||||
this.panel.snapshotLocationData = this.locations;
|
||||
}
|
||||
```
|
||||
|
||||
Now the location data is saved in the dashboard json but we will have to load it manually as well.
|
||||
|
||||
## Loading custom data for snapshots
|
||||
|
||||
The example below shows a function that loads the custom data. The data source for the custom data (an external api in this case) is not available in snapshot mode so a guard check is made to see if there is any snapshot data available first. If there is, then the snapshot data is used instead of trying to load the data from the external api.
|
||||
|
||||
```javascript
|
||||
loadLocationDataFromFile(reload) {
|
||||
if (this.map && !reload) return;
|
||||
|
||||
if (this.panel.snapshotLocationData) {
|
||||
this.locations = this.panel.snapshotLocationData;
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
It is really easy to forget to add this support but it enables a great feature and can be used to demo your panel.
|
||||
|
||||
If there is a panel plugin that you would like to be installed on the Raintank Snapshot server then please contact us via [Slack](https://raintank.slack.com) or [GitHub](https://github.com/grafana/grafana).
|
@ -1,60 +0,0 @@
|
||||
+++
|
||||
title = "Developer Guide"
|
||||
type = "docs"
|
||||
aliases = ["/plugins/datasources/", "/plugins/apps/", "/plugins/panels/"]
|
||||
[menu.docs]
|
||||
name = "Developer Guide"
|
||||
parent = "plugins"
|
||||
weight = 5
|
||||
+++
|
||||
|
||||
# Developer Guide
|
||||
|
||||
From grafana 3.0 it's very easy to develop your own plugins and share them with other grafana users.
|
||||
|
||||
## Short version
|
||||
|
||||
1. [Setup grafana](http://docs.grafana.org/project/building_from_source/)
|
||||
2. Clone an example plugin into ```/var/lib/grafana/plugins``` or `data/plugins` (relative to grafana git repo if your running development version from source dir)
|
||||
3. Code away!
|
||||
|
||||
## What languages?
|
||||
|
||||
Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages.
|
||||
|
||||
## Buildscript
|
||||
|
||||
You can use any build system you like that support systemjs. All the built content should end up in a folder named ```dist``` and committed to the repository.By committing the dist folder the person who installs your plugin does not have to run any buildscript.
|
||||
|
||||
All our example plugins have build scripted configured.
|
||||
|
||||
## module.(js|ts)
|
||||
|
||||
This is the entry point for every plugin. This is the place where you should export
|
||||
your plugin implementation. Depending on what kind of plugin you are developing you
|
||||
will be expected to export different things. You can find what's expected for [datasource](./datasources.md), [panels](./panels.md)
|
||||
and [apps](./apps.md) plugins in the documentation.
|
||||
|
||||
## Start developing your plugin
|
||||
|
||||
There are three ways that you can start developing a Grafana plugin.
|
||||
|
||||
1. Setup a Grafana development environment. [(described here)](http://docs.grafana.org/project/building_from_source/) and place your plugin in the ```data/plugins``` folder.
|
||||
2. Install Grafana and place your plugin in the plugins directory which is set in your [config file]({{< relref "installation/configuration.md" >}}). By default this is `/var/lib/grafana/plugins` on Linux systems.
|
||||
3. Place your plugin directory anywhere you like and specify it grafana.ini.
|
||||
|
||||
We encourage people to setup the full Grafana environment so that you can get inspiration from the rest of grafana code base.
|
||||
|
||||
When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless
|
||||
the folder contains a subfolder named dist. In that case grafana will mount the dist folder instead.
|
||||
This makes it possible to have both built and src content in the same plugin git repo.
|
||||
|
||||
## Examples
|
||||
|
||||
We currently have three different examples that you can fork/download to get started developing your grafana plugin.
|
||||
|
||||
- [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for querying json data from backends)
|
||||
- [example-app](https://github.com/grafana/example-app)
|
||||
- [clock-panel](https://github.com/grafana/clock-panel)
|
||||
- [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts)
|
||||
- [piechart-panel](https://github.com/grafana/piechart-panel)
|
@ -1,10 +0,0 @@
|
||||
---
|
||||
page_title: Plugin json file
|
||||
page_description: Plugin json for Grafana
|
||||
page_keywords: grafana, plugins, documentation
|
||||
---
|
||||
|
||||
# Plugin.json
|
||||
|
||||
TODO
|
||||
|
@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [Go 1.7.3](https://golang.org/dl/)
|
||||
- [Go 1.8](https://golang.org/dl/)
|
||||
- [NodeJS](https://nodejs.org/download/)
|
||||
|
||||
## Get Code
|
||||
|
@ -66,11 +66,11 @@ Each field in the dashboard JSON is explained below with its usage:
|
||||
| **editable** | whether a dashboard is editable or not |
|
||||
| **hideControls** | whether row controls on the left in green are hidden or not |
|
||||
| **graphTooltip** | TODO |
|
||||
| **rows** | row metadata, see [rows section](/docs/sources/reference/dashboard.md/#rows) for details |
|
||||
| **rows** | row metadata, see [rows section](#rows) for details |
|
||||
| **time** | time range for dashboard, i.e. last 6 hours, last 7 days, etc |
|
||||
| **timepicker** | timepicker metadata, see [timepicker section](/docs/sources/reference/dashboard.md/#timepicker) for details |
|
||||
| **templating** | templating metadata, see [templating section](/docs/sources/reference/dashboard.md/#templating) for details |
|
||||
| **annotations** | annotations metadata, see [annotations section](/docs/sources/reference/dashboard.md/#annotations) for details |
|
||||
| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
|
||||
| **templating** | templating metadata, see [templating section](#templating) for details |
|
||||
| **annotations** | annotations metadata, see [annotations section](#annotations) for details |
|
||||
| **schemaVersion** | TODO |
|
||||
| **version** | TODO |
|
||||
| **links** | TODO |
|
||||
@ -79,7 +79,7 @@ Each field in the dashboard JSON is explained below with its usage:
|
||||
|
||||
`rows` field consists of an array of JSON object representing each row in a dashboard, such as shown below:
|
||||
|
||||
```
|
||||
```json
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
@ -105,14 +105,14 @@ Usage of the fields is explained below:
|
||||
| **collapse** | whether row is collapsed or not |
|
||||
| **editable** | whether a row is editable or not |
|
||||
| **height** | height of the row in pixels |
|
||||
| **panels** | panels metadata, see [panels section](/docs/sources/reference/dashboard.md/#panels) for details |
|
||||
| **panels** | panels metadata, see [panels section](#panels) for details |
|
||||
| **title** | title of row |
|
||||
|
||||
#### panels
|
||||
|
||||
Panels are the building blocks a dashboard. It consists of datasource queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel in a row. Most of the fields are common for all panels but some fields depends on the panel type. Following is an example of panel JSON representing a `graph` panel type:
|
||||
|
||||
```
|
||||
```json
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
@ -276,7 +276,7 @@ Usage of each field is explained below:
|
||||
|
||||
Description: TODO
|
||||
|
||||
```
|
||||
```json
|
||||
"timepicker": {
|
||||
"collapse": false,
|
||||
"enable": true,
|
||||
@ -330,7 +330,7 @@ Usage of the fields is explained below:
|
||||
|
||||
`templating` fields contains array of template variables with their saved values along with some other metadata, for example:
|
||||
|
||||
```
|
||||
```json
|
||||
"templating": {
|
||||
"enable": true,
|
||||
"list": [
|
||||
|
@ -16,7 +16,7 @@ Since Grafana automatically scales Dashboards to any resolution they're perfect
|
||||
|
||||
## Creating a Playlist
|
||||
|
||||
{{< docs-imagebox img="img/docs/v3/playlist.png" max-width="25rem" >}}
|
||||
{{< docs-imagebox img="/img/docs/v3/playlist.png" max-width="25rem" >}}
|
||||
|
||||
The Playlist feature can be accessed from Grafana's sidemenu, in the Dashboard submenu.
|
||||
|
||||
|
40
docs/sources/tutorials/ha_setup.md
Normal file
40
docs/sources/tutorials/ha_setup.md
Normal file
@ -0,0 +1,40 @@
|
||||
+++
|
||||
title = "Setup Grafana for High availability"
|
||||
type = "docs"
|
||||
keywords = ["grafana", "tutorials", "HA", "high availability"]
|
||||
[menu.docs]
|
||||
parent = "tutorials"
|
||||
weight = 10
|
||||
+++
|
||||
|
||||
# How to setup Grafana for high availability
|
||||
|
||||
> Alerting does not support high availability yet.
|
||||
|
||||
Setting up Grafana for high availability is fairly simple. It comes down to two things:
|
||||
|
||||
* Use a shared database for multiple grafana instances.
|
||||
* Consider how user sessions are stored.
|
||||
|
||||
## Configure multiple servers to use the same database
|
||||
|
||||
First you need to do is to setup mysql or postgres on another server and configure Grafana to use that database.
|
||||
You can find the configuration for doing that in the [[database]]({{< relref "configuration.md" >}}#database) section in the grafana config.
|
||||
Grafana will now persist all long term data in the database.
|
||||
It also worth considering how to setup the database for high availability but thats outside the scope of this guide.
|
||||
|
||||
## User sessions
|
||||
|
||||
The second thing to consider is how to deal with user sessions and how to balance the load between servers.
|
||||
By default Grafana stores user sessions on disk which works fine if you use `sticky sessions` in your load balancer.
|
||||
Grafana also supports storing the session data in the database, redis or memcache which makes it possible to use round robin in your load balancer.
|
||||
If you use mysql/postgres for session storage you first need a table to store the session data in. More details about that in [[sessions]]({{< relref "configuration.md" >}}#session)
|
||||
|
||||
For Grafana itself it doesn't really matter if you store your sessions on disk or database/redis/memcache.
|
||||
But we suggest that you store the session in redis/memcache since it makes it easier to add/remote instances from the group.
|
||||
|
||||
## Alerting
|
||||
|
||||
Currently alerting does not support high availability. But this is something that we will be working on in the future.
|
||||
|
||||
|
@ -22,15 +22,17 @@ take you to the graph.
|
||||
> is so Hipchat and Slack can show them reliably (they require the image to be publicly available).
|
||||
|
||||
<div class="text-center">
|
||||
<img src="/img/tutorials/hubot_grafana.png" class="center"></a>
|
||||
<img src="/img/docs/tutorials/hubot_grafana.png" class="center"></a>
|
||||
</div>
|
||||
|
||||
## What is Hubot?
|
||||
|
||||
[Hubot](https://hubot.github.com/) is an universal and extensible chat bot that can be used with many chat
|
||||
services and has a huge library of third party plugins that allow you to automate anything from your
|
||||
chat rooms.
|
||||
|
||||
## Install Hubot
|
||||
|
||||
Hubot is very easy to install and host. If you do not already have a bot up and running please
|
||||
read the official [Getting Started With Hubot](https://hubot.github.com/docs/) guide.
|
||||
|
||||
@ -63,6 +65,7 @@ The `hubot-grafana` plugin requires a number of environment variables to be set
|
||||
export HUBOT_GRAFANA_S3_REGION=us-standard
|
||||
|
||||
### Grafana server side rendering
|
||||
|
||||
The hubot plugin will take advantage of the Grafana server side rendering feature that can
|
||||
render any panel on the server using phantomjs. Grafana ships with a phantomjs binary (linux only).
|
||||
|
||||
@ -70,11 +73,13 @@ To verify that this feature works try the `Direct link to rendered image` link i
|
||||
If you do not get an image when opening this link verify that the required font packages are installed for phantomjs to work.
|
||||
|
||||
### Grafana API Key
|
||||
<img src="/img/v2/orgdropdown_api_keys.png" style="width: 150px" class="right"></img>
|
||||
|
||||
<img src="/img/docs/v2/orgdropdown_api_keys.png" style="width: 150px" class="right"></img>
|
||||
You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key.
|
||||
You can add these from the API Keys page which you find in the Organization dropdown.
|
||||
|
||||
### Amazon S3
|
||||
|
||||
The `S3` options are optional but for the images to work properly in services like Slack and Hipchat they need
|
||||
to publicly available. By specifying the `S3` options the hubot-grafana script will publish the rendered
|
||||
panel to `S3` and it will use that URL when it posts to Slack or Hipchat.
|
||||
@ -99,6 +104,7 @@ panel to `S3` and it will use that URL when it posts to Slack or Hipchat.
|
||||
- Get a templated dashboard with the `$host` parameter set to `carbon-a`
|
||||
|
||||
## Aliases
|
||||
|
||||
Some of the hubot commands above can lengthy and you might have to remember the dashboard slug (url id).
|
||||
If you have a few favorite graphs you want to be able check up on often (let's say from your mobile) you
|
||||
can create hubot command aliases with the hubot script `hubot-alias`.
|
||||
@ -115,7 +121,7 @@ Now you can add an alias like this:
|
||||
|
||||
<div class="text-center">
|
||||
Using the alias:<br>
|
||||
<img src="/img/tutorials/hubot_grafana2.png" class="center"></a>
|
||||
<img src="/img/docs/tutorials/hubot_grafana2.png" class="center"></a>
|
||||
</div>
|
||||
|
||||
## Summary
|
||||
|
@ -4,7 +4,7 @@
|
||||
"company": "Coding Instinct AB"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "4.2.0-pre1",
|
||||
"version": "4.3.0-pre1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
|
@ -1,6 +1,6 @@
|
||||
#! /usr/bin/env bash
|
||||
deb_ver=4.1.0-1484127817
|
||||
rpm_ver=4.1.0-1484127817
|
||||
deb_ver=4.1.2-1486989747
|
||||
rpm_ver=4.1.2-1486989747
|
||||
|
||||
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
|
||||
|
||||
|
@ -123,6 +123,7 @@ func (hs *HttpServer) registerRoutes() {
|
||||
// users (admin permission required)
|
||||
r.Group("/users", func() {
|
||||
r.Get("/", wrap(SearchUsers))
|
||||
r.Get("/search", wrap(SearchUsersWithPaging))
|
||||
r.Get("/:id", wrap(GetUserById))
|
||||
r.Get("/:id/orgs", wrap(GetUserOrgList))
|
||||
// query parameters /users/lookup?loginOrEmail=admin@example.com
|
||||
@ -195,10 +196,11 @@ func (hs *HttpServer) registerRoutes() {
|
||||
|
||||
// Data sources
|
||||
r.Group("/datasources", func() {
|
||||
r.Get("/", GetDataSources)
|
||||
r.Get("/", wrap(GetDataSources))
|
||||
r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
|
||||
r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
|
||||
r.Delete("/:id", DeleteDataSource)
|
||||
r.Delete("/:id", DeleteDataSourceById)
|
||||
r.Delete("/name/:name", DeleteDataSourceByName)
|
||||
r.Get("/:id", wrap(GetDataSourceById))
|
||||
r.Get("/name/:name", wrap(GetDataSourceByName))
|
||||
}, reqOrgAdmin)
|
||||
|
@ -1,35 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHttpApi(t *testing.T) {
|
||||
|
||||
// Convey("Given the grafana api", t, func() {
|
||||
// ConveyApiScenario("Can sign up", func(c apiTestContext) {
|
||||
// c.PostJson()
|
||||
// So(c.Resp, ShouldEqualJsonApiResponse, "User created and logged in")
|
||||
// })
|
||||
//
|
||||
// m := macaron.New()
|
||||
// m.Use(middleware.GetContextHandler())
|
||||
// m.Use(middleware.Sessioner(&session.Options{}))
|
||||
// Register(m)
|
||||
//
|
||||
// var context *middleware.Context
|
||||
// m.Get("/", func(c *middleware.Context) {
|
||||
// context = c
|
||||
// })
|
||||
//
|
||||
// resp := httptest.NewRecorder()
|
||||
// req, err := http.NewRequest("GET", "/", nil)
|
||||
// So(err, ShouldBeNil)
|
||||
//
|
||||
// m.ServeHTTP(resp, req)
|
||||
//
|
||||
// Convey("should red 200", func() {
|
||||
// So(resp.Code, ShouldEqual, 200)
|
||||
// })
|
||||
// })
|
||||
}
|
@ -114,7 +114,10 @@ func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
|
||||
DurationSeconds: aws.Int64(900),
|
||||
}
|
||||
|
||||
stsSess := session.New()
|
||||
stsSess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stsCreds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.EnvProvider{},
|
||||
@ -126,7 +129,11 @@ func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
|
||||
Credentials: stsCreds,
|
||||
}
|
||||
|
||||
svc := sts.New(session.New(stsConfig), stsConfig)
|
||||
sess, err := session.NewSession(stsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svc := sts.New(sess, stsConfig)
|
||||
resp, err := svc.AssumeRole(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -139,7 +146,10 @@ func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
|
||||
}
|
||||
}
|
||||
|
||||
sess := session.New()
|
||||
sess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
@ -185,7 +195,12 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
@ -232,7 +247,12 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
@ -273,7 +293,12 @@ func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
@ -316,7 +341,12 @@ func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
@ -360,7 +390,12 @@ func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
@ -396,7 +431,12 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := ec2.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := ec2.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
|
@ -82,6 +82,7 @@ func init() {
|
||||
"AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "CommitLatency", "CommitThroughput", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "FailedSqlStatements", "FreeableMemory", "FreeStorageSpace", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
||||
"AWS/Route53": {"HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
|
||||
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects"},
|
||||
"AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send"},
|
||||
"AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
|
||||
"AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
|
||||
"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed",
|
||||
@ -123,6 +124,7 @@ func init() {
|
||||
"AWS/RDS": {"DBInstanceIdentifier", "DBClusterIdentifier", "DatabaseClass", "EngineName"},
|
||||
"AWS/Route53": {"HealthCheckId"},
|
||||
"AWS/S3": {"BucketName", "StorageType"},
|
||||
"AWS/SES": {},
|
||||
"AWS/SNS": {"Application", "Platform", "TopicName"},
|
||||
"AWS/SQS": {"QueueName"},
|
||||
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
|
||||
@ -256,8 +258,11 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
|
||||
Region: aws.String(cwData.Region),
|
||||
Credentials: creds,
|
||||
}
|
||||
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return cloudwatch.ListMetricsOutput{}, err
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
params := &cloudwatch.ListMetricsInput{
|
||||
Namespace: aws.String(cwData.Namespace),
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/cloudwatch"
|
||||
@ -107,6 +108,13 @@ func ProxyDataSourceRequest(c *middleware.Context) {
|
||||
|
||||
proxyPath := c.Params("*")
|
||||
|
||||
if ds.Type == m.DS_PROMETHEUS {
|
||||
if c.Req.Request.Method != http.MethodGet || !strings.HasPrefix(proxyPath, "api/") {
|
||||
c.JsonApiErr(403, "GET is only allowed on proxied Prometheus datasource", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ds.Type == m.DS_ES {
|
||||
if c.Req.Request.Method == "DELETE" {
|
||||
c.JsonApiErr(403, "Deletes not allowed on proxied Elasticsearch datasource", nil)
|
||||
|
@ -11,12 +11,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func GetDataSources(c *middleware.Context) {
|
||||
func GetDataSources(c *middleware.Context) Response {
|
||||
query := m.GetDataSourcesQuery{OrgId: c.OrgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
c.JsonApiErr(500, "Failed to query datasources", err)
|
||||
return
|
||||
return ApiError(500, "Failed to query datasources", err)
|
||||
}
|
||||
|
||||
result := make(dtos.DataSourceList, 0)
|
||||
@ -46,7 +45,8 @@ func GetDataSources(c *middleware.Context) {
|
||||
}
|
||||
|
||||
sort.Sort(result)
|
||||
c.JSON(200, result)
|
||||
|
||||
return Json(200, &result)
|
||||
}
|
||||
|
||||
func GetDataSourceById(c *middleware.Context) Response {
|
||||
@ -68,7 +68,7 @@ func GetDataSourceById(c *middleware.Context) Response {
|
||||
return Json(200, &dtos)
|
||||
}
|
||||
|
||||
func DeleteDataSource(c *middleware.Context) {
|
||||
func DeleteDataSourceById(c *middleware.Context) {
|
||||
id := c.ParamsInt64(":id")
|
||||
|
||||
if id <= 0 {
|
||||
@ -76,7 +76,26 @@ func DeleteDataSource(c *middleware.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := &m.DeleteDataSourceCommand{Id: id, OrgId: c.OrgId}
|
||||
cmd := &m.DeleteDataSourceByIdCommand{Id: id, OrgId: c.OrgId}
|
||||
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Failed to delete datasource", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JsonOK("Data source deleted")
|
||||
}
|
||||
|
||||
func DeleteDataSourceByName(c *middleware.Context) {
|
||||
name := c.Params(":name")
|
||||
|
||||
if name == "" {
|
||||
c.JsonApiErr(400, "Missing valid datasource name", nil)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := &m.DeleteDataSourceByNameCommand{Name: name, OrgId: c.OrgId}
|
||||
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
|
132
pkg/api/datasources_test.go
Normal file
132
pkg/api/datasources_test.go
Normal file
@ -0,0 +1,132 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
const (
|
||||
TestOrgID = 1
|
||||
TestUserID = 1
|
||||
)
|
||||
|
||||
func TestDataSourcesProxy(t *testing.T) {
|
||||
Convey("Given a user is logged in", t, func() {
|
||||
loggedInUserScenario("When calling GET on", "/api/datasources/", func(sc *scenarioContext) {
|
||||
|
||||
// Stubs the database query
|
||||
bus.AddHandler("test", func(query *models.GetDataSourcesQuery) error {
|
||||
So(query.OrgId, ShouldEqual, TestOrgID)
|
||||
query.Result = []*models.DataSource{
|
||||
{Name: "mmm"},
|
||||
{Name: "ZZZ"},
|
||||
{Name: "BBB"},
|
||||
{Name: "aaa"},
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// handler func being tested
|
||||
sc.handlerFunc = GetDataSources
|
||||
sc.fakeReq("GET", "/api/datasources").exec()
|
||||
|
||||
respJSON := []map[string]interface{}{}
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(&respJSON)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("should return list of datasources for org sorted alphabetically and case insensitively", func() {
|
||||
So(respJSON[0]["name"], ShouldEqual, "aaa")
|
||||
So(respJSON[1]["name"], ShouldEqual, "BBB")
|
||||
So(respJSON[2]["name"], ShouldEqual, "mmm")
|
||||
So(respJSON[3]["name"], ShouldEqual, "ZZZ")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := &scenarioContext{
|
||||
url: url,
|
||||
}
|
||||
viewsPath, _ := filepath.Abs("../../public/views")
|
||||
|
||||
sc.m = macaron.New()
|
||||
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||
Directory: viewsPath,
|
||||
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
||||
}))
|
||||
|
||||
sc.m.Use(middleware.GetContextHandler())
|
||||
sc.m.Use(middleware.Sessioner(&session.Options{}))
|
||||
|
||||
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||
sc.context = c
|
||||
sc.context.UserId = TestUserID
|
||||
sc.context.OrgId = TestOrgID
|
||||
sc.context.OrgRole = models.ROLE_EDITOR
|
||||
if sc.handlerFunc != nil {
|
||||
return sc.handlerFunc(sc.context)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.m.Get(url, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
|
||||
sc.resp = httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
So(err, ShouldBeNil)
|
||||
sc.req = req
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
|
||||
sc.resp = httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
q := req.URL.Query()
|
||||
for k, v := range queryParams {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
So(err, ShouldBeNil)
|
||||
sc.req = req
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
m *macaron.Macaron
|
||||
context *middleware.Context
|
||||
resp *httptest.ResponseRecorder
|
||||
handlerFunc handlerFunc
|
||||
defaultHandler macaron.Handler
|
||||
req *http.Request
|
||||
url string
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) exec() {
|
||||
sc.m.ServeHTTP(sc.resp, sc.req)
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
||||
type handlerFunc func(c *middleware.Context) Response
|
@ -91,7 +91,7 @@ func (slice DataSourceList) Len() int {
|
||||
}
|
||||
|
||||
func (slice DataSourceList) Less(i, j int) bool {
|
||||
return slice[i].Name < slice[j].Name
|
||||
return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name)
|
||||
}
|
||||
|
||||
func (slice DataSourceList) Swap(i, j int) {
|
||||
|
@ -141,6 +141,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
"authProxyEnabled": setting.AuthProxyEnabled,
|
||||
"ldapEnabled": setting.LdapEnabled,
|
||||
"alertingEnabled": setting.AlertingEnabled,
|
||||
"googleAnalyticsId": setting.GoogleAnalyticsId,
|
||||
"buildInfo": map[string]interface{}{
|
||||
"version": setting.BuildVersion,
|
||||
"commit": setting.BuildCommit,
|
||||
|
@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@ -24,6 +25,8 @@ type HttpServer struct {
|
||||
macaron *macaron.Macaron
|
||||
context context.Context
|
||||
streamManager *live.StreamManager
|
||||
|
||||
httpSrv *http.Server
|
||||
}
|
||||
|
||||
func NewHttpServer() *HttpServer {
|
||||
@ -45,11 +48,20 @@ func (hs *HttpServer) Start(ctx context.Context) error {
|
||||
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
|
||||
hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl)
|
||||
|
||||
hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron}
|
||||
switch setting.Protocol {
|
||||
case setting.HTTP:
|
||||
err = http.ListenAndServe(listenAddr, hs.macaron)
|
||||
err = hs.httpSrv.ListenAndServe()
|
||||
if err == http.ErrServerClosed {
|
||||
hs.log.Debug("server was shutdown gracefully")
|
||||
return nil
|
||||
}
|
||||
case setting.HTTPS:
|
||||
err = hs.listenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile)
|
||||
err = hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
|
||||
if err == http.ErrServerClosed {
|
||||
hs.log.Debug("server was shutdown gracefully")
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
hs.log.Error("Invalid protocol", "protocol", setting.Protocol)
|
||||
err = errors.New("Invalid Protocol")
|
||||
@ -58,6 +70,12 @@ func (hs *HttpServer) Start(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (hs *HttpServer) Shutdown(ctx context.Context) error {
|
||||
err := hs.httpSrv.Shutdown(ctx)
|
||||
hs.log.Info("stopped http server")
|
||||
return err
|
||||
}
|
||||
|
||||
func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) error {
|
||||
if certfile == "" {
|
||||
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
|
||||
@ -75,7 +93,32 @@ func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) er
|
||||
return fmt.Errorf(`Cannot find SSL key_file at %v`, setting.KeyFile)
|
||||
}
|
||||
|
||||
return http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, hs.macaron)
|
||||
tlsCfg := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
PreferServerCipherSuites: true,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
},
|
||||
}
|
||||
srv := &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: hs.macaron,
|
||||
TLSConfig: tlsCfg,
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
|
||||
}
|
||||
|
||||
return srv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
|
||||
}
|
||||
|
||||
func (hs *HttpServer) newMacaron() *macaron.Macaron {
|
||||
@ -107,6 +150,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
|
||||
m.Use(middleware.GetContextHandler())
|
||||
m.Use(middleware.Sessioner(&setting.SessionOptions))
|
||||
m.Use(middleware.RequestMetrics())
|
||||
m.Use(middleware.OrgRedirect())
|
||||
|
||||
// needs to be after context handler
|
||||
if setting.EnforceDomain {
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
@ -177,5 +178,11 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
|
||||
metrics.M_Api_Login_OAuth.Inc(1)
|
||||
|
||||
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubUrl + "/")
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := tsdb.HandleRequest(context.TODO(), request)
|
||||
resp, err := tsdb.HandleRequest(context.Background(), request)
|
||||
if err != nil {
|
||||
return ApiError(500, "Metric request error", err)
|
||||
}
|
||||
|
@ -210,14 +210,48 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand)
|
||||
|
||||
// GET /api/users
|
||||
func SearchUsers(c *middleware.Context) Response {
|
||||
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
query, err := searchUser(c)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to fetch users", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result.Users)
|
||||
}
|
||||
|
||||
// GET /api/search
|
||||
func SearchUsersWithPaging(c *middleware.Context) Response {
|
||||
query, err := searchUser(c)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to fetch users", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
|
||||
perPage := c.QueryInt("perpage")
|
||||
if perPage <= 0 {
|
||||
perPage = 1000
|
||||
}
|
||||
page := c.QueryInt("page")
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
searchQuery := c.Query("query")
|
||||
|
||||
query := &m.SearchUsersQuery{Query: searchQuery, Page: page, Limit: perPage}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query.Result.Page = page
|
||||
query.Result.PerPage = perPage
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func SetHelpFlag(c *middleware.Context) Response {
|
||||
flag := c.ParamsInt64(":id")
|
||||
|
||||
|
109
pkg/api/user_test.go
Normal file
109
pkg/api/user_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestUserApiEndpoint(t *testing.T) {
|
||||
Convey("Given a user is logged in", t, func() {
|
||||
mockResult := models.SearchUserQueryResult{
|
||||
Users: []*models.UserSearchHitDTO{
|
||||
{Name: "user1"},
|
||||
{Name: "user2"},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}
|
||||
|
||||
loggedInUserScenario("When calling GET on", "/api/users", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsers
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 1000)
|
||||
So(sendPage, ShouldEqual, 1)
|
||||
|
||||
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||
So(err, ShouldBeNil)
|
||||
So(len(respJSON.MustArray()), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
loggedInUserScenario("When calling GET with page and limit querystring parameters on", "/api/users", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsers
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 10)
|
||||
So(sendPage, ShouldEqual, 2)
|
||||
})
|
||||
|
||||
loggedInUserScenario("When calling GET on", "/api/users/search", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsersWithPaging
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 1000)
|
||||
So(sendPage, ShouldEqual, 1)
|
||||
|
||||
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
|
||||
So(len(respJSON.Get("users").MustArray()), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
loggedInUserScenario("When calling GET with page and perpage querystring parameters on", "/api/users/search", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsersWithPaging
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 10)
|
||||
So(sendPage, ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
}
|
@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
@ -39,6 +38,8 @@ type GrafanaServerImpl struct {
|
||||
shutdownFn context.CancelFunc
|
||||
childRoutines *errgroup.Group
|
||||
log log.Logger
|
||||
|
||||
httpServer *api.HttpServer
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Start() {
|
||||
@ -74,9 +75,9 @@ func (g *GrafanaServerImpl) Start() {
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) startHttpServer() {
|
||||
httpServer := api.NewHttpServer()
|
||||
g.httpServer = api.NewHttpServer()
|
||||
|
||||
err := httpServer.Start(g.context)
|
||||
err := g.httpServer.Start(g.context)
|
||||
|
||||
if err != nil {
|
||||
g.log.Error("Fail to start server", "error", err)
|
||||
@ -88,24 +89,15 @@ func (g *GrafanaServerImpl) startHttpServer() {
|
||||
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
||||
g.log.Info("Shutdown started", "code", code, "reason", reason)
|
||||
|
||||
err := g.httpServer.Shutdown(g.context)
|
||||
if err != nil {
|
||||
g.log.Error("Failed to shutdown server", "error", err)
|
||||
}
|
||||
|
||||
g.shutdownFn()
|
||||
err := g.childRoutines.Wait()
|
||||
err = g.childRoutines.Wait()
|
||||
|
||||
g.log.Info("Shutdown completed", "reason", err)
|
||||
log.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// implement context.Context
|
||||
func (g *GrafanaServerImpl) Deadline() (deadline time.Time, ok bool) {
|
||||
return g.context.Deadline()
|
||||
}
|
||||
func (g *GrafanaServerImpl) Done() <-chan struct{} {
|
||||
return g.context.Done()
|
||||
}
|
||||
func (g *GrafanaServerImpl) Err() error {
|
||||
return g.context.Err()
|
||||
}
|
||||
func (g *GrafanaServerImpl) Value(key interface{}) interface{} {
|
||||
return g.context.Value(key)
|
||||
}
|
||||
|
@ -35,7 +35,10 @@ func NewS3Uploader(region, bucket, acl, accessKey, secretKey string) *S3Uploader
|
||||
}
|
||||
|
||||
func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
|
||||
sess := session.New()
|
||||
sess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
@ -58,7 +61,11 @@ func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
svc := s3.New(session.New(cfg), cfg)
|
||||
sess, err = session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
svc := s3.New(sess, cfg)
|
||||
params := &s3.PutObjectInput{
|
||||
Bucket: aws.String(u.bucket),
|
||||
Key: aws.String(key),
|
||||
|
@ -4,9 +4,11 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@ -17,6 +19,18 @@ type WebdavUploader struct {
|
||||
password string
|
||||
}
|
||||
|
||||
var netTransport = &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
var netClient = &http.Client{
|
||||
Timeout: time.Second * 60,
|
||||
Transport: netTransport,
|
||||
}
|
||||
|
||||
func (u *WebdavUploader) Upload(pa string) (string, error) {
|
||||
url, _ := url.Parse(u.url)
|
||||
url.Path = path.Join(url.Path, util.GetRandomString(20)+".png")
|
||||
@ -28,7 +42,7 @@ func (u *WebdavUploader) Upload(pa string) (string, error) {
|
||||
req.SetBasicAuth(u.username, u.password)
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
res, err := netClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -49,7 +49,9 @@ var (
|
||||
M_Alerting_Notification_Sent_Victorops Counter
|
||||
M_Alerting_Notification_Sent_OpsGenie Counter
|
||||
M_Alerting_Notification_Sent_Telegram Counter
|
||||
M_Alerting_Notification_Sent_Threema Counter
|
||||
M_Alerting_Notification_Sent_Sensu Counter
|
||||
M_Alerting_Notification_Sent_Pushover Counter
|
||||
M_Aws_CloudWatch_GetMetricStatistics Counter
|
||||
M_Aws_CloudWatch_ListMetrics Counter
|
||||
|
||||
@ -118,8 +120,10 @@ func initMetricVars(settings *MetricSettings) {
|
||||
M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
|
||||
M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie")
|
||||
M_Alerting_Notification_Sent_Telegram = RegCounter("alerting.notifications_sent", "type", "telegram")
|
||||
M_Alerting_Notification_Sent_Threema = RegCounter("alerting.notifications_sent", "type", "threema")
|
||||
M_Alerting_Notification_Sent_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu")
|
||||
M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE")
|
||||
M_Alerting_Notification_Sent_Pushover = RegCounter("alerting.notifications_sent", "type", "pushover")
|
||||
|
||||
M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
|
||||
M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")
|
||||
|
@ -326,6 +326,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
|
||||
// mock out gc goroutine
|
||||
startSessionGC = func() {}
|
||||
sc.m.Use(Sessioner(&session.Options{}))
|
||||
sc.m.Use(OrgRedirect())
|
||||
|
||||
sc.defaultHandler = func(c *Context) {
|
||||
sc.context = c
|
||||
|
44
pkg/middleware/org_redirect.go
Normal file
44
pkg/middleware/org_redirect.go
Normal file
@ -0,0 +1,44 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func OrgRedirect() macaron.Handler {
|
||||
return func(res http.ResponseWriter, req *http.Request, c *macaron.Context) {
|
||||
orgIdValue := req.URL.Query().Get("orgId")
|
||||
orgId, err := strconv.ParseInt(orgIdValue, 10, 32)
|
||||
|
||||
if err != nil || orgId == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, ok := c.Data["ctx"].(*Context)
|
||||
if !ok || !ctx.IsSignedIn {
|
||||
return
|
||||
}
|
||||
|
||||
if orgId == ctx.OrgId {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := models.SetUsingOrgCommand{UserId: ctx.UserId, OrgId: orgId}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if ctx.IsApiRequest() {
|
||||
ctx.JsonApiErr(404, "Not found", nil)
|
||||
} else {
|
||||
ctx.Error(404, "Not found")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(c.Req.URL.String(), 302)
|
||||
}
|
||||
}
|
60
pkg/middleware/org_redirect_test.go
Normal file
60
pkg/middleware/org_redirect_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestOrgRedirectMiddleware(t *testing.T) {
|
||||
|
||||
Convey("Can redirect to correct org", t, func() {
|
||||
middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) {
|
||||
sc.fakeReq("GET", "/").handler(func(c *Context) {
|
||||
c.Session.Set(SESS_KEY_USERID, int64(12))
|
||||
}).exec()
|
||||
|
||||
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.m.Get("/", sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/?orgId=3").exec()
|
||||
|
||||
Convey("change org and redirect", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 302)
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) {
|
||||
sc.fakeReq("GET", "/").handler(func(c *Context) {
|
||||
c.Session.Set(SESS_KEY_USERID, int64(12))
|
||||
}).exec()
|
||||
|
||||
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
|
||||
return fmt.Errorf("")
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.m.Get("/", sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/?orgId=3").exec()
|
||||
|
||||
Convey("not allowed to change org", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 404)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/go-macaron/session"
|
||||
@ -8,6 +9,7 @@ import (
|
||||
_ "github.com/go-macaron/session/mysql"
|
||||
_ "github.com/go-macaron/session/postgres"
|
||||
_ "github.com/go-macaron/session/redis"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
@ -22,10 +24,12 @@ var sessionManager *session.Manager
|
||||
var sessionOptions *session.Options
|
||||
var startSessionGC func()
|
||||
var getSessionCount func() int
|
||||
var sessionLogger = log.New("session")
|
||||
|
||||
func init() {
|
||||
startSessionGC = func() {
|
||||
sessionManager.GC()
|
||||
sessionLogger.Debug("Session GC")
|
||||
time.AfterFunc(time.Duration(sessionOptions.Gclifetime)*time.Second, startSessionGC)
|
||||
}
|
||||
getSessionCount = func() int {
|
||||
@ -67,7 +71,9 @@ func Sessioner(options *session.Options) macaron.Handler {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go startSessionGC()
|
||||
// start GC threads after some random seconds
|
||||
rndSeconds := 10 + rand.Int63n(180)
|
||||
time.AfterFunc(time.Duration(rndSeconds)*time.Second, startSessionGC)
|
||||
|
||||
return func(ctx *Context) {
|
||||
ctx.Next()
|
||||
|
@ -35,6 +35,7 @@ const (
|
||||
|
||||
var (
|
||||
ErrCannotChangeStateOnPausedAlert error = fmt.Errorf("Cannot change state on pause alert")
|
||||
ErrRequiresNewState error = fmt.Errorf("update alert state requires a new state.")
|
||||
)
|
||||
|
||||
func (s AlertStateType) IsValid() bool {
|
||||
|
@ -120,11 +120,16 @@ type UpdateDataSourceCommand struct {
|
||||
Id int64 `json:"-"`
|
||||
}
|
||||
|
||||
type DeleteDataSourceCommand struct {
|
||||
type DeleteDataSourceByIdCommand struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
}
|
||||
|
||||
type DeleteDataSourceByNameCommand struct {
|
||||
Name string
|
||||
OrgId int64
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// QUERIES
|
||||
|
||||
|
@ -1,10 +1,6 @@
|
||||
package models
|
||||
|
||||
import "context"
|
||||
|
||||
type GrafanaServer interface {
|
||||
context.Context
|
||||
|
||||
Start()
|
||||
Shutdown(code int, reason string)
|
||||
}
|
||||
|
@ -130,7 +130,14 @@ type SearchUsersQuery struct {
|
||||
Page int
|
||||
Limit int
|
||||
|
||||
Result []*UserSearchHitDTO
|
||||
Result SearchUserQueryResult
|
||||
}
|
||||
|
||||
type SearchUserQueryResult struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Users []*UserSearchHitDTO `json:"users"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
|
||||
type GetUserOrgListQuery struct {
|
||||
|
@ -98,7 +98,7 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
|
||||
}
|
||||
}()
|
||||
|
||||
alertCtx, cancelFn := context.WithTimeout(context.TODO(), alertTimeout)
|
||||
alertCtx, cancelFn := context.WithTimeout(context.Background(), alertTimeout)
|
||||
|
||||
job.Running = true
|
||||
evalContext := NewEvalContext(alertCtx, job.Rule)
|
||||
|
@ -113,7 +113,7 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
if slug, err := c.GetDashboardSlug(); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId)
|
||||
ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d", setting.AppUrl, slug, c.Rule.PanelId, c.Rule.OrgId)
|
||||
return ruleUrl, nil
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,18 @@ type Notifier interface {
|
||||
GetIsDefault() bool
|
||||
}
|
||||
|
||||
type NotifierSlice []Notifier
|
||||
|
||||
func (notifiers NotifierSlice) ShouldUploadImage() bool {
|
||||
for _, notifier := range notifiers {
|
||||
if notifier.NeedsImage() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type ConditionResult struct {
|
||||
Firing bool
|
||||
NoDataFound bool
|
||||
|
@ -21,37 +21,25 @@ type NotifierPlugin struct {
|
||||
Factory NotifierFactory `json:"-"`
|
||||
}
|
||||
|
||||
type RootNotifier struct {
|
||||
type NotificationService interface {
|
||||
Send(context *EvalContext) error
|
||||
}
|
||||
|
||||
func NewNotificationService() NotificationService {
|
||||
return newNotificationService()
|
||||
}
|
||||
|
||||
type notificationService struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewRootNotifier() *RootNotifier {
|
||||
return &RootNotifier{
|
||||
func newNotificationService() *notificationService {
|
||||
return ¬ificationService{
|
||||
log: log.New("alerting.notifier"),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *RootNotifier) GetType() string {
|
||||
return "root"
|
||||
}
|
||||
|
||||
func (n *RootNotifier) NeedsImage() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *RootNotifier) PassesFilter(rule *Rule) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *RootNotifier) GetNotifierId() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (n *RootNotifier) GetIsDefault() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *RootNotifier) Notify(context *EvalContext) error {
|
||||
func (n *notificationService) Send(context *EvalContext) error {
|
||||
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -63,14 +51,16 @@ func (n *RootNotifier) Notify(context *EvalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = n.uploadImage(context); err != nil {
|
||||
n.log.Error("Failed to upload alert panel image.", "error", err)
|
||||
if notifiers.ShouldUploadImage() {
|
||||
if err = n.uploadImage(context); err != nil {
|
||||
n.log.Error("Failed to upload alert panel image.", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return n.sendNotifications(context, notifiers)
|
||||
}
|
||||
|
||||
func (n *RootNotifier) sendNotifications(context *EvalContext, notifiers []Notifier) error {
|
||||
func (n *notificationService) sendNotifications(context *EvalContext, notifiers []Notifier) error {
|
||||
g, _ := errgroup.WithContext(context.Ctx)
|
||||
|
||||
for _, notifier := range notifiers {
|
||||
@ -82,7 +72,7 @@ func (n *RootNotifier) sendNotifications(context *EvalContext, notifiers []Notif
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {
|
||||
func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
uploader, err := imguploader.NewImageUploader()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -116,7 +106,7 @@ func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) ([]Notifier, error) {
|
||||
func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
|
||||
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
|
||||
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
@ -137,7 +127,7 @@ func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, contex
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
|
||||
func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
|
||||
notifierPlugin, found := notifierFactories[model.Type]
|
||||
if !found {
|
||||
return nil, errors.New("Unsupported notification type")
|
||||
|
@ -6,18 +6,22 @@ import (
|
||||
)
|
||||
|
||||
type NotifierBase struct {
|
||||
Name string
|
||||
Type string
|
||||
Id int64
|
||||
IsDeault bool
|
||||
Name string
|
||||
Type string
|
||||
Id int64
|
||||
IsDeault bool
|
||||
UploadImage bool
|
||||
}
|
||||
|
||||
func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase {
|
||||
uploadImage := model.Get("uploadImage").MustBool(true)
|
||||
|
||||
return NotifierBase{
|
||||
Id: id,
|
||||
Name: name,
|
||||
IsDeault: isDefault,
|
||||
Type: notifierType,
|
||||
Id: id,
|
||||
Name: name,
|
||||
IsDeault: isDefault,
|
||||
Type: notifierType,
|
||||
UploadImage: uploadImage,
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +34,7 @@ func (n *NotifierBase) GetType() string {
|
||||
}
|
||||
|
||||
func (n *NotifierBase) NeedsImage() bool {
|
||||
return true
|
||||
return n.UploadImage
|
||||
}
|
||||
|
||||
func (n *NotifierBase) GetNotifierId() int64 {
|
||||
|
@ -16,7 +16,7 @@ func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "email",
|
||||
Name: "Email",
|
||||
Description: "Sends notifications using Grafana server configured STMP settings",
|
||||
Description: "Sends notifications using Grafana server configured SMTP settings",
|
||||
Factory: NewEmailNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Email addresses</h3>
|
||||
|
155
pkg/services/alerting/notifiers/hipchat.go
Normal file
155
pkg/services/alerting/notifiers/hipchat.go
Normal file
@ -0,0 +1,155 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "hipchat",
|
||||
Name: "HipChat",
|
||||
Description: "Sends notifications uto a HipChat Room",
|
||||
Factory: NewHipChatNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">HipChat settings</h3>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Hip Chat Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="HipChat URL (ex https://grafana.hipchat.com)"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">API Key</span>
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.apikey" placeholder="HipChat API Key"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Room ID</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.roomid"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const (
|
||||
maxFieldCount int = 4
|
||||
)
|
||||
|
||||
func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
|
||||
url := model.Settings.Get("url").MustString()
|
||||
if strings.HasSuffix(url, "/") {
|
||||
url = url[:len(url)-1]
|
||||
}
|
||||
if url == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
|
||||
}
|
||||
|
||||
apikey := model.Settings.Get("apikey").MustString()
|
||||
roomId := model.Settings.Get("roomid").MustString()
|
||||
|
||||
return &HipChatNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
ApiKey: apikey,
|
||||
RoomId: roomId,
|
||||
log: log.New("alerting.notifier.hipchat"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type HipChatNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
ApiKey string
|
||||
RoomId string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed get rule link", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "<br><a href=" + ruleUrl + ">Check Dasboard</a>"
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
message += "<br>"
|
||||
for index, evt := range evalContext.EvalMatches {
|
||||
message += evt.Metric + " :: " + strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64) + "<br>"
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"title": evt.Metric,
|
||||
"value": evt.Value,
|
||||
"short": true,
|
||||
})
|
||||
if index > maxFieldCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if evalContext.Error != nil {
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"title": "Error message",
|
||||
"value": evalContext.Error.Error(),
|
||||
"short": false,
|
||||
})
|
||||
}
|
||||
|
||||
if evalContext.Rule.State != models.AlertStateOK { //dont add message when going back to alert state ok.
|
||||
message += " " + evalContext.Rule.Message
|
||||
}
|
||||
//HipChat has a set list of colors
|
||||
var color string
|
||||
switch evalContext.Rule.State {
|
||||
case models.AlertStateOK:
|
||||
color = "green"
|
||||
case models.AlertStateNoData:
|
||||
color = "grey"
|
||||
case models.AlertStateAlerting:
|
||||
color = "red"
|
||||
}
|
||||
|
||||
// Add a card with link to the dashboard
|
||||
card := map[string]interface{}{
|
||||
"style": "link",
|
||||
"url": ruleUrl,
|
||||
"id": "1",
|
||||
"title": evalContext.GetNotificationTitle(),
|
||||
"description": evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text,
|
||||
"icon": map[string]interface{}{
|
||||
"url": "http://grafana.org/assets/img/fav32.png",
|
||||
},
|
||||
"date": evalContext.EndTime.Unix(),
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"message": message,
|
||||
"notify": "true",
|
||||
"message_format": "html",
|
||||
"color": color,
|
||||
"card": card,
|
||||
}
|
||||
|
||||
hipUrl := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", this.Url, this.RoomId, this.ApiKey)
|
||||
data, _ := json.Marshal(&body)
|
||||
cmd := &models.SendWebhookSync{Url: hipUrl, Body: string(data)}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send hipchat notification", "error", err, "webhook", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
81
pkg/services/alerting/notifiers/hipchat_test.go
Normal file
81
pkg/services/alerting/notifiers/hipchat_test.go
Normal file
@ -0,0 +1,81 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestHipChatNotifier(t *testing.T) {
|
||||
Convey("HipChat notifier tests", t, func() {
|
||||
|
||||
Convey("Parsing alert notification from settings", func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "hipchat",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewHipChatNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("from settings", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "http://google.com"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "hipchat",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewHipChatNotifier(model)
|
||||
hipchatNotifier := not.(*HipChatNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(hipchatNotifier.Name, ShouldEqual, "ops")
|
||||
So(hipchatNotifier.Type, ShouldEqual, "hipchat")
|
||||
So(hipchatNotifier.Url, ShouldEqual, "http://google.com")
|
||||
So(hipchatNotifier.ApiKey, ShouldEqual, "")
|
||||
So(hipchatNotifier.RoomId, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("from settings with Recipient and Mention", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "http://www.hipchat.com",
|
||||
"apikey": "1234",
|
||||
"roomid": "1234"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "hipchat",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewHipChatNotifier(model)
|
||||
hipchatNotifier := not.(*HipChatNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(hipchatNotifier.Name, ShouldEqual, "ops")
|
||||
So(hipchatNotifier.Type, ShouldEqual, "hipchat")
|
||||
So(hipchatNotifier.Url, ShouldEqual, "http://www.hipchat.com")
|
||||
So(hipchatNotifier.ApiKey, ShouldEqual, "1234")
|
||||
So(hipchatNotifier.RoomId, ShouldEqual, "1234")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
177
pkg/services/alerting/notifiers/pushover.go
Normal file
177
pkg/services/alerting/notifiers/pushover.go
Normal file
@ -0,0 +1,177 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
const PUSHOVER_ENDPOINT = "https://api.pushover.net/1/messages.json"
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "pushover",
|
||||
Name: "Pushover",
|
||||
Description: "Sends HTTP POST request to the Pushover API",
|
||||
Factory: NewPushoverNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Pushover settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">API Token</span>
|
||||
<input type="text" class="gf-form-input" required placeholder="Application token" ng-model="ctrl.model.settings.apiToken"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">User key(s)</span>
|
||||
<input type="text" class="gf-form-input" required placeholder="comma-separated list" ng-model="ctrl.model.settings.userKey"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Device(s) (optional)</span>
|
||||
<input type="text" class="gf-form-input" placeholder="comma-separated list; leave empty to send to all devices" ng-model="ctrl.model.settings.device"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Priority</span>
|
||||
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.priority" ng-options="v as k for (k, v) in {
|
||||
Emergency: '2',
|
||||
High: '1',
|
||||
Normal: '0',
|
||||
Low: '-1',
|
||||
Lowest: '-2'
|
||||
}" ng-init="ctrl.model.settings.priority=ctrl.model.settings.priority||'0'"></select>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
|
||||
<span class="gf-form-label width-10">Retry</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="minimum 30 seconds" ng-model="ctrl.model.settings.retry" ng-init="ctrl.model.settings.retry=ctrl.model.settings.retry||'60'></input>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
|
||||
<span class="gf-form-label width-10">Expire</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="maximum 86400 seconds" ng-model="ctrl.model.settings.expire" ng-init="ctrl.model.settings.expire=ctrl.model.settings.expire||'3600'"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Sound</span>
|
||||
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.sound" ng-options="s for s in [
|
||||
'default',
|
||||
'pushover',
|
||||
'bike',
|
||||
'bugle',
|
||||
'cashregister',
|
||||
'classical',
|
||||
'cosmic',
|
||||
'falling',
|
||||
'gamelan',
|
||||
'incoming',
|
||||
'intermission',
|
||||
'magic',
|
||||
'mechanical',
|
||||
'pianobar',
|
||||
'siren',
|
||||
'spacealarm',
|
||||
'tugboat',
|
||||
'alien',
|
||||
'climb',
|
||||
'persistent',
|
||||
'echo',
|
||||
'updown',
|
||||
'none'
|
||||
]" ng-init="ctrl.model.settings.sound=ctrl.model.settings.sound||'default'"></select>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
userKey := model.Settings.Get("userKey").MustString()
|
||||
apiToken := model.Settings.Get("apiToken").MustString()
|
||||
device := model.Settings.Get("device").MustString()
|
||||
priority, _ := strconv.Atoi(model.Settings.Get("priority").MustString())
|
||||
retry, _ := strconv.Atoi(model.Settings.Get("retry").MustString())
|
||||
expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString())
|
||||
sound := model.Settings.Get("sound").MustString()
|
||||
|
||||
if userKey == "" {
|
||||
return nil, alerting.ValidationError{Reason: "User key not given"}
|
||||
}
|
||||
if apiToken == "" {
|
||||
return nil, alerting.ValidationError{Reason: "API token not given"}
|
||||
}
|
||||
return &PushoverNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
UserKey: userKey,
|
||||
ApiToken: apiToken,
|
||||
Priority: priority,
|
||||
Retry: retry,
|
||||
Expire: expire,
|
||||
Device: device,
|
||||
Sound: sound,
|
||||
log: log.New("alerting.notifier.pushover"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type PushoverNotifier struct {
|
||||
NotifierBase
|
||||
UserKey string
|
||||
ApiToken string
|
||||
Priority int
|
||||
Retry int
|
||||
Expire int
|
||||
Device string
|
||||
Sound string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
metrics.M_Alerting_Notification_Sent_Pushover.Inc(1)
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed get rule link", "error", err)
|
||||
return err
|
||||
}
|
||||
message := evalContext.Rule.Message
|
||||
for idx, evt := range evalContext.EvalMatches {
|
||||
message += fmt.Sprintf("\n<b>%s</b>: %v", evt.Metric, evt.Value)
|
||||
if idx > 4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if evalContext.Error != nil {
|
||||
message += fmt.Sprintf("\n<b>Error message</b> %s", evalContext.Error.Error())
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Add("user", this.UserKey)
|
||||
q.Add("token", this.ApiToken)
|
||||
q.Add("priority", strconv.Itoa(this.Priority))
|
||||
if this.Priority == 2 {
|
||||
q.Add("retry", strconv.Itoa(this.Retry))
|
||||
q.Add("expire", strconv.Itoa(this.Expire))
|
||||
}
|
||||
if this.Device != "" {
|
||||
q.Add("device", this.Device)
|
||||
}
|
||||
if this.Sound != "default" {
|
||||
q.Add("sound", this.Sound)
|
||||
}
|
||||
q.Add("title", evalContext.GetNotificationTitle())
|
||||
q.Add("url", ruleUrl)
|
||||
q.Add("url_title", "Show dashboard with alert")
|
||||
q.Add("message", message)
|
||||
q.Add("html", "1")
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: PUSHOVER_ENDPOINT,
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
Body: q.Encode(),
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send pushover notification", "error", err, "webhook", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
58
pkg/services/alerting/notifiers/pushover_test.go
Normal file
58
pkg/services/alerting/notifiers/pushover_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPushoverNotifier(t *testing.T) {
|
||||
Convey("Pushover notifier tests", t, func() {
|
||||
|
||||
Convey("Parsing alert notification from settings", func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "Pushover",
|
||||
Type: "pushover",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewPushoverNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("from settings", func() {
|
||||
json := `
|
||||
{
|
||||
"apiToken": "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve",
|
||||
"userKey": "tzNZYf36y0ohWwXo4XoUrB61rz1A4o",
|
||||
"priority": "1",
|
||||
"sound": "pushover"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "Pushover",
|
||||
Type: "pushover",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewPushoverNotifier(model)
|
||||
pushoverNotifier := not.(*PushoverNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(pushoverNotifier.Name, ShouldEqual, "Pushover")
|
||||
So(pushoverNotifier.Type, ShouldEqual, "pushover")
|
||||
So(pushoverNotifier.ApiToken, ShouldEqual, "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve")
|
||||
So(pushoverNotifier.UserKey, ShouldEqual, "tzNZYf36y0ohWwXo4XoUrB61rz1A4o")
|
||||
So(pushoverNotifier.Priority, ShouldEqual, 1)
|
||||
So(pushoverNotifier.Sound, ShouldEqual, "pushover")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
171
pkg/services/alerting/notifiers/threema.go
Normal file
171
pkg/services/alerting/notifiers/threema.go
Normal file
@ -0,0 +1,171 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
var (
|
||||
threemaGwBaseURL = "https://msgapi.threema.ch/%s"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "threema",
|
||||
Name: "Threema Gateway",
|
||||
Description: "Sends notifications to Threema using the Threema Gateway",
|
||||
Factory: NewThreemaNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Threema Gateway settings</h3>
|
||||
<p>
|
||||
Notifications can be configured for any Threema Gateway ID of type
|
||||
"Basic". End-to-End IDs are not currently supported.
|
||||
</p>
|
||||
<p>
|
||||
The Threema Gateway ID can be set up at
|
||||
<a href="https://gateway.threema.ch/" target="_blank" rel="noopener noreferrer">https://gateway.threema.ch/</a>.
|
||||
</p>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Gateway ID</span>
|
||||
<input type="text" required maxlength="8" pattern="\*[0-9A-Z]{7}"
|
||||
class="gf-form-input max-width-14"
|
||||
ng-model="ctrl.model.settings.gateway_id"
|
||||
placeholder="*3MAGWID">
|
||||
</input>
|
||||
<info-popover mode="right-normal">
|
||||
Your 8 character Threema Gateway ID (starting with a *)
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Recipient ID</span>
|
||||
<input type="text" required maxlength="8" pattern="[0-9A-Z]{8}"
|
||||
class="gf-form-input max-width-14"
|
||||
ng-model="ctrl.model.settings.recipient_id"
|
||||
placeholder="YOUR3MID">
|
||||
</input>
|
||||
<info-popover mode="right-normal">
|
||||
The 8 character Threema ID that should receive the alerts
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">API Secret</span>
|
||||
<input type="text" required
|
||||
class="gf-form-input max-width-24"
|
||||
ng-model="ctrl.model.settings.api_secret">
|
||||
</input>
|
||||
<info-popover mode="right-normal">
|
||||
Your Threema Gateway API secret
|
||||
</info-popover>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type ThreemaNotifier struct {
|
||||
NotifierBase
|
||||
GatewayID string
|
||||
RecipientID string
|
||||
APISecret string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
if model.Settings == nil {
|
||||
return nil, alerting.ValidationError{Reason: "No Settings Supplied"}
|
||||
}
|
||||
|
||||
gatewayID := model.Settings.Get("gateway_id").MustString()
|
||||
recipientID := model.Settings.Get("recipient_id").MustString()
|
||||
apiSecret := model.Settings.Get("api_secret").MustString()
|
||||
|
||||
// Validation
|
||||
if gatewayID == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find Threema Gateway ID in settings"}
|
||||
}
|
||||
if !strings.HasPrefix(gatewayID, "*") {
|
||||
return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must start with a *"}
|
||||
}
|
||||
if len(gatewayID) != 8 {
|
||||
return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must be 8 characters long"}
|
||||
}
|
||||
if recipientID == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find Threema Recipient ID in settings"}
|
||||
}
|
||||
if len(recipientID) != 8 {
|
||||
return nil, alerting.ValidationError{Reason: "Invalid Threema Recipient ID: Must be 8 characters long"}
|
||||
}
|
||||
if apiSecret == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find Threema API secret in settings"}
|
||||
}
|
||||
|
||||
return &ThreemaNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
GatewayID: gatewayID,
|
||||
RecipientID: recipientID,
|
||||
APISecret: apiSecret,
|
||||
log: log.New("alerting.notifier.threema"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID)
|
||||
notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID)
|
||||
metrics.M_Alerting_Notification_Sent_Threema.Inc(1)
|
||||
|
||||
// Set up basic API request data
|
||||
data := url.Values{}
|
||||
data.Set("from", notifier.GatewayID)
|
||||
data.Set("to", notifier.RecipientID)
|
||||
data.Set("secret", notifier.APISecret)
|
||||
|
||||
// Determine emoji
|
||||
stateEmoji := ""
|
||||
switch evalContext.Rule.State {
|
||||
case m.AlertStateOK:
|
||||
stateEmoji = "\u2705 " // White Heavy Check Mark
|
||||
case m.AlertStateNoData:
|
||||
stateEmoji = "\u2753 " // Black Question Mark Ornament
|
||||
case m.AlertStateAlerting:
|
||||
stateEmoji = "\u26A0 " // Warning sign
|
||||
}
|
||||
|
||||
// Build message
|
||||
message := fmt.Sprintf("%s%s\n\n*State:* %s\n*Message:* %s\n",
|
||||
stateEmoji, evalContext.GetNotificationTitle(),
|
||||
evalContext.Rule.Name, evalContext.Rule.Message)
|
||||
ruleURL, err := evalContext.GetRuleUrl()
|
||||
if err == nil {
|
||||
message = message + fmt.Sprintf("*URL:* %s\n", ruleURL)
|
||||
}
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
message = message + fmt.Sprintf("*Image:* %s\n", evalContext.ImagePublicUrl)
|
||||
}
|
||||
data.Set("text", message)
|
||||
|
||||
// Prepare and send request
|
||||
url := fmt.Sprintf(threemaGwBaseURL, "send_simple")
|
||||
body := data.Encode()
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: url,
|
||||
Body: body,
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: headers,
|
||||
}
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
notifier.log.Error("Failed to send webhook", "error", err, "webhook", notifier.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
119
pkg/services/alerting/notifiers/threema_test.go
Normal file
119
pkg/services/alerting/notifiers/threema_test.go
Normal file
@ -0,0 +1,119 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestThreemaNotifier(t *testing.T) {
|
||||
Convey("Threema notifier tests", t, func() {
|
||||
|
||||
Convey("Parsing alert notification from settings", func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "threema_testing",
|
||||
Type: "threema",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewThreemaNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("valid settings should be parsed successfully", func() {
|
||||
json := `
|
||||
{
|
||||
"gateway_id": "*3MAGWID",
|
||||
"recipient_id": "ECHOECHO",
|
||||
"api_secret": "1234"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "threema_testing",
|
||||
Type: "threema",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewThreemaNotifier(model)
|
||||
So(err, ShouldBeNil)
|
||||
threemaNotifier := not.(*ThreemaNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(threemaNotifier.Name, ShouldEqual, "threema_testing")
|
||||
So(threemaNotifier.Type, ShouldEqual, "threema")
|
||||
So(threemaNotifier.GatewayID, ShouldEqual, "*3MAGWID")
|
||||
So(threemaNotifier.RecipientID, ShouldEqual, "ECHOECHO")
|
||||
So(threemaNotifier.APISecret, ShouldEqual, "1234")
|
||||
})
|
||||
|
||||
Convey("invalid Threema Gateway IDs should be rejected (prefix)", func() {
|
||||
json := `
|
||||
{
|
||||
"gateway_id": "ECHOECHO",
|
||||
"recipient_id": "ECHOECHO",
|
||||
"api_secret": "1234"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "threema_testing",
|
||||
Type: "threema",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewThreemaNotifier(model)
|
||||
So(not, ShouldBeNil)
|
||||
So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must start with a *")
|
||||
})
|
||||
|
||||
Convey("invalid Threema Gateway IDs should be rejected (length)", func() {
|
||||
json := `
|
||||
{
|
||||
"gateway_id": "*ECHOECHO",
|
||||
"recipient_id": "ECHOECHO",
|
||||
"api_secret": "1234"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "threema_testing",
|
||||
Type: "threema",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewThreemaNotifier(model)
|
||||
So(not, ShouldBeNil)
|
||||
So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must be 8 characters long")
|
||||
})
|
||||
|
||||
Convey("invalid Threema Recipient IDs should be rejected (length)", func() {
|
||||
json := `
|
||||
{
|
||||
"gateway_id": "*3MAGWID",
|
||||
"recipient_id": "ECHOECH",
|
||||
"api_secret": "1234"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "threema_testing",
|
||||
Type: "threema",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewThreemaNotifier(model)
|
||||
So(not, ShouldBeNil)
|
||||
So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Recipient ID: Must be 8 characters long")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
@ -16,14 +16,14 @@ type ResultHandler interface {
|
||||
}
|
||||
|
||||
type DefaultResultHandler struct {
|
||||
notifier Notifier
|
||||
notifier NotificationService
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewResultHandler() *DefaultResultHandler {
|
||||
return &DefaultResultHandler{
|
||||
log: log.New("alerting.resultHandler"),
|
||||
notifier: NewRootNotifier(),
|
||||
notifier: NewNotificationService(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +61,12 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
handler.log.Error("Cannot change state on alert thats pause", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err == m.ErrRequiresNewState {
|
||||
handler.log.Info("Alert already updated")
|
||||
return nil
|
||||
}
|
||||
|
||||
handler.log.Error("Failed to save state", "error", err)
|
||||
}
|
||||
|
||||
@ -85,7 +91,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
}
|
||||
|
||||
if evalContext.ShouldSendNotification() {
|
||||
handler.notifier.Notify(evalContext)
|
||||
handler.notifier.Send(evalContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ func init() {
|
||||
}
|
||||
|
||||
func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||
notifier := NewRootNotifier()
|
||||
notifier := newNotificationService()
|
||||
|
||||
model := &m.AlertNotification{
|
||||
Name: cmd.Name,
|
||||
@ -38,10 +38,10 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return notifier.sendNotifications(createTestEvalContext(), []Notifier{notifiers})
|
||||
return notifier.sendNotifications(createTestEvalContext(cmd), []Notifier{notifiers})
|
||||
}
|
||||
|
||||
func createTestEvalContext() *EvalContext {
|
||||
func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext {
|
||||
testRule := &Rule{
|
||||
DashboardId: 1,
|
||||
PanelId: 1,
|
||||
@ -50,8 +50,10 @@ func createTestEvalContext() *EvalContext {
|
||||
State: m.AlertStateAlerting,
|
||||
}
|
||||
|
||||
ctx := NewEvalContext(context.TODO(), testRule)
|
||||
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
|
||||
ctx := NewEvalContext(context.Background(), testRule)
|
||||
if cmd.Settings.Get("uploadImage").MustBool(true) {
|
||||
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
|
||||
}
|
||||
ctx.IsTestRun = true
|
||||
ctx.Firing = true
|
||||
ctx.Error = nil
|
||||
|
@ -49,7 +49,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
|
||||
func testAlertRule(rule *Rule) *EvalContext {
|
||||
handler := NewEvalHandler()
|
||||
|
||||
context := NewEvalContext(context.TODO(), rule)
|
||||
context := NewEvalContext(context.Background(), rule)
|
||||
context.IsTestRun = true
|
||||
|
||||
handler.Eval(context)
|
||||
|
@ -11,14 +11,14 @@ type Repository interface {
|
||||
type ItemQuery struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
From int64 `json:"from"`
|
||||
To int64 `json:"from"`
|
||||
To int64 `json:"to"`
|
||||
Type ItemType `json:"type"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
NewState []string `json:"newState"`
|
||||
|
||||
Limit int64 `json:"alertId"`
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
type DeleteParams struct {
|
||||
@ -48,7 +48,7 @@ type Item struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
CategoryId int64 `json:"panelId"`
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
Type ItemType `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
|
@ -150,7 +150,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
|
||||
|
||||
return &Message{
|
||||
To: cmd.To,
|
||||
From: setting.Smtp.FromAddress,
|
||||
From: fmt.Sprintf("%s <%s>", setting.Smtp.FromName, setting.Smtp.FromAddress),
|
||||
Subject: subject,
|
||||
Body: buffer.String(),
|
||||
EmbededFiles: cmd.EmbededFiles,
|
||||
|
@ -48,7 +48,7 @@ func Init() error {
|
||||
}
|
||||
|
||||
if !util.IsEmail(setting.Smtp.FromAddress) {
|
||||
return errors.New("Invalid email address for smpt from_adress config")
|
||||
return errors.New("Invalid email address for SMTP from_address config")
|
||||
}
|
||||
|
||||
if setting.EmailCodeValidMinutes == 0 {
|
||||
|
@ -23,6 +23,7 @@ func TestNotifications(t *testing.T) {
|
||||
setting.Smtp.Enabled = true
|
||||
setting.Smtp.TemplatesPattern = "emails/*.html"
|
||||
setting.Smtp.FromAddress = "from@address.com"
|
||||
setting.Smtp.FromName = "Grafana Admin"
|
||||
|
||||
err := Init()
|
||||
So(err, ShouldBeNil)
|
||||
|
@ -18,12 +18,15 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
setting.Smtp.Enabled = true
|
||||
setting.Smtp.TemplatesPattern = "emails/*.html"
|
||||
setting.Smtp.FromAddress = "from@address.com"
|
||||
setting.Smtp.FromName = "Grafana Admin"
|
||||
setting.BuildVersion = "4.0.0"
|
||||
|
||||
err := Init()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
addToMailQueue = func(msg *Message) {
|
||||
So(msg.From, ShouldEqual, "Grafana Admin <from@address.com>")
|
||||
So(msg.To[0], ShouldEqual, "asdf@asdf.com")
|
||||
ioutil.WriteFile("../../../tmp/test_email.html", []byte(msg.Body), 0777)
|
||||
}
|
||||
|
||||
@ -40,6 +43,7 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
"RuleUrl": "http://localhost:3000/dashboard/db/graphite-dashboard",
|
||||
"ImageLink": "http://localhost:3000/render/dashboard-solo/db/graphite-dashboard?panelId=1&from=1471008499616&to=1471012099617&width=1000&height=500",
|
||||
"AlertPageUrl": "http://localhost:3000/alerting",
|
||||
"EmbededImage": "test.png",
|
||||
"EvalMatches": []map[string]string{
|
||||
{
|
||||
"Metric": "desktop",
|
||||
@ -51,7 +55,7 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
To: []string{"asdf@asdf.com "},
|
||||
To: []string{"asdf@asdf.com"},
|
||||
Template: "alert_notification.html",
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context/ctxhttp"
|
||||
|
||||
@ -22,6 +24,18 @@ type Webhook struct {
|
||||
HttpHeader map[string]string
|
||||
}
|
||||
|
||||
var netTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
var netClient = &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
Transport: netTransport,
|
||||
}
|
||||
|
||||
var (
|
||||
webhookQueue chan *Webhook
|
||||
webhookLog log.Logger
|
||||
@ -37,7 +51,7 @@ func processWebhookQueue() {
|
||||
for {
|
||||
select {
|
||||
case webhook := <-webhookQueue:
|
||||
err := sendWebRequestSync(context.TODO(), webhook)
|
||||
err := sendWebRequestSync(context.Background(), webhook)
|
||||
|
||||
if err != nil {
|
||||
webhookLog.Error("Failed to send webrequest ", "error", err)
|
||||
@ -68,7 +82,7 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
|
||||
request.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := ctxhttp.Do(ctx, http.DefaultClient, request)
|
||||
resp, err := ctxhttp.Do(ctx, netClient, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -240,6 +240,10 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
|
||||
return m.ErrCannotChangeStateOnPausedAlert
|
||||
}
|
||||
|
||||
if alert.State == cmd.State {
|
||||
return m.ErrRequiresNewState
|
||||
}
|
||||
|
||||
alert.State = cmd.State
|
||||
alert.StateChanges += 1
|
||||
alert.NewStateDate = time.Now()
|
||||
|
@ -13,7 +13,8 @@ import (
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetDataSources)
|
||||
bus.AddHandler("sql", AddDataSource)
|
||||
bus.AddHandler("sql", DeleteDataSource)
|
||||
bus.AddHandler("sql", DeleteDataSourceById)
|
||||
bus.AddHandler("sql", DeleteDataSourceByName)
|
||||
bus.AddHandler("sql", UpdateDataSource)
|
||||
bus.AddHandler("sql", GetDataSourceById)
|
||||
bus.AddHandler("sql", GetDataSourceByName)
|
||||
@ -50,7 +51,7 @@ func GetDataSources(query *m.GetDataSourcesQuery) error {
|
||||
return sess.Find(&query.Result)
|
||||
}
|
||||
|
||||
func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error {
|
||||
func DeleteDataSourceById(cmd *m.DeleteDataSourceByIdCommand) error {
|
||||
return inTransaction(func(sess *xorm.Session) error {
|
||||
var rawSql = "DELETE FROM data_source WHERE id=? and org_id=?"
|
||||
_, err := sess.Exec(rawSql, cmd.Id, cmd.OrgId)
|
||||
@ -58,6 +59,14 @@ func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteDataSourceByName(cmd *m.DeleteDataSourceByNameCommand) error {
|
||||
return inTransaction(func(sess *xorm.Session) error {
|
||||
var rawSql = "DELETE FROM data_source WHERE name=? and org_id=?"
|
||||
_, err := sess.Exec(rawSql, cmd.Name, cmd.OrgId)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func AddDataSource(cmd *m.AddDataSourceCommand) error {
|
||||
|
||||
return inTransaction(func(sess *xorm.Session) error {
|
||||
|
@ -79,8 +79,16 @@ func TestDataAccess(t *testing.T) {
|
||||
|
||||
ds := query.Result[0]
|
||||
|
||||
Convey("Can delete datasource", func() {
|
||||
err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, OrgId: ds.OrgId})
|
||||
Convey("Can delete datasource by id", func() {
|
||||
err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: ds.OrgId})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
GetDataSources(&query)
|
||||
So(len(query.Result), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Can delete datasource by name", func() {
|
||||
err := DeleteDataSourceByName(&m.DeleteDataSourceByNameCommand{Name: ds.Name, OrgId: ds.OrgId})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
GetDataSources(&query)
|
||||
@ -88,7 +96,7 @@ func TestDataAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Can not delete datasource with wrong orgId", func() {
|
||||
err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, OrgId: 123123})
|
||||
err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: 123123})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
GetDataSources(&query)
|
||||
|
123
pkg/services/sqlstore/logger.go
Normal file
123
pkg/services/sqlstore/logger.go
Normal file
@ -0,0 +1,123 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
glog "github.com/grafana/grafana/pkg/log"
|
||||
|
||||
"github.com/go-xorm/core"
|
||||
)
|
||||
|
||||
type XormLogger struct {
|
||||
grafanaLog glog.Logger
|
||||
level glog.Lvl
|
||||
showSQL bool
|
||||
}
|
||||
|
||||
func NewXormLogger(level glog.Lvl, grafanaLog glog.Logger) *XormLogger {
|
||||
return &XormLogger{
|
||||
grafanaLog: grafanaLog,
|
||||
level: level,
|
||||
showSQL: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Error implement core.ILogger
|
||||
func (s *XormLogger) Err(v ...interface{}) error {
|
||||
if s.level <= glog.LvlError {
|
||||
s.grafanaLog.Error(fmt.Sprint(v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Errorf implement core.ILogger
|
||||
func (s *XormLogger) Errf(format string, v ...interface{}) error {
|
||||
if s.level <= glog.LvlError {
|
||||
s.grafanaLog.Error(fmt.Sprintf(format, v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debug implement core.ILogger
|
||||
func (s *XormLogger) Debug(v ...interface{}) error {
|
||||
if s.level <= glog.LvlDebug {
|
||||
s.grafanaLog.Debug(fmt.Sprint(v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debugf implement core.ILogger
|
||||
func (s *XormLogger) Debugf(format string, v ...interface{}) error {
|
||||
if s.level <= glog.LvlDebug {
|
||||
s.grafanaLog.Debug(fmt.Sprintf(format, v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Info implement core.ILogger
|
||||
func (s *XormLogger) Info(v ...interface{}) error {
|
||||
if s.level <= glog.LvlInfo {
|
||||
s.grafanaLog.Info(fmt.Sprint(v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Infof implement core.ILogger
|
||||
func (s *XormLogger) Infof(format string, v ...interface{}) error {
|
||||
if s.level <= glog.LvlInfo {
|
||||
s.grafanaLog.Info(fmt.Sprintf(format, v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Warn implement core.ILogger
|
||||
func (s *XormLogger) Warning(v ...interface{}) error {
|
||||
if s.level <= glog.LvlWarn {
|
||||
s.grafanaLog.Warn(fmt.Sprint(v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Warnf implement core.ILogger
|
||||
func (s *XormLogger) Warningf(format string, v ...interface{}) error {
|
||||
if s.level <= glog.LvlWarn {
|
||||
s.grafanaLog.Warn(fmt.Sprintf(format, v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Level implement core.ILogger
|
||||
func (s *XormLogger) Level() core.LogLevel {
|
||||
switch s.level {
|
||||
case glog.LvlError:
|
||||
return core.LOG_ERR
|
||||
case glog.LvlWarn:
|
||||
return core.LOG_WARNING
|
||||
case glog.LvlInfo:
|
||||
return core.LOG_INFO
|
||||
case glog.LvlDebug:
|
||||
return core.LOG_DEBUG
|
||||
default:
|
||||
return core.LOG_ERR
|
||||
}
|
||||
}
|
||||
|
||||
// SetLevel implement core.ILogger
|
||||
func (s *XormLogger) SetLevel(l core.LogLevel) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShowSQL implement core.ILogger
|
||||
func (s *XormLogger) ShowSQL(show ...bool) {
|
||||
s.grafanaLog.Error("ShowSQL", "show", "show")
|
||||
if len(show) == 0 {
|
||||
s.showSQL = true
|
||||
return
|
||||
}
|
||||
s.showSQL = show[0]
|
||||
}
|
||||
|
||||
// IsShowSQL implement core.ILogger
|
||||
func (s *XormLogger) IsShowSQL() bool {
|
||||
return s.showSQL
|
||||
}
|
@ -63,8 +63,8 @@ func TestAccountDataAccess(t *testing.T) {
|
||||
err := SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result[0].Email, ShouldEqual, "ac1@test.com")
|
||||
So(query.Result[1].Email, ShouldEqual, "ac2@test.com")
|
||||
So(query.Result.Users[0].Email, ShouldEqual, "ac1@test.com")
|
||||
So(query.Result.Users[1].Email, ShouldEqual, "ac2@test.com")
|
||||
})
|
||||
|
||||
Convey("Given an added org user", func() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user