Merge branch 'master' into websocket

This commit is contained in:
Torkel Ödegaard 2017-01-31 08:56:49 +01:00
commit 8a95c563bb
283 changed files with 29761 additions and 7732 deletions

View File

@ -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 ### Enhancements
* **Postgres**: Add support for Certs for Postgres database [#6655](https://github.com/grafana/grafana/issues/6655) * **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) * **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) * **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) * **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 ### Bugfixes
* **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679) * **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679)

View File

@ -4,7 +4,7 @@ deps-go:
go run build.go setup go run build.go setup
deps-js: deps-js:
npm install yarn install --pure-lockfile
deps: deps-go deps-js deps: deps-go deps-js

View File

@ -1,4 +1,4 @@
[Grafana](http://grafana.org) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Coverage Status](https://coveralls.io/repos/grafana/grafana/badge.png)](https://coveralls.io/r/grafana/grafana) [Grafana](http://grafana.org) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana)
================ ================
[Website](http://grafana.org) | [Website](http://grafana.org) |
[Twitter](https://twitter.com/grafana) | [Twitter](https://twitter.com/grafana) |
@ -10,7 +10,7 @@
Grafana is an open source, feature rich metrics dashboard and graph editor for Grafana is an open source, feature rich metrics dashboard and graph editor for
Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB. Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
![](http://grafana.org/assets/img/start_page_bg.png) ![](http://grafana.org/assets/img/features/dashboard_ex1.png)
- [Install instructions](http://docs.grafana.org/installation/) - [Install instructions](http://docs.grafana.org/installation/)
- [What's New in Grafana 2.0](http://docs.grafana.org/guides/whats-new-in-v2/) - [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 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 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.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 ## Features
### Graphite Target Editor ### 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: npm (v2.5.0) and grunt (v0.4.5). Run the following:
```bash ```bash
npm install npm install -g yarn
yarn install --pure-lockfile
npm run build npm run build
``` ```

View File

@ -5,13 +5,14 @@ os: Windows Server 2012 R2
clone_folder: c:\gopath\src\github.com\grafana\grafana clone_folder: c:\gopath\src\github.com\grafana\grafana
environment: environment:
nodejs_version: "5" nodejs_version: "6"
GOPATH: c:\gopath GOPATH: c:\gopath
install: install:
# install nodejs and npm # install nodejs and npm
- ps: Install-Product node $env:nodejs_version - ps: Install-Product node $env:nodejs_version
- npm install - npm install -g yarn
- yarn install --pure-lockfile
- npm install -g grunt-cli - npm install -g grunt-cli
# install gcc (needed for sqlite3) # install gcc (needed for sqlite3)
- choco install -y --limit-output mingw - choco install -y --limit-output mingw

View File

@ -15,11 +15,12 @@
"dependencies": { "dependencies": {
"jquery": "3.1.0", "jquery": "3.1.0",
"lodash": "4.15.0", "lodash": "4.15.0",
"angular": "1.5.8", "angular": "1.6.1",
"angular-route": "1.5.8", "angular-route": "1.6.1",
"angular-mocks": "1.5.8", "angular-mocks": "1.6.1",
"angular-sanitize": "1.5.8", "angular-sanitize": "1.6.1",
"angular-native-dragdrop": "1.2.2", "angular-native-dragdrop": "1.2.2",
"angular-bindonce": "0.3.3" "angular-bindonce": "0.3.3",
"clipboard": "^1.5.16"
} }
} }

View File

@ -37,6 +37,7 @@ var (
race bool race bool
phjsToRelease string phjsToRelease string
workingDir string workingDir string
includeBuildNumber bool = true
binaries []string = []string{"grafana-server", "grafana-cli"} binaries []string = []string{"grafana-server", "grafana-cli"}
) )
@ -47,9 +48,6 @@ func main() {
log.SetFlags(0) log.SetFlags(0)
ensureGoPath() 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(&goarch, "goarch", runtime.GOARCH, "GOARCH")
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS") flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
@ -59,8 +57,13 @@ func main() {
flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH") flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary") flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
flag.BoolVar(&race, "race", race, "Use race detector") flag.BoolVar(&race, "race", race, "Use race detector")
flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
flag.Parse() flag.Parse()
readVersionFromPackageJson()
log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration)
if flag.NArg() == 0 { if flag.NArg() == 0 {
log.Println("Usage: go run build.go build") log.Println("Usage: go run build.go build")
return return
@ -90,24 +93,20 @@ func main() {
case "package": case "package":
grunt(gruntBuildArg("release")...) grunt(gruntBuildArg("release")...)
createLinuxPackages() createLinuxPackages()
sha1FilesInDist()
case "pkg-rpm": case "pkg-rpm":
grunt(gruntBuildArg("release")...) grunt(gruntBuildArg("release")...)
createRpmPackages() createRpmPackages()
sha1FilesInDist()
case "pkg-deb": case "pkg-deb":
grunt(gruntBuildArg("release")...) grunt(gruntBuildArg("release")...)
createDebPackages() createDebPackages()
sha1FilesInDist()
case "sha1-dist": case "sha1-dist":
sha1FilesInDist() sha1FilesInDist()
case "latest": case "latest":
makeLatestDistCopies() makeLatestDistCopies()
sha1FilesInDist()
case "clean": case "clean":
clean() clean()
@ -157,7 +156,9 @@ func readVersionFromPackageJson() {
} }
// add timestamp to iteration // add timestamp to iteration
if includeBuildNumber {
linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration) linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration)
}
} }
type linuxPackageOptions struct { type linuxPackageOptions struct {
@ -167,7 +168,6 @@ type linuxPackageOptions struct {
serverBinPath string serverBinPath string
cliBinPath string cliBinPath string
configDir string configDir string
configFilePath string
ldapFilePath string ldapFilePath string
etcDefaultPath string etcDefaultPath string
etcDefaultFilePath string etcDefaultFilePath string
@ -188,8 +188,6 @@ func createDebPackages() {
homeDir: "/usr/share/grafana", homeDir: "/usr/share/grafana",
binPath: "/usr/sbin", binPath: "/usr/sbin",
configDir: "/etc/grafana", configDir: "/etc/grafana",
configFilePath: "/etc/grafana/grafana.ini",
ldapFilePath: "/etc/grafana/ldap.toml",
etcDefaultPath: "/etc/default", etcDefaultPath: "/etc/default",
etcDefaultFilePath: "/etc/default/grafana-server", etcDefaultFilePath: "/etc/default/grafana-server",
initdScriptFilePath: "/etc/init.d/grafana-server", initdScriptFilePath: "/etc/init.d/grafana-server",
@ -210,8 +208,6 @@ func createRpmPackages() {
homeDir: "/usr/share/grafana", homeDir: "/usr/share/grafana",
binPath: "/usr/sbin", binPath: "/usr/sbin",
configDir: "/etc/grafana", configDir: "/etc/grafana",
configFilePath: "/etc/grafana/grafana.ini",
ldapFilePath: "/etc/grafana/ldap.toml",
etcDefaultPath: "/etc/sysconfig", etcDefaultPath: "/etc/sysconfig",
etcDefaultFilePath: "/etc/sysconfig/grafana-server", etcDefaultFilePath: "/etc/sysconfig/grafana-server",
initdScriptFilePath: "/etc/init.d/grafana-server", initdScriptFilePath: "/etc/init.d/grafana-server",
@ -222,7 +218,7 @@ func createRpmPackages() {
defaultFileSrc: "packaging/rpm/sysconfig/grafana-server", defaultFileSrc: "packaging/rpm/sysconfig/grafana-server",
systemdFileSrc: "packaging/rpm/systemd/grafana-server.service", 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)) runPrint("cp", "-a", filepath.Join(workingDir, "tmp")+"/.", filepath.Join(packageRoot, options.homeDir))
// remove bin path // remove bin path
runPrint("rm", "-rf", filepath.Join(packageRoot, options.homeDir, "bin")) 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{ args := []string{
"-s", "dir", "-s", "dir",
@ -269,8 +261,6 @@ func createPackage(options linuxPackageOptions) {
"--url", "http://grafana.org", "--url", "http://grafana.org",
"--license", "\"Apache 2.0\"", "--license", "\"Apache 2.0\"",
"--maintainer", "contact@grafana.org", "--maintainer", "contact@grafana.org",
"--config-files", options.configFilePath,
"--config-files", options.ldapFilePath,
"--config-files", options.initdScriptFilePath, "--config-files", options.initdScriptFilePath,
"--config-files", options.etcDefaultFilePath, "--config-files", options.etcDefaultFilePath,
"--config-files", options.systemdServiceFilePath, "--config-files", options.systemdServiceFilePath,
@ -334,7 +324,12 @@ func grunt(params ...string) {
} }
func gruntBuildArg(task string) []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 != "" { if pkgArch != "" {
args = append(args, fmt.Sprintf("--arch=%v", pkgArch)) args = append(args, fmt.Sprintf("--arch=%v", pkgArch))
} }
@ -429,14 +424,10 @@ func setBuildEnv() {
} }
func getGitSha() string { func getGitSha() string {
v, err := runError("git", "describe", "--always", "--dirty") v, err := runError("git", "rev-parse", "--short", "HEAD")
if err != nil { if err != nil {
return "unknown-dev" return "unknown-dev"
} }
v = versionRe.ReplaceAllFunc(v, func(s []byte) []byte {
s[0] = '+'
return s
})
return string(v) return string(v)
} }
@ -516,8 +507,15 @@ func md5File(file string) error {
func sha1FilesInDist() { func sha1FilesInDist() {
filepath.Walk("./dist", func(path string, f os.FileInfo, err error) error { filepath.Walk("./dist", func(path string, f os.FileInfo, err error) error {
if path == "./dist" {
return nil
}
if strings.Contains(path, ".sha1") == false { 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 return nil
}) })

View File

@ -1,18 +1,26 @@
machine: machine:
node: node:
version: 5.11.1 version: 6.9.2
python:
version: 2.7.3
services:
- docker
environment: environment:
GOPATH: "/home/ubuntu/.go_workspace" GOPATH: "/home/ubuntu/.go_workspace"
ORG_PATH: "github.com/grafana" ORG_PATH: "github.com/grafana"
REPO_PATH: "${ORG_PATH}/grafana" REPO_PATH: "${ORG_PATH}/grafana"
GODIST: "go1.7.4.linux-amd64.tar.gz" GODIST: "go1.7.4.linux-amd64.tar.gz"
post: post:
- mkdir -p download - mkdir -p ~/download
- mkdir -p ~/docker
- test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST - test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST
- sudo rm -rf /usr/local/go - sudo rm -rf /usr/local/go
- sudo tar -C /usr/local -xzf download/$GODIST - sudo tar -C /usr/local -xzf download/$GODIST
dependencies: dependencies:
cache_directories:
- "~/docker"
- "~/download"
override: override:
- rm -rf ${GOPATH}/src/${REPO_PATH} - rm -rf ${GOPATH}/src/${REPO_PATH}
- mkdir -p ${GOPATH}/src/${ORG_PATH} - mkdir -p ${GOPATH}/src/${ORG_PATH}
@ -23,9 +31,26 @@ test:
- bash scripts/circle-test.sh - bash scripts/circle-test.sh
deployment: deployment:
master: gh_branch:
branch: master branch: new_master
owner: grafana
commands: commands:
- ./scripts/trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} - pip install awscli
- ./scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} - 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}

View File

@ -113,6 +113,12 @@ cookie_secure = false
session_life_time = 86400 session_life_time = 86400
gc_interval_time = 86400 gc_interval_time = 86400
#################################### Data proxy ###########################
[dataproxy]
# This enables data proxy logging, default is false
logging = false
#################################### Analytics ########################### #################################### Analytics ###########################
[analytics] [analytics]
# Server reporting, sends usage counters to stats.grafana.org every 24 hours. # Server reporting, sends usage counters to stats.grafana.org every 24 hours.
@ -279,6 +285,7 @@ allow_sign_up = true
enabled = false enabled = false
host = localhost:25 host = localhost:25
user = user =
# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
password = password =
cert_file = cert_file =
key_file = key_file =
@ -395,7 +402,9 @@ global_session = -1
#################################### Alerting ############################ #################################### 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 execute_alerts = true
#################################### Internal Grafana Metrics ############ #################################### Internal Grafana Metrics ############

View File

@ -19,6 +19,7 @@ ssl_skip_verify = false
# Search user bind dn # Search user bind dn
bind_dn = "cn=admin,dc=grafana,dc=org" bind_dn = "cn=admin,dc=grafana,dc=org"
# Search user bind password # Search user bind password
# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
bind_password = 'grafana' bind_password = 'grafana'
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"

View File

@ -104,6 +104,13 @@
# Session life time, default is 86400 # Session life time, default is 86400
;session_life_time = 86400 ;session_life_time = 86400
#################################### Data proxy ###########################
[dataproxy]
# This enables data proxy logging, default is false
;logging = false
#################################### Analytics #################################### #################################### Analytics ####################################
[analytics] [analytics]
# Server reporting, sends usage counters to stats.grafana.org every 24 hours. # Server reporting, sends usage counters to stats.grafana.org every 24 hours.
@ -263,6 +270,7 @@
;enabled = false ;enabled = false
;host = localhost:25 ;host = localhost:25
;user = ;user =
# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
;password = ;password =
;cert_file = ;cert_file =
;key_file = ;key_file =
@ -342,9 +350,11 @@
;enabled = false ;enabled = false
;path = /var/lib/grafana/dashboards ;path = /var/lib/grafana/dashboards
#################################### Alerting ###################################### #################################### 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 ;execute_alerts = true
#################################### Internal Grafana Metrics ########################## #################################### Internal Grafana Metrics ##########################

View 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 ...`

View File

@ -72,4 +72,9 @@ label_values(hostname)
You can also use raw queries & regular expressions to extract anything you might need. 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}`.
![](/img/v2/prometheus_templating.png) ![](/img/v2/prometheus_templating.png)

View 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.
![](/img/docs/v41/test_data_add.png)
## 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.
![](/img/docs/v41/test_data_csv_example.png)
## 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.

View File

@ -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. 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 * `min` - The smallest value in the series
* `max` - The largest 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. * `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 * `total` - The sum of all the non-null values in the series
* `first` - The first value 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. * `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. * `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 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. 5. `Units`: Units are appended to the the Singlestat within the panel, and will respect the color and threshold settings for the value.

View File

@ -7,7 +7,7 @@ type = "docs"
name = "Version 3.1" name = "Version 3.1"
identifier = "v3.1" identifier = "v3.1"
parent = "whatsnew" parent = "whatsnew"
weight = 1 weight = 5
+++ +++
# What's New in Grafana v3.1 # What's New in Grafana v3.1

View File

@ -7,7 +7,7 @@ type = "docs"
name = "Version 3.0" name = "Version 3.0"
identifier = "v3.0" identifier = "v3.0"
parent = "whatsnew" parent = "whatsnew"
weight = 2 weight = 6
+++ +++
# What's New in Grafana v3.0 # What's New in Grafana v3.0

View 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!

View File

@ -4,10 +4,10 @@ description = "Feature & improvement highlights for Grafana v4.0"
keywords = ["grafana", "new", "documentation", "4.0"] keywords = ["grafana", "new", "documentation", "4.0"]
type = "docs" type = "docs"
[menu.docs] [menu.docs]
name = "Version 4.0 (Latest)" name = "Version 4.0"
identifier = "v4.0" identifier = "v4.0"
parent = "whatsnew" parent = "whatsnew"
weight = -1 weight = 4
+++ +++
# What's New in Grafana v4.0 # What's New in Grafana v4.0

View File

@ -143,6 +143,7 @@ with Grafana admin permission.
"protocol":"http", "protocol":"http",
"root_url":"%(protocol)s://%(domain)s:%(http_port)s/", "root_url":"%(protocol)s://%(domain)s:%(http_port)s/",
"router_logging":"true", "router_logging":"true",
"data_proxy_logging":"true",
"static_root_path":"public" "static_root_path":"public"
}, },
"session":{ "session":{
@ -275,3 +276,20 @@ Change password for specific user
Content-Type: application/json Content-Type: application/json
{message: "User deleted"} {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}

View 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"
}

View File

@ -200,7 +200,7 @@ Get all tags of dashboards
**Example Request**: **Example Request**:
GET /api/dashboards/home HTTP/1.1 GET /api/dashboards/tags HTTP/1.1
Accept: application/json Accept: application/json
Content-Type: application/json Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk

View File

@ -158,7 +158,7 @@ parent = "http_api"
HTTP/1.1 200 HTTP/1.1 200
Content-Type: application/json Content-Type: application/json
{"id":1,"message":"Datasource added"} {"id":1,"message":"Datasource added", "name": "test_datasource"}
## Update an existing data source ## Update an existing data source
@ -193,7 +193,7 @@ parent = "http_api"
HTTP/1.1 200 HTTP/1.1 200
Content-Type: application/json Content-Type: application/json
{"message":"Datasource updated"} {"message":"Datasource updated", "id": 1, "name": "test_datasource"}
## Delete an existing data source ## Delete an existing data source

View File

@ -69,6 +69,40 @@ parent = "http_api"
"isGrafanaAdmin": true "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 ## User Update
`PUT /api/users/:id` `PUT /api/users/:id`

View File

@ -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 things). By default it is configured to use `sqlite3` which is an
embedded database (included in the main Grafana binary). 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 ### type
Either `mysql`, `postgres` or `sqlite3`, it's your choice. 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 The role new users will be assigned for the main organization (if the
above setting is set to true). Defaults to `Viewer`, other valid 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> <hr>
@ -611,6 +618,11 @@ basic auth password
## [alerting] ## [alerting]
### enabled
Defaults to true. Set to false to disable alerting engine and hide Alerting from UI.
### execute_alerts
### execute_alerts = true ### execute_alerts = true
Makes it possible to turn off alert rule execution. Makes it possible to turn off alert rule execution.

View File

@ -15,14 +15,14 @@ weight = 1
Description | Download 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 ## 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 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 ## APT Repository

View File

@ -43,6 +43,7 @@ ssl_skip_verify = false
# Search user bind dn # Search user bind dn
bind_dn = "cn=admin,dc=grafana,dc=org" bind_dn = "cn=admin,dc=grafana,dc=org"
# Search user bind password # Search user bind password
# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
bind_password = 'grafana' bind_password = 'grafana'
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"

View File

@ -15,24 +15,24 @@ weight = 2
Description | Download 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 ## Install Stable
You can install Grafana using Yum directly. 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`. Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat: #### On CentOS / Fedora / Redhat:
$ sudo yum install initscripts fontconfig $ 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: #### 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 ## Install via YUM Repository

View File

@ -13,7 +13,7 @@ weight = 3
Description | Download 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 ## Configure

View File

@ -37,7 +37,7 @@ The Datasource should contain the following functions.
``` ```
query(options) //used by panels to get data query(options) //used by panels to get data
testDatasource() //used by datasource configuration page to make sure the connection is working 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. metricFindQuery(options) // used by query editor to get metric suggestions.
``` ```
@ -119,7 +119,7 @@ An array of
### Annotation Query ### Annotation Query
Request object passed to datasource.annotationsQuery function Request object passed to datasource.annotationQuery function
```json ```json
{ {
"range": { "from": "2016-03-04T04:07:55.144Z", "to": "2016-03-04T07:07:55.144Z" }, "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. 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.

View File

@ -23,6 +23,8 @@ export GOPATH=`pwd`
go get github.com/grafana/grafana 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 ## Building the backend
``` ```
cd $GOPATH/src/github.com/grafana/grafana 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 (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 npm install -g grunt-cli
grunt grunt
``` ```

View File

@ -1,4 +1,4 @@
{ {
"stable": "4.0.2", "stable": "4.1.1",
"testing": "4.0.2" "testing": "4.1.1"
} }

View File

@ -4,7 +4,7 @@
"company": "Coding Instinct AB" "company": "Coding Instinct AB"
}, },
"name": "grafana", "name": "grafana",
"version": "4.1.0-pre1", "version": "4.2.0-pre1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "http://github.com/grafana/grafana.git" "url": "http://github.com/grafana/grafana.git"
@ -29,7 +29,6 @@
"grunt-contrib-watch": "^1.0.0", "grunt-contrib-watch": "^1.0.0",
"grunt-exec": "^1.0.1", "grunt-exec": "^1.0.1",
"grunt-filerev": "^2.3.1", "grunt-filerev": "^2.3.1",
"grunt-git-describe": "~2.4.2",
"grunt-karma": "~2.0.0", "grunt-karma": "~2.0.0",
"grunt-ng-annotate": "^3.0.0", "grunt-ng-annotate": "^3.0.0",
"grunt-notify": "^0.4.5", "grunt-notify": "^0.4.5",
@ -42,13 +41,12 @@
"karma": "1.3.0", "karma": "1.3.0",
"karma-chrome-launcher": "~2.0.0", "karma-chrome-launcher": "~2.0.0",
"karma-coverage": "1.1.1", "karma-coverage": "1.1.1",
"karma-coveralls": "1.1.2",
"karma-expect": "~1.1.3", "karma-expect": "~1.1.3",
"karma-mocha": "~1.3.0", "karma-mocha": "~1.3.0",
"karma-phantomjs-launcher": "1.0.2", "karma-phantomjs-launcher": "1.0.2",
"load-grunt-tasks": "3.5.2", "load-grunt-tasks": "3.5.2",
"mocha": "3.2.0", "mocha": "3.2.0",
"phantomjs-prebuilt": "^2.1.13", "phantomjs-prebuilt": "^2.1.14",
"reflect-metadata": "0.1.8", "reflect-metadata": "0.1.8",
"rxjs": "^5.0.0-rc.5", "rxjs": "^5.0.0-rc.5",
"sass-lint": "^1.10.2", "sass-lint": "^1.10.2",
@ -60,9 +58,8 @@
"npm": "2.14.x" "npm": "2.14.x"
}, },
"scripts": { "scripts": {
"build": "grunt", "build": "./node_modules/grunt-cli/bin/grunt",
"test": "grunt test", "test": "./node_modules/grunt-cli/bin/grunt test"
"coveralls": "grunt karma:coveralls && rm -rf ./coverage"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -78,7 +75,7 @@
"sinon": "1.17.6", "sinon": "1.17.6",
"systemjs-builder": "^0.15.34", "systemjs-builder": "^0.15.34",
"tether": "^1.4.0", "tether": "^1.4.0",
"tether-drop": "git://github.com/torkelo/drop", "tether-drop": "https://github.com/torkelo/drop",
"tslint": "^4.0.2", "tslint": "^4.0.2",
"typescript": "^2.1.4", "typescript": "^2.1.4",
"virtual-scroll": "^1.1.1" "virtual-scroll": "^1.1.1"

View File

@ -42,6 +42,12 @@ case "$1" in
chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana
chmod 755 /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 # configuration files should not be modifiable by grafana user, as this can be a security issue
chown -Rh root:$GRAFANA_GROUP /etc/grafana/* chown -Rh root:$GRAFANA_GROUP /etc/grafana/*
chmod 755 /etc/grafana chmod 755 /etc/grafana

View File

@ -14,6 +14,6 @@ CONF_DIR=/etc/grafana
CONF_FILE=/etc/grafana/grafana.ini CONF_FILE=/etc/grafana/grafana.ini
RESTART_ON_UPGRADE=false RESTART_ON_UPGRADE=true
PLUGINS_DIR=/var/lib/grafana/plugins PLUGINS_DIR=/var/lib/grafana/plugins

View File

@ -1,6 +1,6 @@
#! /usr/bin/env bash #! /usr/bin/env bash
deb_ver=4.0.2-1481203731 deb_ver=4.1.0-1484127817
rpm_ver=4.0.2-1481203731 rpm_ver=4.1.0-1484127817
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb

4
packaging/publish/publish_testing.sh Normal file → Executable file
View File

@ -1,6 +1,6 @@
#! /usr/bin/env bash #! /usr/bin/env bash
deb_ver=4.0.2-1481203731 deb_ver=4.1.0-1482230757beta1
rpm_ver=4.0.2-1481203731 rpm_ver=4.1.0-1482230757beta1
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb

View File

@ -6,6 +6,7 @@ set -e
startGrafana() { startGrafana() {
if [ -x /bin/systemctl ] ; then if [ -x /bin/systemctl ] ; then
/bin/systemctl daemon-reload
/bin/systemctl start grafana-server.service /bin/systemctl start grafana-server.service
elif [ -x /etc/init.d/grafana-server ] ; then elif [ -x /etc/init.d/grafana-server ] ; then
/etc/init.d/grafana-server start /etc/init.d/grafana-server start
@ -37,6 +38,12 @@ if [ $1 -eq 1 ] ; then
-c "grafana user" grafana -c "grafana user" grafana
fi 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 # Set user permissions on /var/log/grafana, /var/lib/grafana
mkdir -p /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 chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana

View File

@ -14,6 +14,6 @@ CONF_DIR=/etc/grafana
CONF_FILE=/etc/grafana/grafana.ini CONF_FILE=/etc/grafana/grafana.ini
RESTART_ON_UPGRADE=false RESTART_ON_UPGRADE=true
PLUGINS_DIR=/var/lib/grafana/plugins PLUGINS_DIR=/var/lib/grafana/plugins

View File

@ -73,9 +73,9 @@ func GetAlerts(c *middleware.Context) Response {
Name: alert.Name, Name: alert.Name,
Message: alert.Message, Message: alert.Message,
State: alert.State, State: alert.State,
EvalDate: alert.EvalDate,
NewStateDate: alert.NewStateDate, NewStateDate: alert.NewStateDate,
ExecutionError: alert.ExecutionError, ExecutionError: alert.ExecutionError,
EvalData: alert.EvalData,
}) })
} }
@ -121,10 +121,10 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
} }
res := backendCmd.Result res := backendCmd.Result
dtoRes := &dtos.AlertTestResult{ dtoRes := &dtos.AlertTestResult{
Firing: res.Firing, Firing: res.Firing,
ConditionEvals: res.ConditionEvals, ConditionEvals: res.ConditionEvals,
State: res.Rule.State,
} }
if res.Error != nil { if res.Error != nil {
@ -173,6 +173,10 @@ func DelAlert(c *middleware.Context) Response {
return Json(200, resp) return Json(200, resp)
} }
func GetAlertNotifiers(c *middleware.Context) Response {
return Json(200, alerting.GetNotifiers())
}
func GetAlertNotifications(c *middleware.Context) Response { func GetAlertNotifications(c *middleware.Context) Response {
query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId} query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}

View File

@ -125,6 +125,8 @@ func (hs *HttpServer) registerRoutes() {
r.Get("/", wrap(SearchUsers)) r.Get("/", wrap(SearchUsers))
r.Get("/:id", wrap(GetUserById)) r.Get("/:id", wrap(GetUserById))
r.Get("/:id/orgs", wrap(GetUserOrgList)) 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.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg)) r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
}, reqGrafanaAdmin) }, reqGrafanaAdmin)
@ -261,6 +263,7 @@ func (hs *HttpServer) registerRoutes() {
}) })
r.Get("/alert-notifications", wrap(GetAlertNotifications)) r.Get("/alert-notifications", wrap(GetAlertNotifications))
r.Get("/alert-notifiers", wrap(GetAlertNotifiers))
r.Group("/alert-notifications", func() { r.Group("/alert-notifications", func() {
r.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest)) r.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))

View File

@ -17,7 +17,6 @@ import (
"github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/sts" "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/metrics"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
@ -90,7 +89,7 @@ type cache struct {
var awsCredentialCache map[string]cache = make(map[string]cache) var awsCredentialCache map[string]cache = make(map[string]cache)
var credentialCacheLock sync.RWMutex var credentialCacheLock sync.RWMutex
func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials { func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
credentialCacheLock.RLock() credentialCacheLock.RLock()
if _, ok := awsCredentialCache[cacheKey]; ok { if _, ok := awsCredentialCache[cacheKey]; ok {
@ -98,7 +97,7 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) { (*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
result := awsCredentialCache[cacheKey].credential result := awsCredentialCache[cacheKey].credential
credentialCacheLock.RUnlock() credentialCacheLock.RUnlock()
return result return result, nil
} }
} }
credentialCacheLock.RUnlock() credentialCacheLock.RUnlock()
@ -130,8 +129,7 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
svc := sts.New(session.New(stsConfig), stsConfig) svc := sts.New(session.New(stsConfig), stsConfig)
resp, err := svc.AssumeRole(params) resp, err := svc.AssumeRole(params)
if err != nil { if err != nil {
// ignore return nil, err
log.Error(3, "CloudWatch: Failed to assume role", err)
} }
if resp.Credentials != nil { if resp.Credentials != nil {
accessKeyId = *resp.Credentials.AccessKeyId accessKeyId = *resp.Credentials.AccessKeyId
@ -165,19 +163,28 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
} }
credentialCacheLock.Unlock() 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{ cfg := &aws.Config{
Region: aws.String(req.Region), Region: aws.String(req.Region),
Credentials: getCredentials(req.GetDatasourceInfo()), Credentials: creds,
} }
return cfg return cfg, nil
} }
func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) { 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) svc := cloudwatch.New(session.New(cfg), cfg)
reqParam := &struct { reqParam := &struct {
@ -220,7 +227,11 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
} }
func handleListMetrics(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) svc := cloudwatch.New(session.New(cfg), cfg)
reqParam := &struct { reqParam := &struct {
@ -239,7 +250,7 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
} }
var resp cloudwatch.ListMetricsOutput var resp cloudwatch.ListMetricsOutput
err := svc.ListMetricsPages(params, err = svc.ListMetricsPages(params,
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool { func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1) metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
metrics, _ := awsutil.ValuesAtPath(page, "Metrics") metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
@ -257,7 +268,11 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
} }
func handleDescribeAlarms(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) svc := cloudwatch.New(session.New(cfg), cfg)
reqParam := &struct { reqParam := &struct {
@ -296,7 +311,11 @@ func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
} }
func handleDescribeAlarmsForMetric(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) svc := cloudwatch.New(session.New(cfg), cfg)
reqParam := &struct { reqParam := &struct {
@ -336,7 +355,11 @@ func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
} }
func handleDescribeAlarmHistory(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) svc := cloudwatch.New(session.New(cfg), cfg)
reqParam := &struct { reqParam := &struct {
@ -368,7 +391,11 @@ func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
} }
func handleDescribeInstances(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) svc := ec2.New(session.New(cfg), cfg)
reqParam := &struct { reqParam := &struct {
@ -388,7 +415,7 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
} }
var resp ec2.DescribeInstancesOutput var resp ec2.DescribeInstancesOutput
err := svc.DescribeInstancesPages(params, err = svc.DescribeInstancesPages(params,
func(page *ec2.DescribeInstancesOutput, lastPage bool) bool { func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
reservations, _ := awsutil.ValuesAtPath(page, "Reservations") reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
for _, reservation := range reservations { for _, reservation := range reservations {

View File

@ -111,7 +111,7 @@ func init() {
"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"}, "AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
"AWS/ES": {"ClientId", "DomainName"}, "AWS/ES": {"ClientId", "DomainName"},
"AWS/Events": {"RuleName"}, "AWS/Events": {"RuleName"},
"AWS/Firehose": {}, "AWS/Firehose": {"DeliveryStreamName"},
"AWS/IoT": {"Protocol"}, "AWS/IoT": {"Protocol"},
"AWS/Kinesis": {"StreamName", "ShardID"}, "AWS/Kinesis": {"StreamName", "ShardID"},
"AWS/KinesisAnalytics": {"Flow", "Id", "Application"}, "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 // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
func handleGetRegions(req *cwRequest, c *middleware.Context) { func handleGetRegions(req *cwRequest, c *middleware.Context) {
regions := []string{ regions := []string{
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "cn-north-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", "sa-east-1", "us-east-1", "us-west-1", "us-west-2", "us-gov-west-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{}{} result := []interface{}{}
@ -248,9 +248,13 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
} }
func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) { func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
creds, err := getCredentials(cwData)
if err != nil {
return cloudwatch.ListMetricsOutput{}, err
}
cfg := &aws.Config{ cfg := &aws.Config{
Region: aws.String(cwData.Region), Region: aws.String(cwData.Region),
Credentials: getCredentials(cwData), Credentials: creds,
} }
svc := cloudwatch.New(session.New(cfg), cfg) svc := cloudwatch.New(session.New(cfg), cfg)
@ -260,7 +264,7 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
} }
var resp cloudwatch.ListMetricsOutput var resp cloudwatch.ListMetricsOutput
err := svc.ListMetricsPages(params, err = svc.ListMetricsPages(params,
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool { func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1) metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
metrics, _ := awsutil.ValuesAtPath(page, "Metrics") metrics, _ := awsutil.ValuesAtPath(page, "Metrics")

View File

@ -1,6 +1,8 @@
package api package api
import ( import (
"bytes"
"io/ioutil"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
@ -8,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/api/cloudwatch" "github.com/grafana/grafana/pkg/api/cloudwatch"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
@ -15,6 +18,10 @@ import (
"github.com/grafana/grafana/pkg/util" "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 { func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
director := func(req *http.Request) { director := func(req *http.Request) {
req.URL.Scheme = targetUrl.Scheme req.URL.Scheme = targetUrl.Scheme
@ -121,6 +128,32 @@ func ProxyDataSourceRequest(c *middleware.Context) {
c.JsonApiErr(400, "Unable to load TLS certificate", err) c.JsonApiErr(400, "Unable to load TLS certificate", err)
return return
} }
logProxyRequest(ds.Type, c)
proxy.ServeHTTP(c.Resp, c.Req.Request) proxy.ServeHTTP(c.Resp, c.Req.Request)
c.Resp.Header().Del("Set-Cookie") 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)
}

View File

@ -100,7 +100,7 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
return 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 { 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 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 { func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {

View File

@ -3,6 +3,7 @@ package dtos
import ( import (
"time" "time"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
) )
@ -16,6 +17,7 @@ type AlertRule struct {
State m.AlertStateType `json:"state"` State m.AlertStateType `json:"state"`
NewStateDate time.Time `json:"newStateDate"` NewStateDate time.Time `json:"newStateDate"`
EvalDate time.Time `json:"evalDate"` EvalDate time.Time `json:"evalDate"`
EvalData *simplejson.Json `json:"evalData"`
ExecutionError string `json:"executionError"` ExecutionError string `json:"executionError"`
DashbboardUri string `json:"dashboardUri"` DashbboardUri string `json:"dashboardUri"`
} }
@ -36,6 +38,7 @@ type AlertTestCommand struct {
type AlertTestResult struct { type AlertTestResult struct {
Firing bool `json:"firing"` Firing bool `json:"firing"`
State m.AlertStateType `json:"state"`
ConditionEvals string `json:"conditionEvals"` ConditionEvals string `json:"conditionEvals"`
TimeMs string `json:"timeMs"` TimeMs string `json:"timeMs"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
@ -51,7 +54,7 @@ type AlertTestResultLog struct {
type EvalMatch struct { type EvalMatch struct {
Tags map[string]string `json:"tags,omitempty"` Tags map[string]string `json:"tags,omitempty"`
Metric string `json:"metric"` Metric string `json:"metric"`
Value float64 `json:"value"` Value null.Float `json:"value"`
} }
type NotificationTestCommand struct { type NotificationTestCommand struct {

View File

@ -31,7 +31,7 @@ type AdminUpdateUserPasswordForm struct {
} }
type AdminUpdateUserPermissionsForm struct { type AdminUpdateUserPermissionsForm struct {
IsGrafanaAdmin bool `json:"IsGrafanaAdmin"` IsGrafanaAdmin bool `json:"isGrafanaAdmin" binding:"Required"`
} }
type AdminUserListItem struct { type AdminUserListItem struct {

View File

@ -140,6 +140,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin, "allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
"authProxyEnabled": setting.AuthProxyEnabled, "authProxyEnabled": setting.AuthProxyEnabled,
"ldapEnabled": setting.LdapEnabled, "ldapEnabled": setting.LdapEnabled,
"alertingEnabled": setting.AlertingEnabled,
"buildInfo": map[string]interface{}{ "buildInfo": map[string]interface{}{
"version": setting.BuildVersion, "version": setting.BuildVersion,
"commit": setting.BuildCommit, "commit": setting.BuildCommit,

View File

@ -103,10 +103,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
Children: dashboardChildNavs, 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{ alertChildNavs := []*dtos.NavLink{
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"}, {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{ data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{

View File

@ -19,6 +19,7 @@ func RenderToPng(c *middleware.Context) {
Height: queryReader.Get("height", "400"), Height: queryReader.Get("height", "400"),
OrgId: c.OrgId, OrgId: c.OrgId,
Timeout: queryReader.Get("timeout", "30"), Timeout: queryReader.Get("timeout", "30"),
Timezone: queryReader.Get("tz", ""),
} }
pngPath, err := renderer.RenderToPng(renderOpts) pngPath, err := renderer.RenderToPng(renderOpts)

View File

@ -13,7 +13,7 @@ func GetSignedInUser(c *middleware.Context) Response {
return getUserUserProfile(c.UserId) return getUserUserProfile(c.UserId)
} }
// GET /api/user/:id // GET /api/users/:id
func GetUserById(c *middleware.Context) Response { func GetUserById(c *middleware.Context) Response {
return getUserUserProfile(c.ParamsInt64(":id")) return getUserUserProfile(c.ParamsInt64(":id"))
} }
@ -22,12 +22,36 @@ func getUserUserProfile(userId int64) Response {
query := m.GetUserProfileQuery{UserId: userId} query := m.GetUserProfileQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil { 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 ApiError(500, "Failed to get user", err)
} }
return Json(200, query.Result) 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 // POST /api/user
func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response { func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
if setting.AuthProxyEnabled { if setting.AuthProxyEnabled {
@ -60,7 +84,7 @@ func UpdateUserActiveOrg(c *middleware.Context) Response {
cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId} cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId}
if err := bus.Dispatch(&cmd); err != nil { 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") return ApiSuccess("Active organization changed")
@ -70,12 +94,12 @@ func handleUpdateUser(cmd m.UpdateUserCommand) Response {
if len(cmd.Login) == 0 { if len(cmd.Login) == 0 {
cmd.Login = cmd.Email cmd.Login = cmd.Email
if len(cmd.Login) == 0 { 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 { 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") return ApiSuccess("User updated")
@ -95,7 +119,7 @@ func getUserOrgList(userId int64) Response {
query := m.GetUserOrgListQuery{UserId: userId} query := m.GetUserOrgListQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil { 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) return Json(200, query.Result)
@ -130,7 +154,7 @@ func UserSetUsingOrg(c *middleware.Context) Response {
cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId} cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
if err := bus.Dispatch(&cmd); err != nil { 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") return ApiSuccess("Active organization changed")

View File

@ -55,7 +55,7 @@ func (g *GrafanaServerImpl) Start() {
plugins.Init() plugins.Init()
// init alerting // init alerting
if setting.ExecuteAlerts { if setting.AlertingEnabled && setting.ExecuteAlerts {
engine := alerting.NewEngine() engine := alerting.NewEngine()
g.childRoutines.Go(func() error { return engine.Run(g.context) }) g.childRoutines.Go(func() error { return engine.Run(g.context) })
} }

View File

@ -2,6 +2,7 @@ package imguploader
import ( import (
"fmt" "fmt"
"regexp"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -30,19 +31,21 @@ func NewImageUploader() (ImageUploader, error) {
accessKey := s3sec.Key("access_key").MustString("") accessKey := s3sec.Key("access_key").MustString("")
secretKey := s3sec.Key("secret_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") 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 NewS3Uploader(region, bucket, "public-read", accessKey, secretKey), nil
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
case "webdav": case "webdav":
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav") webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
if err != nil { if err != nil {

View File

@ -19,7 +19,7 @@ func TestImageUploaderFactory(t *testing.T) {
setting.ImageUploadProvider = "s3" setting.ImageUploadProvider = "s3"
s3sec, err := setting.Cfg.GetSection("external_image_storage.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("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key") s3sec.NewKey("secret_key", "secret_key")
@ -29,9 +29,10 @@ func TestImageUploaderFactory(t *testing.T) {
original, ok := uploader.(*S3Uploader) original, ok := uploader.(*S3Uploader)
So(ok, ShouldBeTrue) 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.accessKey, ShouldEqual, "access_key")
So(original.secretKey, ShouldEqual, "secret_key") So(original.secretKey, ShouldEqual, "secret_key")
So(original.bucket, ShouldEqual, "bucket_url")
}) })
Convey("Webdav uploader", func() { Convey("Webdav uploader", func() {

View File

@ -1,26 +1,33 @@
package imguploader package imguploader
import ( import (
"io/ioutil" "os"
"net/http" "time"
"net/url"
"path"
"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/log"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/kr/s3/s3util"
) )
type S3Uploader struct { type S3Uploader struct {
region string
bucket string bucket string
acl string
secretKey string secretKey string
accessKey string accessKey string
log log.Logger log log.Logger
} }
func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader { func NewS3Uploader(region, bucket, acl, accessKey, secretKey string) *S3Uploader {
return &S3Uploader{ return &S3Uploader{
region: region,
bucket: bucket, bucket: bucket,
acl: acl,
accessKey: accessKey, accessKey: accessKey,
secretKey: secretKey, secretKey: secretKey,
log: log.New("s3uploader"), log: log.New("s3uploader"),
@ -28,42 +35,41 @@ func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
} }
func (u *S3Uploader) Upload(imageDiskPath string) (string, error) { func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
sess := session.New()
s3util.DefaultConfig.AccessKey = u.accessKey creds := credentials.NewChainCredentials(
s3util.DefaultConfig.SecretKey = u.secretKey []credentials.Provider{
&credentials.StaticProvider{Value: credentials.Value{
header := make(http.Header) AccessKeyID: u.accessKey,
header.Add("x-amz-acl", "public-read") SecretAccessKey: u.secretKey,
header.Add("Content-Type", "image/png") }},
&credentials.EnvProvider{},
var imageUrl *url.URL &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
var err error })
cfg := &aws.Config{
if imageUrl, err = url.Parse(u.bucket); err != nil { Region: aws.String(u.region),
return "", err Credentials: creds,
} }
// add image to url key := util.GetRandomString(20) + ".png"
imageUrl.Path = path.Join(imageUrl.Path, util.GetRandomString(20)+".png") log.Debug("Uploading image to s3", "bucket = ", u.bucket, ", key = ", key)
imageUrlString := imageUrl.String()
log.Debug("Uploading image to s3", "url", imageUrlString)
writer, err := s3util.Create(imageUrlString, header, nil) file, err := os.Open(imageDiskPath)
if err != nil { if err != nil {
return "", err return "", err
} }
defer writer.Close() svc := s3.New(session.New(cfg), cfg)
params := &s3.PutObjectInput{
imgData, err := ioutil.ReadFile(imageDiskPath) 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 { if err != nil {
return "", err return "", err
} }
_, err = writer.Write(imgData) return "https://" + u.bucket + ".s3.amazonaws.com/" + key, nil
if err != nil {
return "", err
}
return imageUrlString, nil
} }

View File

@ -96,6 +96,16 @@ func (f Float) MarshalText() ([]byte, error) {
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil 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. // SetValid changes this Float's value and also sets it to be non-null.
func (f *Float) SetValid(n float64) { func (f *Float) SetValid(n float64) {
f.Float64 = n f.Float64 = n

View File

@ -11,6 +11,8 @@ import (
"strconv" "strconv"
"strings"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -23,10 +25,33 @@ type RenderOpts struct {
Height string Height string
Timeout string Timeout string
OrgId int64 OrgId int64
Timezone string
} }
var rendererLog log.Logger = log.New("png-renderer") 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) { func RenderToPng(params *RenderOpts) (string, error) {
rendererLog.Info("Rendering", "path", params.Path) rendererLog.Info("Rendering", "path", params.Path)
@ -73,6 +98,11 @@ func RenderToPng(params *RenderOpts) (string, error) {
return "", err return "", err
} }
if params.Timezone != "" {
baseEnviron := os.Environ()
cmd.Env = appendEnviron(baseEnviron, "TZ", isoTimeOffsetToPosixTz(params.Timezone))
}
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
return "", err return "", err

View File

@ -45,8 +45,11 @@ var (
M_Alerting_Notification_Sent_Email Counter M_Alerting_Notification_Sent_Email Counter
M_Alerting_Notification_Sent_Webhook Counter M_Alerting_Notification_Sent_Webhook Counter
M_Alerting_Notification_Sent_PagerDuty Counter M_Alerting_Notification_Sent_PagerDuty Counter
M_Alerting_Notification_Sent_LINE Counter
M_Alerting_Notification_Sent_Victorops Counter M_Alerting_Notification_Sent_Victorops Counter
M_Alerting_Notification_Sent_OpsGenie 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_GetMetricStatistics Counter
M_Aws_CloudWatch_ListMetrics 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_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty")
M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops") 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_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_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics") M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")

View File

@ -73,7 +73,6 @@ type Alert struct {
Frequency int64 Frequency int64
EvalData *simplejson.Json EvalData *simplejson.Json
EvalDate time.Time
NewStateDate time.Time NewStateDate time.Time
StateChanges int StateChanges int

View File

@ -23,6 +23,7 @@ type SendWebhookSync struct {
Password string Password string
Body string Body string
HttpMethod string HttpMethod string
HttpHeader map[string]string
} }
type SendResetPasswordEmailCommand struct { type SendResetPasswordEmailCommand struct {

View File

@ -3,9 +3,9 @@ package conditions
import ( import (
"encoding/json" "encoding/json"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
"gopkg.in/guregu/null.v3"
) )
var ( var (

View File

@ -3,10 +3,10 @@ package conditions
import ( import (
"testing" "testing"
"gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/components/simplejson"
. "github.com/smartystreets/goconvey/convey" . "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 { func evalutorScenario(json string, reducedValue float64, datapoints ...float64) bool {

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/grafana/grafana/pkg/bus" "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/components/simplejson"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
@ -45,18 +46,18 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
emptySerieCount := 0 emptySerieCount := 0
evalMatchCount := 0 evalMatchCount := 0
var matches []*alerting.EvalMatch var matches []*alerting.EvalMatch
for _, series := range seriesList { for _, series := range seriesList {
reducedValue := c.Reducer.Reduce(series) reducedValue := c.Reducer.Reduce(series)
evalMatch := c.Evaluator.Eval(reducedValue) evalMatch := c.Evaluator.Eval(reducedValue)
if reducedValue.Valid == false { if reducedValue.Valid == false {
emptySerieCount++ emptySerieCount++
continue
} }
if context.IsTestRun { if context.IsTestRun {
context.Logs = append(context.Logs, &alerting.ResultLogEntry{ 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{ matches = append(matches, &alerting.EvalMatch{
Metric: series.Name, 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{ return &alerting.ConditionResult{
Firing: evalMatchCount > 0, Firing: evalMatchCount > 0,
NoDataFound: emptySerieCount == len(seriesList), NoDataFound: emptySerieCount == len(seriesList),

View File

@ -4,9 +4,8 @@ import (
"context" "context"
"testing" "testing"
null "gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/bus" "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/components/simplejson"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
@ -72,7 +71,38 @@ func TestQueryCondition(t *testing.T) {
So(cr.Firing, ShouldBeTrue) 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("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() { Convey("Should set NoDataFound both series are empty", func() {
ctx.series = tsdb.TimeSeriesSlice{ ctx.series = tsdb.TimeSeriesSlice{
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()), tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),

View File

@ -5,8 +5,8 @@ import (
"sort" "sort"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
"gopkg.in/guregu/null.v3"
) )
type QueryReducer interface { type QueryReducer interface {

View File

@ -3,10 +3,10 @@ package conditions
import ( import (
"testing" "testing"
"gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/tsdb"
) )
func TestSimpleReducer(t *testing.T) { func TestSimpleReducer(t *testing.T) {
@ -57,6 +57,16 @@ func TestSimpleReducer(t *testing.T) {
So(result, ShouldEqual, float64(2)) 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() { Convey("avg of number values and null values should ignore nulls", func() {
reducer := NewSimpleReducer("avg") reducer := NewSimpleReducer("avg")
series := &tsdb.TimeSeries{ series := &tsdb.TimeSeries{

View File

@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/models"
) )
type DefaultEvalHandler struct { type DefaultEvalHandler struct {
@ -60,6 +61,40 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
context.Firing = firing context.Firing = firing
context.NoDataFound = noDataFound context.NoDataFound = noDataFound
context.EndTime = time.Now() context.EndTime = time.Now()
context.Rule.State = e.getNewState(context)
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
metrics.M_Alerting_Execution_Time.Update(elapsedTime) 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
}

View File

@ -2,8 +2,10 @@ package alerting
import ( import (
"context" "context"
"fmt"
"testing" "testing"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey" . "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 return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator, NoDataFound: c.noData}, nil
} }
func TestAlertingExecutor(t *testing.T) { func TestAlertingEvaluationHandler(t *testing.T) {
Convey("Test alert execution", t, func() { Convey("Test alert evaluation handler", t, func() {
handler := NewEvalHandler() handler := NewEvalHandler()
Convey("Show return triggered with single passing condition", func() { 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() { Convey("Show return false with not passing asdf", func() {
context := NewEvalContext(context.TODO(), &Rule{ context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{ Conditions: []Condition{
&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}}, &conditionStub{firing: true, operator: "and", matches: []*EvalMatch{{}, {}}},
&conditionStub{firing: false, operator: "and"}, &conditionStub{firing: false, operator: "and"},
}, },
}) })
@ -164,5 +166,73 @@ func TestAlertingExecutor(t *testing.T) {
handler.Eval(context) handler.Eval(context)
So(context.NoDataFound, ShouldBeTrue) 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)
})
})
})
}) })
} }

View File

@ -60,12 +60,25 @@ func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Jso
return nil 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) { func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
e.log.Debug("GetAlerts") 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) row := simplejson.NewFromAny(rowObj)
for _, panelObj := range row.Get("panels").MustArray() { for _, panelObj := range row.Get("panels").MustArray() {

View File

@ -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() { Convey("Parsing and validating dashboard containing graphite alerts", func() {
dashJson, err := simplejson.NewJson([]byte(json)) dashJson, err := simplejson.NewJson([]byte(json))

View File

@ -1,5 +1,7 @@
package alerting package alerting
import "github.com/grafana/grafana/pkg/components/null"
type Job struct { type Job struct {
Offset int64 Offset int64
OffsetWait bool OffsetWait bool
@ -14,7 +16,7 @@ type ResultLogEntry struct {
} }
type EvalMatch struct { type EvalMatch struct {
Value float64 `json:"value"` Value null.Float `json:"value"`
Metric string `json:"metric"` Metric string `json:"metric"`
Tags map[string]string `json:"tags"` Tags map[string]string `json:"tags"`
} }

View File

@ -13,6 +13,14 @@ import (
m "github.com/grafana/grafana/pkg/models" 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 { type RootNotifier struct {
log log.Logger 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) { func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
factory, found := notifierFactories[model.Type] notifierPlugin, found := notifierFactories[model.Type]
if !found { if !found {
return nil, errors.New("Unsupported notification type") return nil, errors.New("Unsupported notification type")
} }
return factory(model) return notifierPlugin.Factory(model)
} }
func shouldUseNotification(notifier Notifier, context *EvalContext) bool { 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) 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) { func RegisterNotifier(plugin *NotifierPlugin) {
notifierFactories[typeName] = factory notifierFactories[plugin.Type] = plugin
}
func GetNotifiers() []*NotifierPlugin {
list := make([]*NotifierPlugin, 0)
for _, value := range notifierFactories {
list = append(list, value)
}
return list
} }

View File

@ -13,7 +13,21 @@ import (
) )
func init() { 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 { type EmailNotifier struct {

View 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
}

View 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")
})
})
}

View File

@ -13,7 +13,28 @@ import (
) )
func init() { 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 ( var (

View File

@ -12,7 +12,28 @@ import (
) )
func init() { 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 ( var (

View 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
}

View 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")
})
})
})
}

View File

@ -13,7 +13,42 @@ import (
) )
func init() { 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) { func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {

View 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
}

View 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")
})
})
})
}

View File

@ -16,7 +16,19 @@ import (
const AlertStateCritical = "CRITICAL" const AlertStateCritical = "CRITICAL"
func init() { 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 // NewVictoropsNotifier creates an instance of VictoropsNotifier that

View File

@ -10,7 +10,35 @@ import (
) )
func init() { 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) { func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
@ -22,7 +50,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
return &WebhookNotifier{ return &WebhookNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Url: url, Url: url,
User: model.Settings.Get("user").MustString(), User: model.Settings.Get("username").MustString(),
Password: model.Settings.Get("password").MustString(), Password: model.Settings.Get("password").MustString(),
HttpMethod: model.Settings.Get("httpMethod").MustString("POST"), HttpMethod: model.Settings.Get("httpMethod").MustString("POST"),
log: log.New("alerting.notifier.webhook"), log: log.New("alerting.notifier.webhook"),

View File

@ -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 { func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
executionError := "" executionError := ""
annotationData := simplejson.New() annotationData := simplejson.New()
evalContext.Rule.State = handler.GetStateFromEvaluation(evalContext) if evalContext.Firing {
annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
}
if evalContext.Error != nil { if evalContext.Error != nil {
executionError = evalContext.Error.Error() executionError = evalContext.Error.Error()
annotationData.Set("errorMessage", executionError) annotationData.Set("errorMessage", executionError)
} }
if evalContext.Firing { if evalContext.NoDataFound {
annotationData = simplejson.NewFromAny(evalContext.EvalMatches) annotationData.Set("no_data", true)
} }
countStateResult(evalContext.Rule.State) countStateResult(evalContext.Rule.State)

View File

@ -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)
})
})
})
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/grafana/grafana/pkg/bus" "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/components/simplejson"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
@ -63,12 +64,12 @@ func evalMatchesBasedOnState() []*EvalMatch {
matches := make([]*EvalMatch, 0) matches := make([]*EvalMatch, 0)
matches = append(matches, &EvalMatch{ matches = append(matches, &EvalMatch{
Metric: "High value", Metric: "High value",
Value: 100, Value: null.FloatFrom(100),
}) })
matches = append(matches, &EvalMatch{ matches = append(matches, &EvalMatch{
Metric: "Higher Value", Metric: "Higher Value",
Value: 200, Value: null.FloatFrom(200),
}) })
return matches return matches

View File

@ -101,6 +101,7 @@ func createDialer() (*gomail.Dialer, error) {
d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password) d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
d.TLSConfig = tlsconfig d.TLSConfig = tlsconfig
d.LocalName = setting.InstanceName
return d, nil return d, nil
} }

View File

@ -65,6 +65,7 @@ func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
Password: cmd.Password, Password: cmd.Password,
Body: cmd.Body, Body: cmd.Body,
HttpMethod: cmd.HttpMethod, HttpMethod: cmd.HttpMethod,
HttpHeader: cmd.HttpHeader,
}) })
} }

View File

@ -19,6 +19,7 @@ type Webhook struct {
Password string Password string
Body string Body string
HttpMethod string HttpMethod string
HttpHeader map[string]string
} }
var ( var (
@ -63,6 +64,10 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password)) 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) resp, err := ctxhttp.Do(ctx, http.DefaultClient, request)
if err != nil { if err != nil {
return err return err

Some files were not shown because too many files have changed in this diff Show More