mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into websocket
This commit is contained in:
commit
8a95c563bb
52
CHANGELOG.md
52
CHANGELOG.md
@ -1,4 +1,53 @@
|
||||
# 4.1-beta (unreleased)
|
||||
# 4.2.0 (unreleased)
|
||||
|
||||
## Enhancements
|
||||
* **Alerting**: 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)
|
||||
* **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)
|
||||
|
||||
## Tech
|
||||
|
||||
* **Library Upgrade**: Upgraded angularjs from 1.5.8 to 1.6.1 [#7274](https://github.com/grafana/grafana/issues/7274)
|
||||
|
||||
## Bugfixes
|
||||
* **Alerting**: Fixes missing support for no_data and execution error when testing alerts [#7149](https://github.com/grafana/grafana/issues/7149)
|
||||
* **Dashboard**: Avoid duplicate data in dashboard json for panels with alerts [#7256](https://github.com/grafana/grafana/pull/7256)
|
||||
* **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)
|
||||
|
||||
# 4.1.2 (unreleased)
|
||||
|
||||
### Bugfixes
|
||||
* **Table**: Fixes broken annotation rendering mode in the table panel [#7268](https://github.com/grafana/grafana/issues/7268)
|
||||
|
||||
# 4.1.1 (2017-01-11)
|
||||
|
||||
### Bugfixes
|
||||
* **Graph Panel**: Fixed issue with legend height in table mode [#7221](https://github.com/grafana/grafana/issues/7221)
|
||||
|
||||
# 4.1.0 (2017-01-11)
|
||||
|
||||
### Bugfixes
|
||||
* **Server side PNG rendering**: Fixed issue with y-axis label rotation in phantomjs rendered images [#6924](https://github.com/grafana/grafana/issues/6924)
|
||||
* **Graph**: Fixed centering of y-axis label [#7099](https://github.com/grafana/grafana/issues/7099)
|
||||
* **Graph**: Fixed graph legend table mode and always visible scrollbar [#6828](https://github.com/grafana/grafana/issues/6828)
|
||||
* **Templating**: Fixed template variable value groups/tags feature [#6752](https://github.com/grafana/grafana/issues/6752)
|
||||
* **Webhook**: Fixed webhook username mismatch [#7195](https://github.com/grafana/grafana/pull/7195), thx [@theisenmark](https://github.com/theisenmark)
|
||||
* **Influxdb**: Handles time(auto) the same way as time($interval) [#6997](https://github.com/grafana/grafana/issues/6997)
|
||||
|
||||
## Enhancements
|
||||
* **Elasticsearch**: Added support for all moving average options [#7154](https://github.com/grafana/grafana/pull/7154), thx [@vaibhavinbayarea](https://github.com/vaibhavinbayarea)
|
||||
|
||||
# 4.1-beta1 (2016-12-21)
|
||||
|
||||
### Enhancements
|
||||
* **Postgres**: Add support for Certs for Postgres database [#6655](https://github.com/grafana/grafana/issues/6655)
|
||||
@ -17,6 +66,7 @@
|
||||
* **Alerting**: Adds OK as no data option. [#6866](https://github.com/grafana/grafana/issues/6866)
|
||||
* **Alert list**: Order alerts based on state. [#6676](https://github.com/grafana/grafana/issues/6676)
|
||||
* **Alerting**: Add api endpoint for pausing all alerts. [#6589](https://github.com/grafana/grafana/issues/6589)
|
||||
* **Panel**: Added help text for panels. [#4079](https://github.com/grafana/grafana/issues/4079), thx [@utkarshcmu](https://github.com/utkarshcmu)
|
||||
|
||||
### Bugfixes
|
||||
* **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679)
|
||||
|
2
Makefile
2
Makefile
@ -4,7 +4,7 @@ deps-go:
|
||||
go run build.go setup
|
||||
|
||||
deps-js:
|
||||
npm install
|
||||
yarn install --pure-lockfile
|
||||
|
||||
deps: deps-go deps-js
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
[Grafana](http://grafana.org) [](https://circleci.com/gh/grafana/grafana) [](https://coveralls.io/r/grafana/grafana)
|
||||
[Grafana](http://grafana.org) [](https://circleci.com/gh/grafana/grafana)
|
||||
================
|
||||
[Website](http://grafana.org) |
|
||||
[Twitter](https://twitter.com/grafana) |
|
||||
@ -10,7 +10,7 @@
|
||||
Grafana is an open source, feature rich metrics dashboard and graph editor for
|
||||
Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
|
||||
|
||||

|
||||

|
||||
|
||||
- [Install instructions](http://docs.grafana.org/installation/)
|
||||
- [What's New in Grafana 2.0](http://docs.grafana.org/guides/whats-new-in-v2/)
|
||||
@ -18,6 +18,7 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
|
||||
- [What's New in Grafana 2.5](http://docs.grafana.org/guides/whats-new-in-v2-5/)
|
||||
- [What's New in Grafana 3.0](http://docs.grafana.org/guides/whats-new-in-v3/)
|
||||
- [What's New in Grafana 4.0](http://docs.grafana.org/guides/whats-new-in-v4/)
|
||||
- [What's New in Grafana 4.1](http://docs.grafana.org/guides/whats-new-in-v4-1/)
|
||||
|
||||
## Features
|
||||
### Graphite Target Editor
|
||||
@ -113,7 +114,8 @@ To build less to css for the frontend you will need a recent version of **node (
|
||||
npm (v2.5.0) and grunt (v0.4.5). Run the following:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm install -g yarn
|
||||
yarn install --pure-lockfile
|
||||
npm run build
|
||||
```
|
||||
|
||||
|
@ -5,13 +5,14 @@ os: Windows Server 2012 R2
|
||||
clone_folder: c:\gopath\src\github.com\grafana\grafana
|
||||
|
||||
environment:
|
||||
nodejs_version: "5"
|
||||
nodejs_version: "6"
|
||||
GOPATH: c:\gopath
|
||||
|
||||
install:
|
||||
# install nodejs and npm
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- npm install
|
||||
- npm install -g yarn
|
||||
- yarn install --pure-lockfile
|
||||
- npm install -g grunt-cli
|
||||
# install gcc (needed for sqlite3)
|
||||
- choco install -y --limit-output mingw
|
||||
|
11
bower.json
11
bower.json
@ -15,11 +15,12 @@
|
||||
"dependencies": {
|
||||
"jquery": "3.1.0",
|
||||
"lodash": "4.15.0",
|
||||
"angular": "1.5.8",
|
||||
"angular-route": "1.5.8",
|
||||
"angular-mocks": "1.5.8",
|
||||
"angular-sanitize": "1.5.8",
|
||||
"angular": "1.6.1",
|
||||
"angular-route": "1.6.1",
|
||||
"angular-mocks": "1.6.1",
|
||||
"angular-sanitize": "1.6.1",
|
||||
"angular-native-dragdrop": "1.2.2",
|
||||
"angular-bindonce": "0.3.3"
|
||||
"angular-bindonce": "0.3.3",
|
||||
"clipboard": "^1.5.16"
|
||||
}
|
||||
}
|
||||
|
62
build.go
62
build.go
@ -37,6 +37,7 @@ var (
|
||||
race bool
|
||||
phjsToRelease string
|
||||
workingDir string
|
||||
includeBuildNumber bool = true
|
||||
binaries []string = []string{"grafana-server", "grafana-cli"}
|
||||
)
|
||||
|
||||
@ -47,9 +48,6 @@ func main() {
|
||||
log.SetFlags(0)
|
||||
|
||||
ensureGoPath()
|
||||
readVersionFromPackageJson()
|
||||
|
||||
log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration)
|
||||
|
||||
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
|
||||
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
|
||||
@ -59,8 +57,13 @@ func main() {
|
||||
flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
|
||||
flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
|
||||
flag.BoolVar(&race, "race", race, "Use race detector")
|
||||
flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
|
||||
flag.Parse()
|
||||
|
||||
readVersionFromPackageJson()
|
||||
|
||||
log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration)
|
||||
|
||||
if flag.NArg() == 0 {
|
||||
log.Println("Usage: go run build.go build")
|
||||
return
|
||||
@ -73,9 +76,9 @@ func main() {
|
||||
case "setup":
|
||||
setup()
|
||||
|
||||
case "build-cli":
|
||||
clean()
|
||||
build("grafana-cli", "./pkg/cmd/grafana-cli", []string{})
|
||||
case "build-cli":
|
||||
clean()
|
||||
build("grafana-cli", "./pkg/cmd/grafana-cli", []string{})
|
||||
|
||||
case "build":
|
||||
clean()
|
||||
@ -90,24 +93,20 @@ func main() {
|
||||
case "package":
|
||||
grunt(gruntBuildArg("release")...)
|
||||
createLinuxPackages()
|
||||
sha1FilesInDist()
|
||||
|
||||
case "pkg-rpm":
|
||||
grunt(gruntBuildArg("release")...)
|
||||
createRpmPackages()
|
||||
sha1FilesInDist()
|
||||
|
||||
case "pkg-deb":
|
||||
grunt(gruntBuildArg("release")...)
|
||||
createDebPackages()
|
||||
sha1FilesInDist()
|
||||
|
||||
case "sha1-dist":
|
||||
sha1FilesInDist()
|
||||
case "sha1-dist":
|
||||
sha1FilesInDist()
|
||||
|
||||
case "latest":
|
||||
makeLatestDistCopies()
|
||||
sha1FilesInDist()
|
||||
|
||||
case "clean":
|
||||
clean()
|
||||
@ -157,7 +156,9 @@ func readVersionFromPackageJson() {
|
||||
}
|
||||
|
||||
// add timestamp to iteration
|
||||
linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration)
|
||||
if includeBuildNumber {
|
||||
linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration)
|
||||
}
|
||||
}
|
||||
|
||||
type linuxPackageOptions struct {
|
||||
@ -167,7 +168,6 @@ type linuxPackageOptions struct {
|
||||
serverBinPath string
|
||||
cliBinPath string
|
||||
configDir string
|
||||
configFilePath string
|
||||
ldapFilePath string
|
||||
etcDefaultPath string
|
||||
etcDefaultFilePath string
|
||||
@ -188,8 +188,6 @@ func createDebPackages() {
|
||||
homeDir: "/usr/share/grafana",
|
||||
binPath: "/usr/sbin",
|
||||
configDir: "/etc/grafana",
|
||||
configFilePath: "/etc/grafana/grafana.ini",
|
||||
ldapFilePath: "/etc/grafana/ldap.toml",
|
||||
etcDefaultPath: "/etc/default",
|
||||
etcDefaultFilePath: "/etc/default/grafana-server",
|
||||
initdScriptFilePath: "/etc/init.d/grafana-server",
|
||||
@ -210,8 +208,6 @@ func createRpmPackages() {
|
||||
homeDir: "/usr/share/grafana",
|
||||
binPath: "/usr/sbin",
|
||||
configDir: "/etc/grafana",
|
||||
configFilePath: "/etc/grafana/grafana.ini",
|
||||
ldapFilePath: "/etc/grafana/ldap.toml",
|
||||
etcDefaultPath: "/etc/sysconfig",
|
||||
etcDefaultFilePath: "/etc/sysconfig/grafana-server",
|
||||
initdScriptFilePath: "/etc/init.d/grafana-server",
|
||||
@ -222,7 +218,7 @@ func createRpmPackages() {
|
||||
defaultFileSrc: "packaging/rpm/sysconfig/grafana-server",
|
||||
systemdFileSrc: "packaging/rpm/systemd/grafana-server.service",
|
||||
|
||||
depends: []string{"initscripts", "fontconfig"},
|
||||
depends: []string{"/sbin/service", "fontconfig"},
|
||||
})
|
||||
}
|
||||
|
||||
@ -256,10 +252,6 @@ func createPackage(options linuxPackageOptions) {
|
||||
runPrint("cp", "-a", filepath.Join(workingDir, "tmp")+"/.", filepath.Join(packageRoot, options.homeDir))
|
||||
// remove bin path
|
||||
runPrint("rm", "-rf", filepath.Join(packageRoot, options.homeDir, "bin"))
|
||||
// copy sample ini file to /etc/grafana
|
||||
runPrint("cp", "conf/sample.ini", filepath.Join(packageRoot, options.configFilePath))
|
||||
// copy sample ldap toml config file to /etc/grafana/ldap.toml
|
||||
runPrint("cp", "conf/ldap.toml", filepath.Join(packageRoot, options.ldapFilePath))
|
||||
|
||||
args := []string{
|
||||
"-s", "dir",
|
||||
@ -269,8 +261,6 @@ func createPackage(options linuxPackageOptions) {
|
||||
"--url", "http://grafana.org",
|
||||
"--license", "\"Apache 2.0\"",
|
||||
"--maintainer", "contact@grafana.org",
|
||||
"--config-files", options.configFilePath,
|
||||
"--config-files", options.ldapFilePath,
|
||||
"--config-files", options.initdScriptFilePath,
|
||||
"--config-files", options.etcDefaultFilePath,
|
||||
"--config-files", options.systemdServiceFilePath,
|
||||
@ -334,7 +324,12 @@ func grunt(params ...string) {
|
||||
}
|
||||
|
||||
func gruntBuildArg(task string) []string {
|
||||
args := []string{task, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration)}
|
||||
var args []string
|
||||
if includeBuildNumber {
|
||||
args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
|
||||
} else {
|
||||
args = append(args, fmt.Sprintf("--pkgVer=%v", linuxPackageVersion))
|
||||
}
|
||||
if pkgArch != "" {
|
||||
args = append(args, fmt.Sprintf("--arch=%v", pkgArch))
|
||||
}
|
||||
@ -429,14 +424,10 @@ func setBuildEnv() {
|
||||
}
|
||||
|
||||
func getGitSha() string {
|
||||
v, err := runError("git", "describe", "--always", "--dirty")
|
||||
v, err := runError("git", "rev-parse", "--short", "HEAD")
|
||||
if err != nil {
|
||||
return "unknown-dev"
|
||||
}
|
||||
v = versionRe.ReplaceAllFunc(v, func(s []byte) []byte {
|
||||
s[0] = '+'
|
||||
return s
|
||||
})
|
||||
return string(v)
|
||||
}
|
||||
|
||||
@ -516,8 +507,15 @@ func md5File(file string) error {
|
||||
|
||||
func sha1FilesInDist() {
|
||||
filepath.Walk("./dist", func(path string, f os.FileInfo, err error) error {
|
||||
if path == "./dist" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(path, ".sha1") == false {
|
||||
sha1File(path)
|
||||
err := sha1File(path)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create sha file. error: %v\n", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
39
circle.yml
39
circle.yml
@ -1,18 +1,26 @@
|
||||
machine:
|
||||
node:
|
||||
version: 5.11.1
|
||||
version: 6.9.2
|
||||
python:
|
||||
version: 2.7.3
|
||||
services:
|
||||
- docker
|
||||
environment:
|
||||
GOPATH: "/home/ubuntu/.go_workspace"
|
||||
ORG_PATH: "github.com/grafana"
|
||||
REPO_PATH: "${ORG_PATH}/grafana"
|
||||
GODIST: "go1.7.4.linux-amd64.tar.gz"
|
||||
post:
|
||||
- mkdir -p download
|
||||
- mkdir -p ~/download
|
||||
- mkdir -p ~/docker
|
||||
- test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST
|
||||
- sudo rm -rf /usr/local/go
|
||||
- sudo tar -C /usr/local -xzf download/$GODIST
|
||||
|
||||
dependencies:
|
||||
cache_directories:
|
||||
- "~/docker"
|
||||
- "~/download"
|
||||
override:
|
||||
- rm -rf ${GOPATH}/src/${REPO_PATH}
|
||||
- mkdir -p ${GOPATH}/src/${ORG_PATH}
|
||||
@ -23,9 +31,26 @@ test:
|
||||
- bash scripts/circle-test.sh
|
||||
|
||||
deployment:
|
||||
master:
|
||||
branch: master
|
||||
owner: grafana
|
||||
gh_branch:
|
||||
branch: new_master
|
||||
commands:
|
||||
- ./scripts/trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
|
||||
- ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN}
|
||||
- pip install awscli
|
||||
- sudo apt-get update; sudo apt-get install rpm; sudo apt-get install expect
|
||||
- ./scripts/build/build_container.sh
|
||||
- ./scripts/build/deploy.sh
|
||||
- ./scripts/build/sign_packages.sh
|
||||
- go run build.go sha1-dist
|
||||
- aws s3 sync ./dist s3://$BUCKET_NAME/master
|
||||
#- ./scripts/trigger_grafana_docker_build.sh ${TRIGGER_GRAFANA_DOCKER_CIRCLECI_TOKEN}
|
||||
gh_tag:
|
||||
tag: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
|
||||
commands:
|
||||
- pip install awscli
|
||||
- sudo apt-get update; sudo apt-get install rpm; sudo apt-get install expect
|
||||
- ./scripts/build/build_container.sh
|
||||
- ./scripts/build/deploy.sh
|
||||
- ./scripts/build/sign_packages.sh
|
||||
- go run build.go sha1-dist
|
||||
- aws s3 sync ./dist s3://$BUCKET_NAME/release
|
||||
#- ./scripts/trigger_grafana_docker_build.sh ${TRIGGER_GRAFANA_DOCKER_CIRCLECI_TOKEN}
|
||||
|
||||
|
@ -113,6 +113,12 @@ cookie_secure = false
|
||||
session_life_time = 86400
|
||||
gc_interval_time = 86400
|
||||
|
||||
#################################### Data proxy ###########################
|
||||
[dataproxy]
|
||||
|
||||
# This enables data proxy logging, default is false
|
||||
logging = false
|
||||
|
||||
#################################### Analytics ###########################
|
||||
[analytics]
|
||||
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
|
||||
@ -279,6 +285,7 @@ allow_sign_up = true
|
||||
enabled = false
|
||||
host = localhost:25
|
||||
user =
|
||||
# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
|
||||
password =
|
||||
cert_file =
|
||||
key_file =
|
||||
@ -395,7 +402,9 @@ global_session = -1
|
||||
|
||||
#################################### Alerting ############################
|
||||
[alerting]
|
||||
# Makes it possible to turn off alert rule execution.
|
||||
# Disable alerting engine & UI features
|
||||
enabled = true
|
||||
# Makes it possible to turn off alert rule execution but alerting UI is visible
|
||||
execute_alerts = true
|
||||
|
||||
#################################### Internal Grafana Metrics ############
|
||||
|
@ -19,6 +19,7 @@ ssl_skip_verify = false
|
||||
# Search user bind dn
|
||||
bind_dn = "cn=admin,dc=grafana,dc=org"
|
||||
# Search user bind password
|
||||
# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
|
||||
bind_password = 'grafana'
|
||||
|
||||
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
|
||||
|
@ -104,6 +104,13 @@
|
||||
# Session life time, default is 86400
|
||||
;session_life_time = 86400
|
||||
|
||||
#################################### Data proxy ###########################
|
||||
[dataproxy]
|
||||
|
||||
# This enables data proxy logging, default is false
|
||||
;logging = false
|
||||
|
||||
|
||||
#################################### Analytics ####################################
|
||||
[analytics]
|
||||
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
|
||||
@ -263,6 +270,7 @@
|
||||
;enabled = false
|
||||
;host = localhost:25
|
||||
;user =
|
||||
# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
|
||||
;password =
|
||||
;cert_file =
|
||||
;key_file =
|
||||
@ -342,9 +350,11 @@
|
||||
;enabled = false
|
||||
;path = /var/lib/grafana/dashboards
|
||||
|
||||
#################################### Alerting ######################################
|
||||
#################################### Alerting ############################
|
||||
[alerting]
|
||||
# Makes it possible to turn off alert rule execution.
|
||||
# Disable alerting engine & UI features
|
||||
;enabled = true
|
||||
# Makes it possible to turn off alert rule execution but alerting UI is visible
|
||||
;execute_alerts = true
|
||||
|
||||
#################################### Internal Grafana Metrics ##########################
|
||||
|
30
docs/sources/administration/cli.md
Normal file
30
docs/sources/administration/cli.md
Normal file
@ -0,0 +1,30 @@
|
||||
+++
|
||||
title = "Grafana CLI"
|
||||
description = "Guide to using grafana-cli"
|
||||
keywords = ["grafana", "cli", "grafana-cli", "command line interface"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
parent = "admin"
|
||||
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.
|
||||
|
||||
## Plugins
|
||||
|
||||
The CLI helps you install, upgrade and manage your plugins on the same machine it CLI is running. You can find more information about how to install and manage your plugins at the [plugin page] ({{< relref "/installation.md" >}})
|
||||
|
||||
## Admin
|
||||
|
||||
> This feature is only available in grafana 4.1 and above.
|
||||
|
||||
To show all admin commands:
|
||||
`grafana-cli admin`
|
||||
|
||||
### Reset admin password
|
||||
|
||||
You can reset the password for the admin user using the CLI.
|
||||
|
||||
`grafana-cli admin reset-admin-password ...`
|
@ -72,4 +72,9 @@ label_values(hostname)
|
||||
|
||||
You can also use raw queries & regular expressions to extract anything you might need.
|
||||
|
||||
### Using templated variables in queries
|
||||
|
||||
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}`.
|
||||
|
||||

|
54
docs/sources/features/datasources/testdata.md
Normal file
54
docs/sources/features/datasources/testdata.md
Normal file
@ -0,0 +1,54 @@
|
||||
+++
|
||||
title = "Grafana TestData"
|
||||
keywords = ["grafana", "dashboard", "documentation", "panels", "testdata"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Grafana TestData"
|
||||
parent = "datasources"
|
||||
weight = 2
|
||||
+++
|
||||
|
||||
|
||||
# Grafana TestData
|
||||
|
||||
> NOTE: This plugin is disable by default.
|
||||
|
||||
The purpose of this data sources is to make it easier to create fake data for any panel.
|
||||
Using `Grafana TestData` you can build your own time series and have any panel render it.
|
||||
This make is much easier to verify functionally since the data can be shared very
|
||||
|
||||
## Enable
|
||||
|
||||
`Grafana TestData` is not enabled by default. To enable it you have to go to `/plugins/testdata/edit` and click the enable button to enable it for each server.
|
||||
|
||||
## Create mock data.
|
||||
|
||||
Once `Grafana TestData` is enabled you use it as a datasource in the metric panel.
|
||||
|
||||

|
||||
|
||||
## Scenarios
|
||||
|
||||
You can now choose different scenario that you want rendered in the drop down menu. If you have scenarios that you think should be added, please add them to `` and submit a pull request.
|
||||
|
||||
## CSV
|
||||
|
||||
The comma separated values scenario is the most powerful one since it lets you create any kind of graph you like.
|
||||
Once you provided the numbers `Grafana TestData` will distribute them evenly based on the time range of your query.
|
||||
|
||||

|
||||
|
||||
|
||||
## Dashboards
|
||||
|
||||
`Grafana TestData` also contains some dashboards with example. `/plugins/testdata/edit`
|
||||
|
||||
### Commit updates to the dashboards
|
||||
|
||||
If you want to submit a change to one of the current dashboards bundled with `Grafana TestData` you have to update the revision property.
|
||||
Otherwise the dashboard will not be updated automatically for other Grafana users.
|
||||
|
||||
## Using test data in issues
|
||||
|
||||
If you post an issue on github regarding time series data or rendering of time series data we strongly advice you to use this data source to replicate the data.
|
||||
That makes it much easier for the developers to replicate and solve the issue you have.
|
@ -26,11 +26,12 @@ The singlestat panel has a normal query editor to allow you define your exact me
|
||||
3. `Values`: The Value fields let you set the function (min, max, average, current, total, first, delta, range) that your entire query is reduced into a single value with. You can also set the font size of the Value field and font-size (as a %) of the metric query that the Panel is configured with. This reduces the entire query into a single summary value that is displayed.
|
||||
* `min` - The smallest value in the series
|
||||
* `max` - The largest value in the series
|
||||
* `average` - The average of all the non-null values in the series
|
||||
* `avg` - The average of all the non-null values in the series
|
||||
* `current` - The last value in the series. If the series ends on null the previous value will be used.
|
||||
* `total` - The sum of all the non-null values in the series
|
||||
* `first` - The first value in the series
|
||||
* `delta` - The total incremental increase (of a counter) in the series. An attempt is made to account for counter resets, but this will only be accurate for single instance metrics. Used to show total counter increase in time series.
|
||||
* `diff` - The difference betwen 'current' (last value) and 'first'.
|
||||
* `range` - The difference between 'min' and 'max'. Useful the show the range of change for a gauge.
|
||||
4. `Postfixes`: The Postfix fields let you define a custom label and font-size (as a %) to appear *after* the value
|
||||
5. `Units`: Units are appended to the the Singlestat within the panel, and will respect the color and threshold settings for the value.
|
@ -7,7 +7,7 @@ type = "docs"
|
||||
name = "Version 3.1"
|
||||
identifier = "v3.1"
|
||||
parent = "whatsnew"
|
||||
weight = 1
|
||||
weight = 5
|
||||
+++
|
||||
|
||||
# What's New in Grafana v3.1
|
||||
|
@ -7,7 +7,7 @@ type = "docs"
|
||||
name = "Version 3.0"
|
||||
identifier = "v3.0"
|
||||
parent = "whatsnew"
|
||||
weight = 2
|
||||
weight = 6
|
||||
+++
|
||||
|
||||
# What's New in Grafana v3.0
|
||||
|
70
docs/sources/guides/whats-new-in-v4-1.md
Normal file
70
docs/sources/guides/whats-new-in-v4-1.md
Normal file
@ -0,0 +1,70 @@
|
||||
+++
|
||||
title = "What's New in Grafana v4.1"
|
||||
description = "Feature & improvement highlights for Grafana v4.1"
|
||||
keywords = ["grafana", "new", "documentation", "4.1.0"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Version 4.1"
|
||||
identifier = "v4.1"
|
||||
parent = "whatsnew"
|
||||
weight = -1
|
||||
+++
|
||||
|
||||
|
||||
## Whats new in Grafana v4.1
|
||||
- **Graph**: Support for shared tooltip on all graphs as you hover over one graph. [#1578](https://github.com/grafana/grafana/pull/1578), [#6274](https://github.com/grafana/grafana/pull/6274)
|
||||
- **Victorops**: Add VictorOps notification integration [#6411](https://github.com/grafana/grafana/issues/6411), thx [@ichekrygin](https://github.com/ichekrygin)
|
||||
- **Opsgenie**: Add OpsGenie notification integratiion [#6687](https://github.com/grafana/grafana/issues/6687), thx [@kylemcc](https://github.com/kylemcc)
|
||||
- **Cloudwatch**: Make it possible to specify access and secret key on the data source config page [#6697](https://github.com/grafana/grafana/issues/6697)
|
||||
- **Elasticsearch**: Added support for Elasticsearch 5.x [#5740](https://github.com/grafana/grafana/issues/5740), thx [@lpic10](https://github.com/lpic10)
|
||||
- **Panel**: Added help text for panels. [#4079](https://github.com/grafana/grafana/issues/4079), thx [@utkarshcmu](https://github.com/utkarshcmu)
|
||||
- [Full changelog](https://github.com/grafana/grafana/blob/master/CHANGELOG.md)
|
||||
|
||||
### Shared tooltip
|
||||
|
||||
{{< imgbox max-width="60%" img="/img/docs/v41/shared_tooltip.gif" caption="Shared tooltip" >}}
|
||||
|
||||
Showing the tooltip on all panels at the same time has been a long standing request in Grafana and we are really happy to finally be able to release it.
|
||||
You can enable/disable the shared tooltip from the dashboard settings menu or cycle between default, shared tooltip and shared crosshair by pressing `CTRL + O` or `CMD + O`.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
### Help text for panel
|
||||
|
||||
{{< imgbox max-width="60%" img="/img/docs/v41/helptext_for_panel_settings.png" caption="Hovering help text" >}}
|
||||
|
||||
You can set a help text in the general tab on any panel. The help text is using Markdown to enable better formating and linking to other sites that can provide more information.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
{{< imgbox max-width="60%" img="/img/docs/v41/helptext_hover.png" caption="Hovering help text" >}}
|
||||
|
||||
Panels with a help text available have a little indicator in the top left corner. You can show the help text by hovering the icon.
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
||||
### Easier Cloudwatch configuration
|
||||
|
||||
{{< imgbox max-width="60%" img="/img/docs/v41/cloudwatch_settings.png" caption="Cloudwatch configuration" >}}
|
||||
|
||||
In Grafana 4.1.0 you can configure your Cloudwatch data source with `access key` and `secret key` directly in the data source configuration page.
|
||||
This enables people to use the Cloudwatch data source without having access to the filesystem where Grafana is running.
|
||||
|
||||
Once the `access key` and `secret key` have been saved the user will no longer be able to view them.
|
||||
<div class="clearfix"></div>
|
||||
|
||||
## Upgrade & Breaking changes
|
||||
|
||||
Elasticsearch 1.x is no longer supported. Please upgrade to Elasticsearch 2.x or 5.x. Otherwise Grafana 4.1.0 contains no breaking changes.
|
||||
|
||||
## 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.1 download page](/download/4_1_0/) for download links & instructions.
|
||||
|
||||
## Thanks
|
||||
A big thanks to all the Grafana users who contribute by submitting PRs, bug reports & feedback!
|
@ -4,10 +4,10 @@ description = "Feature & improvement highlights for Grafana v4.0"
|
||||
keywords = ["grafana", "new", "documentation", "4.0"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Version 4.0 (Latest)"
|
||||
name = "Version 4.0"
|
||||
identifier = "v4.0"
|
||||
parent = "whatsnew"
|
||||
weight = -1
|
||||
weight = 4
|
||||
+++
|
||||
|
||||
# What's New in Grafana v4.0
|
||||
|
@ -143,6 +143,7 @@ with Grafana admin permission.
|
||||
"protocol":"http",
|
||||
"root_url":"%(protocol)s://%(domain)s:%(http_port)s/",
|
||||
"router_logging":"true",
|
||||
"data_proxy_logging":"true",
|
||||
"static_root_path":"public"
|
||||
},
|
||||
"session":{
|
||||
@ -275,3 +276,20 @@ Change password for specific user
|
||||
Content-Type: application/json
|
||||
|
||||
{message: "User deleted"}
|
||||
|
||||
## Pause all alerts
|
||||
|
||||
`DELETE /api/admin/pause-all-alerts`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
DELETE /api/admin/pause-all-alerts HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100}
|
||||
|
216
docs/sources/http_api/alerting.md
Normal file
216
docs/sources/http_api/alerting.md
Normal file
@ -0,0 +1,216 @@
|
||||
+++
|
||||
title = "Alerting HTTP API "
|
||||
description = "Grafana Alerting HTTP API"
|
||||
keywords = ["grafana", "http", "documentation", "api", "alerting"]
|
||||
aliases = ["/http_api/alerting/"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Alerting"
|
||||
parent = "http_api"
|
||||
+++
|
||||
|
||||
|
||||
# Alerting API
|
||||
|
||||
You can use the Alerting API to get information about alerts and their states but this API cannot be used to modify the alert.
|
||||
To create new alerts or modify them you need to update the dashboard json that contains the alerts.
|
||||
|
||||
This API can also be used to create, update and delete alert notifications.
|
||||
|
||||
## Get alerts
|
||||
|
||||
`GET /api/alerts/`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
GET /api/alerts HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"dashboardId": 1,
|
||||
"panelId": 1,
|
||||
"name": "fire place sensor",
|
||||
"message": "Someone is trying to break in through the fire place",
|
||||
"state": "alerting",
|
||||
"newStateDate": "2016-12-25",
|
||||
"executionError": "",
|
||||
"dashboardUri": "http://grafana.com/dashboard/db/sensors"
|
||||
}
|
||||
]
|
||||
|
||||
## Get one alert
|
||||
|
||||
`GET /api/alerts/:id`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
GET /api/alerts/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"id": 1,
|
||||
"dashboardId": 1,
|
||||
"panelId": 1,
|
||||
"name": "fire place sensor",
|
||||
"message": "Someone is trying to break in through the fire place",
|
||||
"state": "alerting",
|
||||
"newStateDate": "2016-12-25",
|
||||
"executionError": "",
|
||||
"dashboardUri": "http://grafana.com/dashboard/db/sensors"
|
||||
}
|
||||
|
||||
|
||||
## Pause alert
|
||||
|
||||
`POST /api/alerts/:id/pause`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
POST /api/alerts/1/pause HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"alertId": 1,
|
||||
"paused: true
|
||||
}
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"alertId": 1,
|
||||
"state": "Paused",
|
||||
"message": "alert paused"
|
||||
}
|
||||
|
||||
## Get alert notifications
|
||||
|
||||
`GET /api/alert-notifications`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
GET /api/alert-notifications HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Team A",
|
||||
"type": "email",
|
||||
"isDefault": true,
|
||||
"created": "2017-01-01 12:45",
|
||||
"updated": "2017-01-01 12:45"
|
||||
}
|
||||
|
||||
## Create alert notification
|
||||
|
||||
`POST /api/alerts-notifications`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
POST /api/alerts-notifications HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"name": "new alert notification", //Required
|
||||
"type": "email", //Required
|
||||
"isDefault": false,
|
||||
"settings": {
|
||||
"addresses": "carl@grafana.com;dev@grafana.com"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "new alert notification",
|
||||
"type": "email",
|
||||
"isDefault": false,
|
||||
"settings": { addresses: "carl@grafana.com;dev@grafana.com"} }
|
||||
"created": "2017-01-01 12:34",
|
||||
"updated": "2017-01-01 12:34"
|
||||
}
|
||||
|
||||
## Update alert notification
|
||||
|
||||
`PUT /api/alerts-notifications/1`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
PUT /api/alerts-notifications/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "new alert notification", //Required
|
||||
"type": "email", //Required
|
||||
"isDefault": false,
|
||||
"settings": {
|
||||
"addresses: "carl@grafana.com;dev@grafana.com"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "new alert notification",
|
||||
"type": "email",
|
||||
"isDefault": false,
|
||||
"settings": { addresses: "carl@grafana.com;dev@grafana.com"} }
|
||||
"created": "2017-01-01 12:34",
|
||||
"updated": "2017-01-01 12:34"
|
||||
}
|
||||
|
||||
## Delete alert notification
|
||||
|
||||
`DELETE /api/alerts-notifications/:notificationId`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
DELETE /api/alerts-notifications/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
"message": "Notification deleted"
|
||||
}
|
@ -200,7 +200,7 @@ Get all tags of dashboards
|
||||
|
||||
**Example Request**:
|
||||
|
||||
GET /api/dashboards/home HTTP/1.1
|
||||
GET /api/dashboards/tags HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
@ -158,7 +158,7 @@ parent = "http_api"
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"id":1,"message":"Datasource added"}
|
||||
{"id":1,"message":"Datasource added", "name": "test_datasource"}
|
||||
|
||||
## Update an existing data source
|
||||
|
||||
@ -193,7 +193,7 @@ parent = "http_api"
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Datasource updated"}
|
||||
{"message":"Datasource updated", "id": 1, "name": "test_datasource"}
|
||||
|
||||
## Delete an existing data source
|
||||
|
||||
|
@ -69,6 +69,40 @@ parent = "http_api"
|
||||
"isGrafanaAdmin": true
|
||||
}
|
||||
|
||||
## Get single user by Username(login) or Email
|
||||
|
||||
`GET /api/users/lookup`
|
||||
|
||||
**Parameter:** `loginOrEmail`
|
||||
|
||||
**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
|
||||
|
||||
**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**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "user@mygraf.com"
|
||||
"name": "admin",
|
||||
"login": "admin",
|
||||
"theme": "light",
|
||||
"orgId": 1,
|
||||
"isGrafanaAdmin": true
|
||||
}
|
||||
|
||||
|
||||
## User Update
|
||||
|
||||
`PUT /api/users/:id`
|
||||
|
@ -144,6 +144,10 @@ Grafana needs a database to store users and dashboards (and other
|
||||
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`
|
||||
|
||||
### type
|
||||
|
||||
Either `mysql`, `postgres` or `sqlite3`, it's your choice.
|
||||
@ -244,7 +248,10 @@ organization to be created for that new user.
|
||||
|
||||
The role new users will be assigned for the main organization (if the
|
||||
above setting is set to true). Defaults to `Viewer`, other valid
|
||||
options are `Admin` and `Editor` and `Read-Only Editor`.
|
||||
options are `Admin` and `Editor` and `Read Only Editor`. e.g. :
|
||||
|
||||
`auto_assign_org_role = Read Only Editor`
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
@ -611,6 +618,11 @@ basic auth password
|
||||
|
||||
## [alerting]
|
||||
|
||||
### enabled
|
||||
Defaults to true. Set to false to disable alerting engine and hide Alerting from UI.
|
||||
|
||||
### execute_alerts
|
||||
|
||||
### execute_alerts = true
|
||||
|
||||
Makes it possible to turn off alert rule execution.
|
||||
|
@ -15,14 +15,14 @@ weight = 1
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Stable for Debian-based Linux | [4.0.2 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.2-1481203731_amd64.deb)
|
||||
Stable for Debian-based Linux | [4.1.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.1-1484211277_amd64.deb)
|
||||
|
||||
## Install Stable
|
||||
|
||||
```
|
||||
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.2-1481203731_amd64.deb
|
||||
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.1.1-1484211277_amd64.deb
|
||||
$ sudo apt-get install -y adduser libfontconfig
|
||||
$ sudo dpkg -i grafana_4.0.2-1481203731_amd64.deb
|
||||
$ sudo dpkg -i grafana_4.1.1-1484211277_amd64.deb
|
||||
```
|
||||
|
||||
## APT Repository
|
||||
|
@ -43,6 +43,7 @@ ssl_skip_verify = false
|
||||
# Search user bind dn
|
||||
bind_dn = "cn=admin,dc=grafana,dc=org"
|
||||
# Search user bind password
|
||||
# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
|
||||
bind_password = 'grafana'
|
||||
|
||||
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
|
||||
|
@ -15,24 +15,24 @@ weight = 2
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.0.2 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm)
|
||||
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)
|
||||
|
||||
## Install Stable
|
||||
|
||||
You can install Grafana using Yum directly.
|
||||
|
||||
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm
|
||||
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.1.1-1484211277.x86_64.rpm
|
||||
|
||||
Or install manually using `rpm`.
|
||||
|
||||
#### On CentOS / Fedora / Redhat:
|
||||
|
||||
$ sudo yum install initscripts fontconfig
|
||||
$ sudo rpm -Uvh grafana-4.0.2-1481203731.x86_64.rpm
|
||||
$ sudo rpm -Uvh grafana-4.1.1-1484211277.x86_64.rpm
|
||||
|
||||
#### On OpenSuse:
|
||||
|
||||
$ sudo rpm -i --nodeps grafana-4.0.2-1481203731.x86_64.rpm
|
||||
$ sudo rpm -i --nodeps grafana-4.1.1-1484211277.x86_64.rpm
|
||||
|
||||
## Install via YUM Repository
|
||||
|
||||
|
@ -13,7 +13,7 @@ weight = 3
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Latest stable package for Windows | [grafana.4.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2.windows-x64.zip)
|
||||
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)
|
||||
|
||||
## Configure
|
||||
|
||||
|
@ -37,7 +37,7 @@ 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
|
||||
annotationsQuery(options) // used by dashboards to get annotations
|
||||
annotationQuery(options) // used by dashboards to get annotations
|
||||
metricFindQuery(options) // used by query editor to get metric suggestions.
|
||||
```
|
||||
|
||||
@ -119,7 +119,7 @@ An array of
|
||||
|
||||
### Annotation Query
|
||||
|
||||
Request object passed to datasource.annotationsQuery function
|
||||
Request object passed to datasource.annotationQuery function
|
||||
```json
|
||||
{
|
||||
"range": { "from": "2016-03-04T04:07:55.144Z", "to": "2016-03-04T07:07:55.144Z" },
|
||||
@ -172,4 +172,4 @@ Requires a static template or templateUrl variable which will be rendered as the
|
||||
|
||||
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 annotationsQuery 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 is then sent to the Database objects annotationQuery function.
|
||||
|
@ -23,6 +23,8 @@ export GOPATH=`pwd`
|
||||
go get github.com/grafana/grafana
|
||||
```
|
||||
|
||||
You may see an error such as: `package github.com/grafana/grafana: no buildable Go source files`. This is just a warning, and you can proceed with the directions.
|
||||
|
||||
## Building the backend
|
||||
```
|
||||
cd $GOPATH/src/github.com/grafana/grafana
|
||||
@ -40,7 +42,8 @@ To build less to css for the frontend you will need a recent version of node (v0
|
||||
npm (v2.5.0) and grunt (v0.4.5). Run the following:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm install -g yarn
|
||||
yarn install --pure-lockfile
|
||||
npm install -g grunt-cli
|
||||
grunt
|
||||
```
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "4.0.2",
|
||||
"testing": "4.0.2"
|
||||
"stable": "4.1.1",
|
||||
"testing": "4.1.1"
|
||||
}
|
||||
|
13
package.json
13
package.json
@ -4,7 +4,7 @@
|
||||
"company": "Coding Instinct AB"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "4.1.0-pre1",
|
||||
"version": "4.2.0-pre1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
@ -29,7 +29,6 @@
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-exec": "^1.0.1",
|
||||
"grunt-filerev": "^2.3.1",
|
||||
"grunt-git-describe": "~2.4.2",
|
||||
"grunt-karma": "~2.0.0",
|
||||
"grunt-ng-annotate": "^3.0.0",
|
||||
"grunt-notify": "^0.4.5",
|
||||
@ -42,13 +41,12 @@
|
||||
"karma": "1.3.0",
|
||||
"karma-chrome-launcher": "~2.0.0",
|
||||
"karma-coverage": "1.1.1",
|
||||
"karma-coveralls": "1.1.2",
|
||||
"karma-expect": "~1.1.3",
|
||||
"karma-mocha": "~1.3.0",
|
||||
"karma-phantomjs-launcher": "1.0.2",
|
||||
"load-grunt-tasks": "3.5.2",
|
||||
"mocha": "3.2.0",
|
||||
"phantomjs-prebuilt": "^2.1.13",
|
||||
"phantomjs-prebuilt": "^2.1.14",
|
||||
"reflect-metadata": "0.1.8",
|
||||
"rxjs": "^5.0.0-rc.5",
|
||||
"sass-lint": "^1.10.2",
|
||||
@ -60,9 +58,8 @@
|
||||
"npm": "2.14.x"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "grunt",
|
||||
"test": "grunt test",
|
||||
"coveralls": "grunt karma:coveralls && rm -rf ./coverage"
|
||||
"build": "./node_modules/grunt-cli/bin/grunt",
|
||||
"test": "./node_modules/grunt-cli/bin/grunt test"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@ -78,7 +75,7 @@
|
||||
"sinon": "1.17.6",
|
||||
"systemjs-builder": "^0.15.34",
|
||||
"tether": "^1.4.0",
|
||||
"tether-drop": "git://github.com/torkelo/drop",
|
||||
"tether-drop": "https://github.com/torkelo/drop",
|
||||
"tslint": "^4.0.2",
|
||||
"typescript": "^2.1.4",
|
||||
"virtual-scroll": "^1.1.1"
|
||||
|
@ -42,6 +42,12 @@ case "$1" in
|
||||
chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana
|
||||
chmod 755 /var/log/grafana /var/lib/grafana
|
||||
|
||||
# copy user config files
|
||||
if [ ! -f $CONF_FILE ]; then
|
||||
cp /usr/share/grafana/conf/sample.ini $CONF_FILE
|
||||
cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
|
||||
fi
|
||||
|
||||
# configuration files should not be modifiable by grafana user, as this can be a security issue
|
||||
chown -Rh root:$GRAFANA_GROUP /etc/grafana/*
|
||||
chmod 755 /etc/grafana
|
||||
|
@ -14,6 +14,6 @@ CONF_DIR=/etc/grafana
|
||||
|
||||
CONF_FILE=/etc/grafana/grafana.ini
|
||||
|
||||
RESTART_ON_UPGRADE=false
|
||||
RESTART_ON_UPGRADE=true
|
||||
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
|
@ -1,6 +1,6 @@
|
||||
#! /usr/bin/env bash
|
||||
deb_ver=4.0.2-1481203731
|
||||
rpm_ver=4.0.2-1481203731
|
||||
deb_ver=4.1.0-1484127817
|
||||
rpm_ver=4.1.0-1484127817
|
||||
|
||||
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
|
||||
|
||||
|
4
packaging/publish/publish_testing.sh
Normal file → Executable file
4
packaging/publish/publish_testing.sh
Normal file → Executable file
@ -1,6 +1,6 @@
|
||||
#! /usr/bin/env bash
|
||||
deb_ver=4.0.2-1481203731
|
||||
rpm_ver=4.0.2-1481203731
|
||||
deb_ver=4.1.0-1482230757beta1
|
||||
rpm_ver=4.1.0-1482230757beta1
|
||||
|
||||
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
|
||||
|
||||
|
@ -6,6 +6,7 @@ set -e
|
||||
|
||||
startGrafana() {
|
||||
if [ -x /bin/systemctl ] ; then
|
||||
/bin/systemctl daemon-reload
|
||||
/bin/systemctl start grafana-server.service
|
||||
elif [ -x /etc/init.d/grafana-server ] ; then
|
||||
/etc/init.d/grafana-server start
|
||||
@ -37,6 +38,12 @@ if [ $1 -eq 1 ] ; then
|
||||
-c "grafana user" grafana
|
||||
fi
|
||||
|
||||
# copy user config files
|
||||
if [ ! -f $CONF_FILE ]; then
|
||||
cp /usr/share/grafana/conf/sample.ini $CONF_FILE
|
||||
cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
|
||||
fi
|
||||
|
||||
# Set user permissions on /var/log/grafana, /var/lib/grafana
|
||||
mkdir -p /var/log/grafana /var/lib/grafana
|
||||
chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana
|
||||
|
@ -14,6 +14,6 @@ CONF_DIR=/etc/grafana
|
||||
|
||||
CONF_FILE=/etc/grafana/grafana.ini
|
||||
|
||||
RESTART_ON_UPGRADE=false
|
||||
RESTART_ON_UPGRADE=true
|
||||
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
|
@ -73,9 +73,9 @@ func GetAlerts(c *middleware.Context) Response {
|
||||
Name: alert.Name,
|
||||
Message: alert.Message,
|
||||
State: alert.State,
|
||||
EvalDate: alert.EvalDate,
|
||||
NewStateDate: alert.NewStateDate,
|
||||
ExecutionError: alert.ExecutionError,
|
||||
EvalData: alert.EvalData,
|
||||
})
|
||||
}
|
||||
|
||||
@ -121,10 +121,10 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
|
||||
}
|
||||
|
||||
res := backendCmd.Result
|
||||
|
||||
dtoRes := &dtos.AlertTestResult{
|
||||
Firing: res.Firing,
|
||||
ConditionEvals: res.ConditionEvals,
|
||||
State: res.Rule.State,
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
@ -173,6 +173,10 @@ func DelAlert(c *middleware.Context) Response {
|
||||
return Json(200, resp)
|
||||
}
|
||||
|
||||
func GetAlertNotifiers(c *middleware.Context) Response {
|
||||
return Json(200, alerting.GetNotifiers())
|
||||
}
|
||||
|
||||
func GetAlertNotifications(c *middleware.Context) Response {
|
||||
query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
|
||||
|
||||
|
@ -125,6 +125,8 @@ func (hs *HttpServer) registerRoutes() {
|
||||
r.Get("/", wrap(SearchUsers))
|
||||
r.Get("/:id", wrap(GetUserById))
|
||||
r.Get("/:id/orgs", wrap(GetUserOrgList))
|
||||
// query parameters /users/lookup?loginOrEmail=admin@example.com
|
||||
r.Get("/lookup", wrap(GetUserByLoginOrEmail))
|
||||
r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
|
||||
r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
|
||||
}, reqGrafanaAdmin)
|
||||
@ -261,6 +263,7 @@ func (hs *HttpServer) registerRoutes() {
|
||||
})
|
||||
|
||||
r.Get("/alert-notifications", wrap(GetAlertNotifications))
|
||||
r.Get("/alert-notifiers", wrap(GetAlertNotifiers))
|
||||
|
||||
r.Group("/alert-notifications", func() {
|
||||
r.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
|
||||
|
@ -17,7 +17,6 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@ -90,7 +89,7 @@ type cache struct {
|
||||
var awsCredentialCache map[string]cache = make(map[string]cache)
|
||||
var credentialCacheLock sync.RWMutex
|
||||
|
||||
func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
|
||||
func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
|
||||
cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
|
||||
credentialCacheLock.RLock()
|
||||
if _, ok := awsCredentialCache[cacheKey]; ok {
|
||||
@ -98,7 +97,7 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
|
||||
(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
|
||||
result := awsCredentialCache[cacheKey].credential
|
||||
credentialCacheLock.RUnlock()
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
credentialCacheLock.RUnlock()
|
||||
@ -130,8 +129,7 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
|
||||
svc := sts.New(session.New(stsConfig), stsConfig)
|
||||
resp, err := svc.AssumeRole(params)
|
||||
if err != nil {
|
||||
// ignore
|
||||
log.Error(3, "CloudWatch: Failed to assume role", err)
|
||||
return nil, err
|
||||
}
|
||||
if resp.Credentials != nil {
|
||||
accessKeyId = *resp.Credentials.AccessKeyId
|
||||
@ -165,19 +163,28 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
|
||||
}
|
||||
credentialCacheLock.Unlock()
|
||||
|
||||
return creds
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
func getAwsConfig(req *cwRequest) *aws.Config {
|
||||
func getAwsConfig(req *cwRequest) (*aws.Config, error) {
|
||||
creds, err := getCredentials(req.GetDatasourceInfo())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &aws.Config{
|
||||
Region: aws.String(req.Region),
|
||||
Credentials: getCredentials(req.GetDatasourceInfo()),
|
||||
Credentials: creds,
|
||||
}
|
||||
return cfg
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@ -220,7 +227,11 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@ -239,7 +250,7 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
var resp cloudwatch.ListMetricsOutput
|
||||
err := svc.ListMetricsPages(params,
|
||||
err = svc.ListMetricsPages(params,
|
||||
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
|
||||
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
|
||||
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
|
||||
@ -257,7 +268,11 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@ -296,7 +311,11 @@ func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@ -336,7 +355,11 @@ func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@ -368,7 +391,11 @@ func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
|
||||
cfg := getAwsConfig(req)
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := ec2.New(session.New(cfg), cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
@ -388,7 +415,7 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
var resp ec2.DescribeInstancesOutput
|
||||
err := svc.DescribeInstancesPages(params,
|
||||
err = svc.DescribeInstancesPages(params,
|
||||
func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
|
||||
reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
|
||||
for _, reservation := range reservations {
|
||||
|
@ -111,7 +111,7 @@ func init() {
|
||||
"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
|
||||
"AWS/ES": {"ClientId", "DomainName"},
|
||||
"AWS/Events": {"RuleName"},
|
||||
"AWS/Firehose": {},
|
||||
"AWS/Firehose": {"DeliveryStreamName"},
|
||||
"AWS/IoT": {"Protocol"},
|
||||
"AWS/Kinesis": {"StreamName", "ShardID"},
|
||||
"AWS/KinesisAnalytics": {"Flow", "Id", "Application"},
|
||||
@ -140,8 +140,8 @@ func init() {
|
||||
// Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
|
||||
func handleGetRegions(req *cwRequest, c *middleware.Context) {
|
||||
regions := []string{
|
||||
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "cn-north-1",
|
||||
"eu-central-1", "eu-west-1", "sa-east-1", "us-east-1", "us-west-1", "us-west-2", "us-gov-west-1",
|
||||
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1",
|
||||
"eu-central-1", "eu-west-1", "eu-west-2", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2",
|
||||
}
|
||||
|
||||
result := []interface{}{}
|
||||
@ -248,9 +248,13 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
|
||||
}
|
||||
|
||||
func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
|
||||
creds, err := getCredentials(cwData)
|
||||
if err != nil {
|
||||
return cloudwatch.ListMetricsOutput{}, err
|
||||
}
|
||||
cfg := &aws.Config{
|
||||
Region: aws.String(cwData.Region),
|
||||
Credentials: getCredentials(cwData),
|
||||
Credentials: creds,
|
||||
}
|
||||
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
@ -260,7 +264,7 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
|
||||
}
|
||||
|
||||
var resp cloudwatch.ListMetricsOutput
|
||||
err := svc.ListMetricsPages(params,
|
||||
err = svc.ListMetricsPages(params,
|
||||
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
|
||||
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
|
||||
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
|
||||
|
@ -1,6 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@ -8,6 +10,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/cloudwatch"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@ -15,6 +18,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var (
|
||||
dataproxyLogger log.Logger = log.New("data-proxy-log")
|
||||
)
|
||||
|
||||
func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = targetUrl.Scheme
|
||||
@ -121,6 +128,32 @@ func ProxyDataSourceRequest(c *middleware.Context) {
|
||||
c.JsonApiErr(400, "Unable to load TLS certificate", err)
|
||||
return
|
||||
}
|
||||
|
||||
logProxyRequest(ds.Type, c)
|
||||
proxy.ServeHTTP(c.Resp, c.Req.Request)
|
||||
c.Resp.Header().Del("Set-Cookie")
|
||||
}
|
||||
|
||||
func logProxyRequest(dataSourceType string, c *middleware.Context) {
|
||||
if !setting.DataProxyLogging {
|
||||
return
|
||||
}
|
||||
|
||||
var body string
|
||||
if c.Req.Request.Body != nil {
|
||||
buffer, err := ioutil.ReadAll(c.Req.Request.Body)
|
||||
if err == nil {
|
||||
c.Req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buffer))
|
||||
body = string(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
dataproxyLogger.Info("Proxying incoming request",
|
||||
"userid", c.UserId,
|
||||
"orgid", c.OrgId,
|
||||
"username", c.Login,
|
||||
"datasource", dataSourceType,
|
||||
"uri", c.Req.RequestURI,
|
||||
"method", c.Req.Request.Method,
|
||||
"body", body)
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id})
|
||||
c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id, "name": cmd.Result.Name})
|
||||
}
|
||||
|
||||
func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Response {
|
||||
@ -117,7 +117,7 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp
|
||||
return ApiError(500, "Failed to update datasource", err)
|
||||
}
|
||||
|
||||
return Json(200, util.DynMap{"message": "Datasource updated"})
|
||||
return Json(200, util.DynMap{"message": "Datasource updated", "id": cmd.Id, "name": cmd.Name})
|
||||
}
|
||||
|
||||
func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
|
||||
|
@ -3,6 +3,7 @@ package dtos
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
@ -16,6 +17,7 @@ type AlertRule struct {
|
||||
State m.AlertStateType `json:"state"`
|
||||
NewStateDate time.Time `json:"newStateDate"`
|
||||
EvalDate time.Time `json:"evalDate"`
|
||||
EvalData *simplejson.Json `json:"evalData"`
|
||||
ExecutionError string `json:"executionError"`
|
||||
DashbboardUri string `json:"dashboardUri"`
|
||||
}
|
||||
@ -36,6 +38,7 @@ type AlertTestCommand struct {
|
||||
|
||||
type AlertTestResult struct {
|
||||
Firing bool `json:"firing"`
|
||||
State m.AlertStateType `json:"state"`
|
||||
ConditionEvals string `json:"conditionEvals"`
|
||||
TimeMs string `json:"timeMs"`
|
||||
Error string `json:"error,omitempty"`
|
||||
@ -51,7 +54,7 @@ type AlertTestResultLog struct {
|
||||
type EvalMatch struct {
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
Metric string `json:"metric"`
|
||||
Value float64 `json:"value"`
|
||||
Value null.Float `json:"value"`
|
||||
}
|
||||
|
||||
type NotificationTestCommand struct {
|
||||
|
@ -31,7 +31,7 @@ type AdminUpdateUserPasswordForm struct {
|
||||
}
|
||||
|
||||
type AdminUpdateUserPermissionsForm struct {
|
||||
IsGrafanaAdmin bool `json:"IsGrafanaAdmin"`
|
||||
IsGrafanaAdmin bool `json:"isGrafanaAdmin" binding:"Required"`
|
||||
}
|
||||
|
||||
type AdminUserListItem struct {
|
||||
|
@ -140,6 +140,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
|
||||
"authProxyEnabled": setting.AuthProxyEnabled,
|
||||
"ldapEnabled": setting.LdapEnabled,
|
||||
"alertingEnabled": setting.AlertingEnabled,
|
||||
"buildInfo": map[string]interface{}{
|
||||
"version": setting.BuildVersion,
|
||||
"commit": setting.BuildCommit,
|
||||
|
@ -103,10 +103,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
Children: dashboardChildNavs,
|
||||
})
|
||||
|
||||
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
|
||||
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
|
||||
alertChildNavs := []*dtos.NavLink{
|
||||
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"},
|
||||
{Text: "Notifications", Url: setting.AppSubUrl + "/alerting/notifications"},
|
||||
{Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications"},
|
||||
}
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
|
@ -14,11 +14,12 @@ func RenderToPng(c *middleware.Context) {
|
||||
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
|
||||
|
||||
renderOpts := &renderer.RenderOpts{
|
||||
Path: c.Params("*") + queryParams,
|
||||
Width: queryReader.Get("width", "800"),
|
||||
Height: queryReader.Get("height", "400"),
|
||||
OrgId: c.OrgId,
|
||||
Timeout: queryReader.Get("timeout", "30"),
|
||||
Path: c.Params("*") + queryParams,
|
||||
Width: queryReader.Get("width", "800"),
|
||||
Height: queryReader.Get("height", "400"),
|
||||
OrgId: c.OrgId,
|
||||
Timeout: queryReader.Get("timeout", "30"),
|
||||
Timezone: queryReader.Get("tz", ""),
|
||||
}
|
||||
|
||||
pngPath, err := renderer.RenderToPng(renderOpts)
|
||||
|
@ -13,7 +13,7 @@ func GetSignedInUser(c *middleware.Context) Response {
|
||||
return getUserUserProfile(c.UserId)
|
||||
}
|
||||
|
||||
// GET /api/user/:id
|
||||
// GET /api/users/:id
|
||||
func GetUserById(c *middleware.Context) Response {
|
||||
return getUserUserProfile(c.ParamsInt64(":id"))
|
||||
}
|
||||
@ -22,12 +22,36 @@ func getUserUserProfile(userId int64) Response {
|
||||
query := m.GetUserProfileQuery{UserId: userId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrUserNotFound {
|
||||
return ApiError(404, m.ErrUserNotFound.Error(), nil)
|
||||
}
|
||||
return ApiError(500, "Failed to get user", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
// GET /api/users/lookup
|
||||
func GetUserByLoginOrEmail(c *middleware.Context) Response {
|
||||
query := m.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrUserNotFound {
|
||||
return ApiError(404, m.ErrUserNotFound.Error(), nil)
|
||||
}
|
||||
return ApiError(500, "Failed to get user", err)
|
||||
}
|
||||
user := query.Result
|
||||
result := m.UserProfileDTO{
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Login: user.Login,
|
||||
Theme: user.Theme,
|
||||
IsGrafanaAdmin: user.IsAdmin,
|
||||
OrgId: user.OrgId,
|
||||
}
|
||||
return Json(200, &result)
|
||||
}
|
||||
|
||||
// POST /api/user
|
||||
func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
|
||||
if setting.AuthProxyEnabled {
|
||||
@ -60,7 +84,7 @@ func UpdateUserActiveOrg(c *middleware.Context) Response {
|
||||
cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed change active organization", err)
|
||||
return ApiError(500, "Failed to change active organization", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Active organization changed")
|
||||
@ -70,12 +94,12 @@ func handleUpdateUser(cmd m.UpdateUserCommand) Response {
|
||||
if len(cmd.Login) == 0 {
|
||||
cmd.Login = cmd.Email
|
||||
if len(cmd.Login) == 0 {
|
||||
return ApiError(400, "Validation error, need specify either username or email", nil)
|
||||
return ApiError(400, "Validation error, need to specify either username or email", nil)
|
||||
}
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "failed to update user", err)
|
||||
return ApiError(500, "Failed to update user", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("User updated")
|
||||
@ -95,7 +119,7 @@ func getUserOrgList(userId int64) Response {
|
||||
query := m.GetUserOrgListQuery{UserId: userId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Faile to get user organziations", err)
|
||||
return ApiError(500, "Failed to get user organizations", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
@ -130,7 +154,7 @@ func UserSetUsingOrg(c *middleware.Context) Response {
|
||||
cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed change active organization", err)
|
||||
return ApiError(500, "Failed to change active organization", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Active organization changed")
|
||||
|
@ -55,7 +55,7 @@ func (g *GrafanaServerImpl) Start() {
|
||||
plugins.Init()
|
||||
|
||||
// init alerting
|
||||
if setting.ExecuteAlerts {
|
||||
if setting.AlertingEnabled && setting.ExecuteAlerts {
|
||||
engine := alerting.NewEngine()
|
||||
g.childRoutines.Go(func() error { return engine.Run(g.context) })
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package imguploader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@ -30,19 +31,21 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
accessKey := s3sec.Key("access_key").MustString("")
|
||||
secretKey := s3sec.Key("secret_key").MustString("")
|
||||
|
||||
if bucket == "" {
|
||||
region := ""
|
||||
rBucket := regexp.MustCompile(`https?:\/\/(.*)\.s3(-([^.]+))?\.amazonaws\.com\/?`)
|
||||
matches := rBucket.FindStringSubmatch(bucket)
|
||||
if len(matches) == 0 {
|
||||
return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3")
|
||||
} else {
|
||||
bucket = matches[1]
|
||||
if matches[3] != "" {
|
||||
region = matches[3]
|
||||
} else {
|
||||
region = "us-east-1"
|
||||
}
|
||||
}
|
||||
|
||||
if accessKey == "" {
|
||||
return nil, fmt.Errorf("Could not find accessKey setting for image.uploader.s3")
|
||||
}
|
||||
|
||||
if secretKey == "" {
|
||||
return nil, fmt.Errorf("Could not find secretKey setting for image.uploader.s3")
|
||||
}
|
||||
|
||||
return NewS3Uploader(bucket, accessKey, secretKey), nil
|
||||
return NewS3Uploader(region, bucket, "public-read", accessKey, secretKey), nil
|
||||
case "webdav":
|
||||
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
|
||||
if err != nil {
|
||||
|
@ -19,7 +19,7 @@ func TestImageUploaderFactory(t *testing.T) {
|
||||
setting.ImageUploadProvider = "s3"
|
||||
|
||||
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
|
||||
s3sec.NewKey("bucket_url", "bucket_url")
|
||||
s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com")
|
||||
s3sec.NewKey("access_key", "access_key")
|
||||
s3sec.NewKey("secret_key", "secret_key")
|
||||
|
||||
@ -29,9 +29,10 @@ func TestImageUploaderFactory(t *testing.T) {
|
||||
original, ok := uploader.(*S3Uploader)
|
||||
|
||||
So(ok, ShouldBeTrue)
|
||||
So(original.region, ShouldEqual, "us-east-2")
|
||||
So(original.bucket, ShouldEqual, "foo.bar.baz")
|
||||
So(original.accessKey, ShouldEqual, "access_key")
|
||||
So(original.secretKey, ShouldEqual, "secret_key")
|
||||
So(original.bucket, ShouldEqual, "bucket_url")
|
||||
})
|
||||
|
||||
Convey("Webdav uploader", func() {
|
||||
|
@ -1,26 +1,33 @@
|
||||
package imguploader
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/kr/s3/s3util"
|
||||
)
|
||||
|
||||
type S3Uploader struct {
|
||||
region string
|
||||
bucket string
|
||||
acl string
|
||||
secretKey string
|
||||
accessKey string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
|
||||
func NewS3Uploader(region, bucket, acl, accessKey, secretKey string) *S3Uploader {
|
||||
return &S3Uploader{
|
||||
region: region,
|
||||
bucket: bucket,
|
||||
acl: acl,
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
log: log.New("s3uploader"),
|
||||
@ -28,42 +35,41 @@ func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
|
||||
}
|
||||
|
||||
func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
|
||||
|
||||
s3util.DefaultConfig.AccessKey = u.accessKey
|
||||
s3util.DefaultConfig.SecretKey = u.secretKey
|
||||
|
||||
header := make(http.Header)
|
||||
header.Add("x-amz-acl", "public-read")
|
||||
header.Add("Content-Type", "image/png")
|
||||
|
||||
var imageUrl *url.URL
|
||||
var err error
|
||||
|
||||
if imageUrl, err = url.Parse(u.bucket); err != nil {
|
||||
return "", err
|
||||
sess := session.New()
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
AccessKeyID: u.accessKey,
|
||||
SecretAccessKey: u.secretKey,
|
||||
}},
|
||||
&credentials.EnvProvider{},
|
||||
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
|
||||
})
|
||||
cfg := &aws.Config{
|
||||
Region: aws.String(u.region),
|
||||
Credentials: creds,
|
||||
}
|
||||
|
||||
// add image to url
|
||||
imageUrl.Path = path.Join(imageUrl.Path, util.GetRandomString(20)+".png")
|
||||
imageUrlString := imageUrl.String()
|
||||
log.Debug("Uploading image to s3", "url", imageUrlString)
|
||||
key := util.GetRandomString(20) + ".png"
|
||||
log.Debug("Uploading image to s3", "bucket = ", u.bucket, ", key = ", key)
|
||||
|
||||
writer, err := s3util.Create(imageUrlString, header, nil)
|
||||
file, err := os.Open(imageDiskPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer writer.Close()
|
||||
|
||||
imgData, err := ioutil.ReadFile(imageDiskPath)
|
||||
svc := s3.New(session.New(cfg), cfg)
|
||||
params := &s3.PutObjectInput{
|
||||
Bucket: aws.String(u.bucket),
|
||||
Key: aws.String(key),
|
||||
ACL: aws.String(u.acl),
|
||||
Body: file,
|
||||
ContentType: aws.String("image/png"),
|
||||
}
|
||||
_, err = svc.PutObject(params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = writer.Write(imgData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return imageUrlString, nil
|
||||
return "https://" + u.bucket + ".s3.amazonaws.com/" + key, nil
|
||||
}
|
||||
|
@ -96,6 +96,16 @@ func (f Float) MarshalText() ([]byte, error) {
|
||||
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
// It will encode a blank string if this Float is null.
|
||||
func (f Float) String() string {
|
||||
if !f.Valid {
|
||||
return "null"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%1.3f", f.Float64)
|
||||
}
|
||||
|
||||
// SetValid changes this Float's value and also sets it to be non-null.
|
||||
func (f *Float) SetValid(n float64) {
|
||||
f.Float64 = n
|
@ -11,6 +11,8 @@ import (
|
||||
|
||||
"strconv"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -18,15 +20,38 @@ import (
|
||||
)
|
||||
|
||||
type RenderOpts struct {
|
||||
Path string
|
||||
Width string
|
||||
Height string
|
||||
Timeout string
|
||||
OrgId int64
|
||||
Path string
|
||||
Width string
|
||||
Height string
|
||||
Timeout string
|
||||
OrgId int64
|
||||
Timezone string
|
||||
}
|
||||
|
||||
var rendererLog log.Logger = log.New("png-renderer")
|
||||
|
||||
func isoTimeOffsetToPosixTz(isoOffset string) string {
|
||||
// invert offset
|
||||
if strings.HasPrefix(isoOffset, "UTC+") {
|
||||
return strings.Replace(isoOffset, "UTC+", "UTC-", 1)
|
||||
}
|
||||
if strings.HasPrefix(isoOffset, "UTC-") {
|
||||
return strings.Replace(isoOffset, "UTC-", "UTC+", 1)
|
||||
}
|
||||
return isoOffset
|
||||
}
|
||||
|
||||
func appendEnviron(baseEnviron []string, name string, value string) []string {
|
||||
results := make([]string, 0)
|
||||
prefix := fmt.Sprintf("%s=", name)
|
||||
for _, v := range baseEnviron {
|
||||
if !strings.HasPrefix(v, prefix) {
|
||||
results = append(results, v)
|
||||
}
|
||||
}
|
||||
return append(results, fmt.Sprintf("%s=%s", name, value))
|
||||
}
|
||||
|
||||
func RenderToPng(params *RenderOpts) (string, error) {
|
||||
rendererLog.Info("Rendering", "path", params.Path)
|
||||
|
||||
@ -73,6 +98,11 @@ func RenderToPng(params *RenderOpts) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if params.Timezone != "" {
|
||||
baseEnviron := os.Environ()
|
||||
cmd.Env = appendEnviron(baseEnviron, "TZ", isoTimeOffsetToPosixTz(params.Timezone))
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -45,8 +45,11 @@ var (
|
||||
M_Alerting_Notification_Sent_Email Counter
|
||||
M_Alerting_Notification_Sent_Webhook Counter
|
||||
M_Alerting_Notification_Sent_PagerDuty Counter
|
||||
M_Alerting_Notification_Sent_LINE Counter
|
||||
M_Alerting_Notification_Sent_Victorops Counter
|
||||
M_Alerting_Notification_Sent_OpsGenie Counter
|
||||
M_Alerting_Notification_Sent_Telegram Counter
|
||||
M_Alerting_Notification_Sent_Sensu Counter
|
||||
M_Aws_CloudWatch_GetMetricStatistics Counter
|
||||
M_Aws_CloudWatch_ListMetrics Counter
|
||||
|
||||
@ -114,6 +117,9 @@ func initMetricVars(settings *MetricSettings) {
|
||||
M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty")
|
||||
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_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu")
|
||||
M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE")
|
||||
|
||||
M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
|
||||
M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")
|
||||
|
@ -73,7 +73,6 @@ type Alert struct {
|
||||
Frequency int64
|
||||
|
||||
EvalData *simplejson.Json
|
||||
EvalDate time.Time
|
||||
NewStateDate time.Time
|
||||
StateChanges int
|
||||
|
||||
|
@ -23,6 +23,7 @@ type SendWebhookSync struct {
|
||||
Password string
|
||||
Body string
|
||||
HttpMethod string
|
||||
HttpHeader map[string]string
|
||||
}
|
||||
|
||||
type SendResetPasswordEmailCommand struct {
|
||||
|
@ -3,9 +3,9 @@ package conditions
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -3,10 +3,10 @@ package conditions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/guregu/null.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
func evalutorScenario(json string, reducedValue float64, datapoints ...float64) bool {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
@ -45,18 +46,18 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
|
||||
emptySerieCount := 0
|
||||
evalMatchCount := 0
|
||||
var matches []*alerting.EvalMatch
|
||||
|
||||
for _, series := range seriesList {
|
||||
reducedValue := c.Reducer.Reduce(series)
|
||||
evalMatch := c.Evaluator.Eval(reducedValue)
|
||||
|
||||
if reducedValue.Valid == false {
|
||||
emptySerieCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if context.IsTestRun {
|
||||
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
|
||||
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue.Float64),
|
||||
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %s", c.Index, evalMatch, series.Name, reducedValue),
|
||||
})
|
||||
}
|
||||
|
||||
@ -65,11 +66,28 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
|
||||
|
||||
matches = append(matches, &alerting.EvalMatch{
|
||||
Metric: series.Name,
|
||||
Value: reducedValue.Float64,
|
||||
Value: reducedValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// handle no series special case
|
||||
if len(seriesList) == 0 {
|
||||
// eval condition for null value
|
||||
evalMatch := c.Evaluator.Eval(null.FloatFromPtr(nil))
|
||||
|
||||
if context.IsTestRun {
|
||||
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
|
||||
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Query Returned No Series (reduced to null/no value)", evalMatch),
|
||||
})
|
||||
}
|
||||
|
||||
if evalMatch {
|
||||
evalMatchCount++
|
||||
matches = append(matches, &alerting.EvalMatch{Metric: "NoData", Value: null.FloatFromPtr(nil)})
|
||||
}
|
||||
}
|
||||
|
||||
return &alerting.ConditionResult{
|
||||
Firing: evalMatchCount > 0,
|
||||
NoDataFound: emptySerieCount == len(seriesList),
|
||||
|
@ -4,9 +4,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
null "gopkg.in/guregu/null.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
@ -72,7 +71,38 @@ func TestQueryCondition(t *testing.T) {
|
||||
So(cr.Firing, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("No series", func() {
|
||||
Convey("Should set NoDataFound when condition is gt", func() {
|
||||
ctx.series = tsdb.TimeSeriesSlice{}
|
||||
cr, err := ctx.exec()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.Firing, ShouldBeFalse)
|
||||
So(cr.NoDataFound, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Should be firing when condition is no_value", func() {
|
||||
ctx.evaluator = `{"type": "no_value", "params": []}`
|
||||
ctx.series = tsdb.TimeSeriesSlice{}
|
||||
cr, err := ctx.exec()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.Firing, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Empty series", func() {
|
||||
Convey("Should set Firing if eval match", func() {
|
||||
ctx.evaluator = `{"type": "no_value", "params": []}`
|
||||
ctx.series = tsdb.TimeSeriesSlice{
|
||||
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
|
||||
}
|
||||
cr, err := ctx.exec()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(cr.Firing, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Should set NoDataFound both series are empty", func() {
|
||||
ctx.series = tsdb.TimeSeriesSlice{
|
||||
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
|
||||
|
@ -5,8 +5,8 @@ import (
|
||||
|
||||
"sort"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
type QueryReducer interface {
|
||||
|
@ -3,10 +3,10 @@ package conditions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/guregu/null.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
func TestSimpleReducer(t *testing.T) {
|
||||
@ -57,6 +57,16 @@ func TestSimpleReducer(t *testing.T) {
|
||||
So(result, ShouldEqual, float64(2))
|
||||
})
|
||||
|
||||
Convey("avg with only nulls", func() {
|
||||
reducer := NewSimpleReducer("avg")
|
||||
series := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("avg of number values and null values should ignore nulls", func() {
|
||||
reducer := NewSimpleReducer("avg")
|
||||
series := &tsdb.TimeSeries{
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type DefaultEvalHandler struct {
|
||||
@ -60,6 +61,40 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
||||
context.Firing = firing
|
||||
context.NoDataFound = noDataFound
|
||||
context.EndTime = time.Now()
|
||||
context.Rule.State = e.getNewState(context)
|
||||
|
||||
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
|
||||
metrics.M_Alerting_Execution_Time.Update(elapsedTime)
|
||||
}
|
||||
|
||||
// This should be move into evalContext once its been refactored.
|
||||
func (handler *DefaultEvalHandler) getNewState(evalContext *EvalContext) models.AlertStateType {
|
||||
if evalContext.Error != nil {
|
||||
handler.log.Error("Alert Rule Result Error",
|
||||
"ruleId", evalContext.Rule.Id,
|
||||
"name", evalContext.Rule.Name,
|
||||
"error", evalContext.Error,
|
||||
"changing state to", evalContext.Rule.ExecutionErrorState.ToAlertState())
|
||||
|
||||
if evalContext.Rule.ExecutionErrorState == models.ExecutionErrorKeepState {
|
||||
return evalContext.PrevAlertState
|
||||
} else {
|
||||
return evalContext.Rule.ExecutionErrorState.ToAlertState()
|
||||
}
|
||||
} else if evalContext.Firing {
|
||||
return models.AlertStateAlerting
|
||||
} else if evalContext.NoDataFound {
|
||||
handler.log.Info("Alert Rule returned no data",
|
||||
"ruleId", evalContext.Rule.Id,
|
||||
"name", evalContext.Rule.Name,
|
||||
"changing state to", evalContext.Rule.NoDataState.ToAlertState())
|
||||
|
||||
if evalContext.Rule.NoDataState == models.NoDataKeepState {
|
||||
return evalContext.PrevAlertState
|
||||
} else {
|
||||
return evalContext.Rule.NoDataState.ToAlertState()
|
||||
}
|
||||
}
|
||||
|
||||
return models.AlertStateOK
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
@ -18,8 +20,8 @@ func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
|
||||
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator, NoDataFound: c.noData}, nil
|
||||
}
|
||||
|
||||
func TestAlertingExecutor(t *testing.T) {
|
||||
Convey("Test alert execution", t, func() {
|
||||
func TestAlertingEvaluationHandler(t *testing.T) {
|
||||
Convey("Test alert evaluation handler", t, func() {
|
||||
handler := NewEvalHandler()
|
||||
|
||||
Convey("Show return triggered with single passing condition", func() {
|
||||
@ -37,7 +39,7 @@ func TestAlertingExecutor(t *testing.T) {
|
||||
Convey("Show return false with not passing asdf", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
|
||||
&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{{}, {}}},
|
||||
&conditionStub{firing: false, operator: "and"},
|
||||
},
|
||||
})
|
||||
@ -164,5 +166,73 @@ func TestAlertingExecutor(t *testing.T) {
|
||||
handler.Eval(context)
|
||||
So(context.NoDataFound, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("EvalHandler can replace alert state based for errors and no_data", func() {
|
||||
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
|
||||
dummieError := fmt.Errorf("dummie error")
|
||||
Convey("Should update alert state", func() {
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Firing = true
|
||||
|
||||
So(handler.getNewState(ctx), ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> error(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
})
|
||||
|
||||
Convey("pending -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
})
|
||||
|
||||
Convey("pending -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -60,12 +60,25 @@ func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Jso
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyJson(in *simplejson.Json) (*simplejson.Json, error) {
|
||||
rawJson, err := in.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return simplejson.NewJson(rawJson)
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
e.log.Debug("GetAlerts")
|
||||
|
||||
alerts := make([]*m.Alert, 0)
|
||||
dashboardJson, err := copyJson(e.Dash.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, rowObj := range e.Dash.Data.Get("rows").MustArray() {
|
||||
alerts := make([]*m.Alert, 0)
|
||||
for _, rowObj := range dashboardJson.Get("rows").MustArray() {
|
||||
row := simplejson.NewFromAny(rowObj)
|
||||
|
||||
for _, panelObj := range row.Get("panels").MustArray() {
|
||||
|
@ -110,6 +110,34 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
]
|
||||
}`
|
||||
|
||||
Convey("Extractor should not modify the original json", func() {
|
||||
dashJson, err := simplejson.NewJson([]byte(json))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
|
||||
getTarget := func(j *simplejson.Json) string {
|
||||
rowObj := j.Get("rows").MustArray()[0]
|
||||
row := simplejson.NewFromAny(rowObj)
|
||||
panelObj := row.Get("panels").MustArray()[0]
|
||||
panel := simplejson.NewFromAny(panelObj)
|
||||
conditionObj := panel.Get("alert").Get("conditions").MustArray()[0]
|
||||
condition := simplejson.NewFromAny(conditionObj)
|
||||
return condition.Get("query").Get("model").Get("target").MustString()
|
||||
}
|
||||
|
||||
Convey("Dashboard json rows.panels.alert.query.model.target should be empty", func() {
|
||||
So(getTarget(dashJson), ShouldEqual, "")
|
||||
})
|
||||
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
_, _ = extractor.GetAlerts()
|
||||
|
||||
Convey("Dashboard json should not be updated after extracting rules", func() {
|
||||
So(getTarget(dashJson), ShouldEqual, "")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Parsing and validating dashboard containing graphite alerts", func() {
|
||||
|
||||
dashJson, err := simplejson.NewJson([]byte(json))
|
||||
|
@ -1,5 +1,7 @@
|
||||
package alerting
|
||||
|
||||
import "github.com/grafana/grafana/pkg/components/null"
|
||||
|
||||
type Job struct {
|
||||
Offset int64
|
||||
OffsetWait bool
|
||||
@ -14,7 +16,7 @@ type ResultLogEntry struct {
|
||||
}
|
||||
|
||||
type EvalMatch struct {
|
||||
Value float64 `json:"value"`
|
||||
Value null.Float `json:"value"`
|
||||
Metric string `json:"metric"`
|
||||
Tags map[string]string `json:"tags"`
|
||||
}
|
||||
|
@ -13,6 +13,14 @@ import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type NotifierPlugin struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
OptionsTemplate string `json:"optionsTemplate"`
|
||||
Factory NotifierFactory `json:"-"`
|
||||
}
|
||||
|
||||
type RootNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
@ -130,12 +138,12 @@ func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, contex
|
||||
}
|
||||
|
||||
func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
|
||||
factory, found := notifierFactories[model.Type]
|
||||
notifierPlugin, found := notifierFactories[model.Type]
|
||||
if !found {
|
||||
return nil, errors.New("Unsupported notification type")
|
||||
}
|
||||
|
||||
return factory(model)
|
||||
return notifierPlugin.Factory(model)
|
||||
}
|
||||
|
||||
func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
|
||||
@ -152,8 +160,18 @@ func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
|
||||
|
||||
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
|
||||
|
||||
var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory)
|
||||
var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)
|
||||
|
||||
func RegisterNotifier(typeName string, factory NotifierFactory) {
|
||||
notifierFactories[typeName] = factory
|
||||
func RegisterNotifier(plugin *NotifierPlugin) {
|
||||
notifierFactories[plugin.Type] = plugin
|
||||
}
|
||||
|
||||
func GetNotifiers() []*NotifierPlugin {
|
||||
list := make([]*NotifierPlugin, 0)
|
||||
|
||||
for _, value := range notifierFactories {
|
||||
list = append(list, value)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
@ -13,7 +13,21 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier("email", NewEmailNotifier)
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "email",
|
||||
Name: "Email",
|
||||
Description: "Sends notifications using Grafana server configured STMP settings",
|
||||
Factory: NewEmailNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Email addresses</h3>
|
||||
<div class="gf-form">
|
||||
<textarea rows="7" class="gf-form-input width-25" required ng-model="ctrl.model.settings.addresses"></textarea>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span>You can enter multiple email addresses using a ";" separator</span>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
type EmailNotifier struct {
|
||||
|
94
pkg/services/alerting/notifiers/line.go
Normal file
94
pkg/services/alerting/notifiers/line.go
Normal file
@ -0,0 +1,94 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "LINE",
|
||||
Name: "LINE",
|
||||
Description: "Send notifications to LINE notify",
|
||||
Factory: NewLINENotifier,
|
||||
OptionsTemplate: `
|
||||
<div class="gf-form-group">
|
||||
<h3 class="page-heading">LINE notify settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Token</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.token" placeholder="LINE notify token key"></input>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
lineNotifyUrl string = "https://notify-api.line.me/api/notify"
|
||||
)
|
||||
|
||||
func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
token := model.Settings.Get("token").MustString()
|
||||
if token == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find token in settings"}
|
||||
}
|
||||
|
||||
return &LineNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Token: token,
|
||||
log: log.New("alerting.notifier.line"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type LineNotifier struct {
|
||||
NotifierBase
|
||||
Token string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
metrics.M_Alerting_Notification_Sent_LINE.Inc(1)
|
||||
|
||||
var err error
|
||||
switch evalContext.Rule.State {
|
||||
case m.AlertStateAlerting:
|
||||
err = this.createAlert(evalContext)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (this *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Creating Line notify", "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
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
body := fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message)
|
||||
form.Add("message", body)
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: lineNotifyUrl,
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", this.Token),
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
Body: form.Encode(),
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send notification to LINE", "error", err, "body", string(body))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
49
pkg/services/alerting/notifiers/line_test.go
Normal file
49
pkg/services/alerting/notifiers/line_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestLineNotifier(t *testing.T) {
|
||||
Convey("Line notifier tests", t, func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "line_testing",
|
||||
Type: "line",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewLINENotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
})
|
||||
Convey("settings should trigger incident", func() {
|
||||
json := `
|
||||
{
|
||||
"token": "abcdefgh0123456789"
|
||||
}`
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "line_testing",
|
||||
Type: "line",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewLINENotifier(model)
|
||||
lineNotifier := not.(*LineNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(lineNotifier.Name, ShouldEqual, "line_testing")
|
||||
So(lineNotifier.Type, ShouldEqual, "line")
|
||||
So(lineNotifier.Token, ShouldEqual, "abcdefgh0123456789")
|
||||
})
|
||||
|
||||
})
|
||||
}
|
@ -13,7 +13,28 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier("opsgenie", NewOpsGenieNotifier)
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "opsgenie",
|
||||
Name: "OpsGenie",
|
||||
Description: "Sends notifications to OpsGenie",
|
||||
Factory: NewOpsGenieNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">OpsGenie settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">API Key</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.apiKey" placeholder="OpsGenie API Key"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Auto close incidents"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.settings.autoClose"
|
||||
tooltip="Automatically close alerts in OpseGenie once the alert goes back to ok.">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -12,7 +12,28 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier("pagerduty", NewPagerdutyNotifier)
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "pagerduty",
|
||||
Name: "PagerDuty",
|
||||
Description: "Sends notifications to PagerDuty",
|
||||
Factory: NewPagerdutyNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">PagerDuty settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Integration Key</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.integrationKey" placeholder="Pagerduty integeration Key"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Auto resolve incidents"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.settings.autoResolve"
|
||||
tooltip="Resolve incidents in pagerduty once the alert goes back to ok.">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
|
115
pkg/services/alerting/notifiers/sensu.go
Normal file
115
pkg/services/alerting/notifiers/sensu.go
Normal file
@ -0,0 +1,115 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "sensu",
|
||||
Name: "Sensu",
|
||||
Description: "Sends HTTP POST request to a Sensu API",
|
||||
Factory: NewSensuNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Sensu settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url" placeholder="http://sensu-api.local:4567/results"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Username</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.username"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Password</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.password"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
url := model.Settings.Get("url").MustString()
|
||||
if url == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
|
||||
}
|
||||
|
||||
return &SensuNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
User: model.Settings.Get("username").MustString(),
|
||||
Password: model.Settings.Get("password").MustString(),
|
||||
log: log.New("alerting.notifier.sensu"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type SensuNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
User string
|
||||
Password string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending sensu result")
|
||||
metrics.M_Alerting_Notification_Sent_Sensu.Inc(1)
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("ruleId", evalContext.Rule.Id)
|
||||
// Sensu alerts cannot have spaces in them
|
||||
bodyJSON.Set("name", strings.Replace(evalContext.Rule.Name, " ", "_", -1))
|
||||
// Sensu alerts require a command
|
||||
// We set it to the grafana ruleID
|
||||
bodyJSON.Set("source", "grafana_rule_"+strconv.FormatInt(evalContext.Rule.Id, 10))
|
||||
// Finally, sensu expects an output
|
||||
// We set it to a default output
|
||||
bodyJSON.Set("output", "Grafana Metric Condition Met")
|
||||
bodyJSON.Set("evalMatches", evalContext.EvalMatches)
|
||||
|
||||
if evalContext.Rule.State == "alerting" {
|
||||
bodyJSON.Set("status", 2)
|
||||
} else if evalContext.Rule.State == "no_data" {
|
||||
bodyJSON.Set("status", 1)
|
||||
} else {
|
||||
bodyJSON.Set("status", 0)
|
||||
}
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err == nil {
|
||||
bodyJSON.Set("ruleUrl", ruleUrl)
|
||||
}
|
||||
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
bodyJSON.Set("imageUrl", evalContext.ImagePublicUrl)
|
||||
}
|
||||
|
||||
if evalContext.Rule.Message != "" {
|
||||
bodyJSON.Set("message", evalContext.Rule.Message)
|
||||
}
|
||||
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: this.Url,
|
||||
User: this.User,
|
||||
Password: this.Password,
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send sensu event", "error", err, "sensu", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
52
pkg/services/alerting/notifiers/sensu_test.go
Normal file
52
pkg/services/alerting/notifiers/sensu_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestSensuNotifier(t *testing.T) {
|
||||
Convey("Sensu 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: "sensu",
|
||||
Type: "sensu",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewSensuNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("from settings", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "http://sensu-api.example.com:4567/results"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "sensu",
|
||||
Type: "sensu",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewSensuNotifier(model)
|
||||
sensuNotifier := not.(*SensuNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(sensuNotifier.Name, ShouldEqual, "sensu")
|
||||
So(sensuNotifier.Type, ShouldEqual, "sensu")
|
||||
So(sensuNotifier.Url, ShouldEqual, "http://sensu-api.example.com:4567/results")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@ -13,7 +13,42 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier("slack", NewSlackNotifier)
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "slack",
|
||||
Name: "Slack",
|
||||
Description: "Sends notifications using Grafana server configured STMP settings",
|
||||
Factory: NewSlackNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Slack settings</h3>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-6">Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Slack incoming webhook url"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-6">Recipient</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.recipient"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Override default channel or user, use #channel-name or @username
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-6">Mention</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.mention"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Mention a user or a group using @ when notifying in a channel
|
||||
</info-popover>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
|
113
pkg/services/alerting/notifiers/telegram.go
Normal file
113
pkg/services/alerting/notifiers/telegram.go
Normal file
@ -0,0 +1,113 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
var (
|
||||
telegeramApiUrl string = "https://api.telegram.org/bot%s/%s"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "telegram",
|
||||
Name: "Telegram",
|
||||
Description: "Sends notifications to Telegram",
|
||||
Factory: NewTelegramNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Telegram API settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">BOT API Token</span>
|
||||
<input type="text" required
|
||||
class="gf-form-input"
|
||||
ng-model="ctrl.model.settings.bottoken"
|
||||
placeholder="Telegram BOT API Token"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Chat ID</span>
|
||||
<input type="text" required
|
||||
class="gf-form-input"
|
||||
ng-model="ctrl.model.settings.chatid"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Integer Telegram Chat Identifier
|
||||
</info-popover>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type TelegramNotifier struct {
|
||||
NotifierBase
|
||||
BotToken string
|
||||
ChatID string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
if model.Settings == nil {
|
||||
return nil, alerting.ValidationError{Reason: "No Settings Supplied"}
|
||||
}
|
||||
|
||||
botToken := model.Settings.Get("bottoken").MustString()
|
||||
chatId := model.Settings.Get("chatid").MustString()
|
||||
|
||||
if botToken == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find Bot Token in settings"}
|
||||
}
|
||||
|
||||
if chatId == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find Chat Id in settings"}
|
||||
}
|
||||
|
||||
return &TelegramNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
BotToken: botToken,
|
||||
ChatID: chatId,
|
||||
log: log.New("alerting.notifier.telegram"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending alert notification to", "bot_token", this.BotToken)
|
||||
this.log.Info("Sending alert notification to", "chat_id", this.ChatID)
|
||||
metrics.M_Alerting_Notification_Sent_Telegram.Inc(1)
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
|
||||
bodyJSON.Set("chat_id", this.ChatID)
|
||||
bodyJSON.Set("parse_mode", "html")
|
||||
|
||||
message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err == nil {
|
||||
message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
|
||||
}
|
||||
bodyJSON.Set("text", message)
|
||||
|
||||
url := fmt.Sprintf(telegeramApiUrl, this.BotToken, "sendMessage")
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: url,
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
55
pkg/services/alerting/notifiers/telegram_test.go
Normal file
55
pkg/services/alerting/notifiers/telegram_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestTelegramNotifier(t *testing.T) {
|
||||
Convey("Telegram 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: "telegram_testing",
|
||||
Type: "telegram",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewTelegramNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("settings should trigger incident", func() {
|
||||
json := `
|
||||
{
|
||||
"bottoken": "abcdefgh0123456789",
|
||||
"chatid": "-1234567890"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "telegram_testing",
|
||||
Type: "telegram",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewTelegramNotifier(model)
|
||||
telegramNotifier := not.(*TelegramNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(telegramNotifier.Name, ShouldEqual, "telegram_testing")
|
||||
So(telegramNotifier.Type, ShouldEqual, "telegram")
|
||||
So(telegramNotifier.BotToken, ShouldEqual, "abcdefgh0123456789")
|
||||
So(telegramNotifier.ChatID, ShouldEqual, "-1234567890")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
@ -16,7 +16,19 @@ import (
|
||||
const AlertStateCritical = "CRITICAL"
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier("victorops", NewVictoropsNotifier)
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "victorops",
|
||||
Name: "VictorOps",
|
||||
Description: "Sends notifications to VictorOps",
|
||||
Factory: NewVictoropsNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">VictorOps settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="VictorOps url"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
// NewVictoropsNotifier creates an instance of VictoropsNotifier that
|
||||
|
@ -10,7 +10,35 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier("webhook", NewWebHookNotifier)
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "webhook",
|
||||
Name: "webhook",
|
||||
Description: "Sends HTTP POST request to a URL",
|
||||
Factory: NewWebHookNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Webhook settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Http Method</span>
|
||||
<div class="gf-form-select-wrapper width-14">
|
||||
<select class="gf-form-input" ng-model="ctrl.model.settings.httpMethod" ng-options="t for t in ['POST', 'PUT']">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Username</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.username"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Password</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.password"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
@ -22,7 +50,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
return &WebhookNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
User: model.Settings.Get("user").MustString(),
|
||||
User: model.Settings.Get("username").MustString(),
|
||||
Password: model.Settings.Get("password").MustString(),
|
||||
HttpMethod: model.Settings.Get("httpMethod").MustString("POST"),
|
||||
log: log.New("alerting.notifier.webhook"),
|
||||
|
@ -27,50 +27,21 @@ func NewResultHandler() *DefaultResultHandler {
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *DefaultResultHandler) GetStateFromEvaluation(evalContext *EvalContext) m.AlertStateType {
|
||||
if evalContext.Error != nil {
|
||||
handler.log.Error("Alert Rule Result Error",
|
||||
"ruleId", evalContext.Rule.Id,
|
||||
"name", evalContext.Rule.Name,
|
||||
"error", evalContext.Error,
|
||||
"changing state to", evalContext.Rule.ExecutionErrorState.ToAlertState())
|
||||
|
||||
if evalContext.Rule.ExecutionErrorState == m.ExecutionErrorKeepState {
|
||||
return evalContext.PrevAlertState
|
||||
} else {
|
||||
return evalContext.Rule.ExecutionErrorState.ToAlertState()
|
||||
}
|
||||
} else if evalContext.Firing {
|
||||
return m.AlertStateAlerting
|
||||
} else if evalContext.NoDataFound {
|
||||
handler.log.Info("Alert Rule returned no data",
|
||||
"ruleId", evalContext.Rule.Id,
|
||||
"name", evalContext.Rule.Name,
|
||||
"changing state to", evalContext.Rule.NoDataState.ToAlertState())
|
||||
|
||||
if evalContext.Rule.NoDataState == m.NoDataKeepState {
|
||||
return evalContext.PrevAlertState
|
||||
} else {
|
||||
return evalContext.Rule.NoDataState.ToAlertState()
|
||||
}
|
||||
}
|
||||
|
||||
return m.AlertStateOK
|
||||
}
|
||||
|
||||
func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
executionError := ""
|
||||
annotationData := simplejson.New()
|
||||
|
||||
evalContext.Rule.State = handler.GetStateFromEvaluation(evalContext)
|
||||
if evalContext.Firing {
|
||||
annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
|
||||
}
|
||||
|
||||
if evalContext.Error != nil {
|
||||
executionError = evalContext.Error.Error()
|
||||
annotationData.Set("errorMessage", executionError)
|
||||
}
|
||||
|
||||
if evalContext.Firing {
|
||||
annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
|
||||
if evalContext.NoDataFound {
|
||||
annotationData.Set("no_data", true)
|
||||
}
|
||||
|
||||
countStateResult(evalContext.Rule.State)
|
||||
|
@ -1,90 +0,0 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestAlertingResultHandler(t *testing.T) {
|
||||
Convey("Result handler", t, func() {
|
||||
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
|
||||
dummieError := fmt.Errorf("dummie")
|
||||
handler := NewResultHandler()
|
||||
|
||||
Convey("Should update alert state", func() {
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Firing = true
|
||||
|
||||
So(handler.GetStateFromEvaluation(ctx), ShouldEqual, models.AlertStateAlerting)
|
||||
So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("ok -> error(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
|
||||
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("ok -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("pending -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("pending -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@ -63,12 +64,12 @@ func evalMatchesBasedOnState() []*EvalMatch {
|
||||
matches := make([]*EvalMatch, 0)
|
||||
matches = append(matches, &EvalMatch{
|
||||
Metric: "High value",
|
||||
Value: 100,
|
||||
Value: null.FloatFrom(100),
|
||||
})
|
||||
|
||||
matches = append(matches, &EvalMatch{
|
||||
Metric: "Higher Value",
|
||||
Value: 200,
|
||||
Value: null.FloatFrom(200),
|
||||
})
|
||||
|
||||
return matches
|
||||
|
@ -101,6 +101,7 @@ func createDialer() (*gomail.Dialer, error) {
|
||||
|
||||
d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
|
||||
d.TLSConfig = tlsconfig
|
||||
d.LocalName = setting.InstanceName
|
||||
return d, nil
|
||||
}
|
||||
|
||||
|
@ -65,6 +65,7 @@ func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
|
||||
Password: cmd.Password,
|
||||
Body: cmd.Body,
|
||||
HttpMethod: cmd.HttpMethod,
|
||||
HttpHeader: cmd.HttpHeader,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ type Webhook struct {
|
||||
Password string
|
||||
Body string
|
||||
HttpMethod string
|
||||
HttpHeader map[string]string
|
||||
}
|
||||
|
||||
var (
|
||||
@ -63,6 +64,10 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
|
||||
request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
|
||||
}
|
||||
|
||||
for k, v := range webhook.HttpHeader {
|
||||
request.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := ctxhttp.Do(ctx, http.DefaultClient, request)
|
||||
if err != nil {
|
||||
return err
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user