mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into teams-page-replace-mobx
This commit is contained in:
commit
108a20196f
@ -19,7 +19,7 @@ version: 2
|
||||
jobs:
|
||||
mysql-integration-test:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
- image: circleci/golang:1.11
|
||||
- image: circleci/mysql:5.6-ram
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
postgres-integration-test:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
- image: circleci/golang:1.11
|
||||
- image: circleci/postgres:9.3-ram
|
||||
environment:
|
||||
POSTGRES_USER: grafanatest
|
||||
@ -74,7 +74,7 @@ jobs:
|
||||
|
||||
gometalinter:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
- image: circleci/golang:1.11
|
||||
environment:
|
||||
# we need CGO because of go-sqlite3
|
||||
CGO_ENABLED: 1
|
||||
@ -115,7 +115,7 @@ jobs:
|
||||
|
||||
test-backend:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
- image: circleci/golang:1.11
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -125,7 +125,7 @@ jobs:
|
||||
|
||||
build-all:
|
||||
docker:
|
||||
- image: grafana/build-container:1.0.0
|
||||
- image: grafana/build-container:1.1.0
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -168,7 +168,7 @@ jobs:
|
||||
|
||||
build:
|
||||
docker:
|
||||
- image: grafana/build-container:1.0.0
|
||||
- image: grafana/build-container:1.1.0
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
|
110
CHANGELOG.md
110
CHANGELOG.md
@ -1,76 +1,114 @@
|
||||
# 5.3.0 (unreleased)
|
||||
|
||||
* **OAuth**: Gitlab OAuth with support for filter by groups [#5623](https://github.com/grafana/grafana/issues/5623), thx [@BenoitKnecht](https://github.com/BenoitKnecht)
|
||||
* **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
|
||||
* **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
|
||||
* **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
|
||||
* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos)
|
||||
* **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476)
|
||||
* **LDAP**: Client certificates support [#12805](https://github.com/grafana/grafana/issues/12805), thx [@nyxi](https://github.com/nyxi)
|
||||
* **Postgres**: TimescaleDB support, e.g. use `time_bucket` for grouping by time when option enabled [#12680](https://github.com/grafana/grafana/pull/12680), thx [svenklemm](https://github.com/svenklemm)
|
||||
# 5.4.0 (unreleased)
|
||||
|
||||
### Minor
|
||||
|
||||
* **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
|
||||
* **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
|
||||
* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705)
|
||||
* **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
|
||||
* **OAuth**: Allow oauth email attribute name to be configurable [#12986](https://github.com/grafana/grafana/issues/12986), thx [@bobmshannon](https://github.com/bobmshannon)
|
||||
|
||||
# 5.3.0 (unreleased)
|
||||
|
||||
### Minor
|
||||
|
||||
* **Alerting**: Link to view full size image in Microsoft Teams alert notifier [#13121](https://github.com/grafana/grafana/issues/13121), thx [@holiiveira](https://github.com/holiiveira)
|
||||
|
||||
# 5.3.0-beta1 (2018-09-06)
|
||||
|
||||
### New Major Features
|
||||
|
||||
* **Alerting**: Notification reminders [#7330](https://github.com/grafana/grafana/issues/7330), thx [@jbaublitz](https://github.com/jbaublitz)
|
||||
* **Dashboard**: TV & Kiosk mode changes, new cycle view mode button in dashboard toolbar [#13025](https://github.com/grafana/grafana/pull/13025)
|
||||
* **OAuth**: Gitlab OAuth with support for filter by groups [#5623](https://github.com/grafana/grafana/issues/5623), thx [@BenoitKnecht](https://github.com/BenoitKnecht)
|
||||
* **Postgres**: Graphical query builder [#10095](https://github.com/grafana/grafana/issues/10095), thx [svenklemm](https://github.com/svenklemm)
|
||||
|
||||
### New Features
|
||||
|
||||
* **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
|
||||
* **LDAP**: Client certificates support [#12805](https://github.com/grafana/grafana/issues/12805), thx [@nyxi](https://github.com/nyxi)
|
||||
* **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476)
|
||||
* **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos)
|
||||
* **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
|
||||
* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Postgres**: TimescaleDB support, e.g. use `time_bucket` for grouping by time when option enabled [#12680](https://github.com/grafana/grafana/pull/12680), thx [svenklemm](https://github.com/svenklemm)
|
||||
* **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
|
||||
|
||||
### Minor
|
||||
|
||||
* **Alerting**: Its now possible to configure the default value for how to handle errors and no data in alerting. [#10424](https://github.com/grafana/grafana/issues/10424)
|
||||
* **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
|
||||
* **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151)
|
||||
* **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
|
||||
* **GrafanaCli**: Fixed issue with grafana-cli install plugin resulting in corrupt http response from source error. Fixes [#13079](https://github.com/grafana/grafana/issues/13079)
|
||||
* **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229)
|
||||
* **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
|
||||
* **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
|
||||
* **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
|
||||
* **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
|
||||
* **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie)
|
||||
* **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890)
|
||||
* **Variables**: Support query variable refresh when another variable referenced in `Regex` field change its value [#12952](https://github.com/grafana/grafana/issues/12952), thx [@franciscocpg](https://github.com/franciscocpg)
|
||||
* **Variables**: Support variables in query variable `Custom all value` field [#12965](https://github.com/grafana/grafana/issues/12965), thx [@franciscocpg](https://github.com/franciscocpg)
|
||||
* **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
|
||||
* **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927)
|
||||
* **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen)
|
||||
* **Postgres/MySQL/MSSQL**: New $__unixEpochGroup and $__unixEpochGroupAlias macros [#12892](https://github.com/grafana/grafana/issues/12892), thx [@svenklemm](https://github.com/svenklemm)
|
||||
* **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm)
|
||||
* **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
|
||||
* **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm)
|
||||
* **Postgres/MySQL/MSSQL**: New $__timeGroupAlias macro. Postgres $__timeGroup no longer automatically adds time column alias [#12749](https://github.com/grafana/grafana/issues/12749), thx [@svenklemm](https://github.com/svenklemm)
|
||||
* **Postgres/MySQL/MSSQL**: Escape single quotes in variables [#12785](https://github.com/grafana/grafana/issues/12785), thx [@eMerzh](https://github.com/eMerzh)
|
||||
* **Postgres/MySQL/MSSQL**: Min time interval support [#13157](https://github.com/grafana/grafana/issues/13157), thx [@svenklemm](https://github.com/svenklemm)
|
||||
* **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
|
||||
* **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan)
|
||||
* **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
|
||||
* **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
|
||||
* **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151)
|
||||
* **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg)
|
||||
* **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88)
|
||||
* **Cloudwatch**: Added BurstBalance metric to list of AWS RDS metrics [#12561](https://github.com/grafana/grafana/pulls/12561), thx [@activeshadow](https://github.com/activeshadow)
|
||||
* **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z)
|
||||
* **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
|
||||
* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705)
|
||||
* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
|
||||
* **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
|
||||
* **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766)
|
||||
om/grafana/grafana/issues/12668)
|
||||
* **Table**: Fix for useless horizontal scrollbar for table panel [#9964](https://github.com/grafana/grafana/issues/9964)
|
||||
* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
|
||||
* **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
|
||||
* **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
|
||||
* **Heatmap**: Fix broken tooltip and crosshair on Firefox [#12486](https://github.com/grafana/grafana/issues/12486)
|
||||
* **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270)
|
||||
* **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890)
|
||||
* **Variables**: Support query variable refresh when another variable referenced in `Regex` field change its value [#12952](https://github.com/grafana/grafana/issues/12952), thx [@franciscocpg](https://github.com/franciscocpg)
|
||||
* **Variables**: Support variables in query variable `Custom all value` field [#12965](https://github.com/grafana/grafana/issues/12965), thx [@franciscocpg](https://github.com/franciscocpg)
|
||||
* **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)
|
||||
* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
|
||||
* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Units**: Adds bitcoin axes unit. [#13125](https://github.com/grafana/grafana/pull/13125)
|
||||
* **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
|
||||
* **Logging**: Reopen log files after receiving a SIGHUP signal [#13112](https://github.com/grafana/grafana/pull/13112), thx [@filewalkwithme](https://github.com/filewalkwithme)
|
||||
* **Login**: Show loading animation while waiting for authentication response on login [#12865](https://github.com/grafana/grafana/issues/12865)
|
||||
* **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig)
|
||||
* **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270)
|
||||
* **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud)
|
||||
* **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
|
||||
* **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927)
|
||||
* **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen)
|
||||
* **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229)
|
||||
* **Heatmap**: Fix broken tooltip and crosshair on Firefox [#12486](https://github.com/grafana/grafana/issues/12486)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Postgres datasource no longer automatically adds time column alias when using the $__timeGroup alias. However, there's code in place which should make this change backward compatible and shouldn't create any issues.
|
||||
* Kiosk mode now also hides submenu (variables)
|
||||
* ?inactive url parameter no longer supported, replaced with kiosk=tv url parameter
|
||||
|
||||
### New experimental features
|
||||
|
||||
These are new features that's still being worked on and are in an experimental phase. We incourage users to try these out and provide any feedback in related issue.
|
||||
These are new features that's still being worked on and are in an experimental phase. We encourage users to try these out and provide any feedback in related issue.
|
||||
|
||||
* **Dashboard**: Auto fit dashboard panels to optimize space used for current TV / Monitor [#12768](https://github.com/grafana/grafana/issues/12768)
|
||||
|
||||
### Tech
|
||||
|
||||
* **Frontend**: Convert all Frontend Karma tests to Jest tests [#12224](https://github.com/grafana/grafana/issues/12224)
|
||||
* **Backend**: Upgrade to golang 1.11 [#13030](https://github.com/grafana/grafana/issues/13030)
|
||||
|
||||
# 5.2.4 (2018-09-07)
|
||||
|
||||
* **GrafanaCli**: Fixed issue with grafana-cli install plugin resulting in corrupt http response from source error. Fixes [#13079](https://github.com/grafana/grafana/issues/13079)
|
||||
|
||||
# 5.2.3 (2018-08-29)
|
||||
|
||||
### Important fix for LDAP & OAuth login vulnerability
|
||||
|
||||
See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4-6-4-security-update/10050) for details.
|
||||
|
||||
# 5.2.2 (2018-07-25)
|
||||
|
||||
@ -440,6 +478,12 @@ The following properties have been deprecated and will be removed in a future re
|
||||
- `uri` property in `GET /api/search` -> Use new `url` or `uid` property instead
|
||||
- `meta.slug` property in `GET /api/dashboards/uid/:uid` and `GET /api/dashboards/db/:slug` -> Use new `meta.url` or `dashboard.uid` property instead
|
||||
|
||||
# 4.6.4 (2018-08-29)
|
||||
|
||||
### Important fix for LDAP & OAuth login vulnerability
|
||||
|
||||
See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4-6-4-security-update/10050) for details.
|
||||
|
||||
# 4.6.3 (2017-12-14)
|
||||
|
||||
## Fixes
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Golang build container
|
||||
FROM golang:1.10
|
||||
FROM golang:1.11
|
||||
|
||||
WORKDIR $GOPATH/src/github.com/grafana/grafana
|
||||
|
||||
|
8
Gopkg.lock
generated
8
Gopkg.lock
generated
@ -427,12 +427,6 @@
|
||||
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/shurcooL/sanitized_anchor_name"
|
||||
packages = ["."]
|
||||
revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/smartystreets/assertions"
|
||||
packages = [
|
||||
@ -679,6 +673,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "cb8e7fd81f23ec987fc4d5dd9d31ae0f1164bc2f30cbea2fe86e0d97dd945beb"
|
||||
inputs-digest = "81a37e747b875cf870c1b9486fa3147e704dea7db8ba86f7cb942d3ddc01d3e3"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Go 1.10
|
||||
- Go 1.11
|
||||
- NodeJS LTS
|
||||
|
||||
### Building the backend
|
||||
|
@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
|
||||
environment:
|
||||
nodejs_version: "6"
|
||||
GOPATH: C:\gopath
|
||||
GOVERSION: 1.10
|
||||
GOVERSION: 1.11
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
|
@ -321,6 +321,7 @@ allow_sign_up = true
|
||||
client_id = some_id
|
||||
client_secret = some_secret
|
||||
scopes = user:email
|
||||
email_attribute_name = email:primary
|
||||
auth_url =
|
||||
token_url =
|
||||
api_url =
|
||||
@ -467,6 +468,12 @@ enabled = true
|
||||
# Makes it possible to turn off alert rule execution but alerting UI is visible
|
||||
execute_alerts = true
|
||||
|
||||
# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
|
||||
error_or_timeout = alerting
|
||||
|
||||
# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
|
||||
nodata_or_nullvalues = no_data
|
||||
|
||||
#################################### Explore #############################
|
||||
[explore]
|
||||
# Enable the Explore section
|
||||
@ -538,3 +545,8 @@ container_name =
|
||||
|
||||
[external_image_storage.local]
|
||||
# does not require any configuration
|
||||
|
||||
[rendering]
|
||||
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
|
||||
server_url =
|
||||
callback_url =
|
||||
|
@ -387,6 +387,12 @@ log_queries =
|
||||
# Makes it possible to turn off alert rule execution but alerting UI is visible
|
||||
;execute_alerts = true
|
||||
|
||||
# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
|
||||
;error_or_timeout = alerting
|
||||
|
||||
# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
|
||||
;nodata_or_nullvalues = no_data
|
||||
|
||||
#################################### Explore #############################
|
||||
[explore]
|
||||
# Enable the Explore section
|
||||
@ -460,3 +466,8 @@ log_queries =
|
||||
|
||||
[external_image_storage.local]
|
||||
# does not require any configuration
|
||||
|
||||
[rendering]
|
||||
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
|
||||
;server_url =
|
||||
;callback_url =
|
||||
|
@ -52,8 +52,6 @@ This admin flag makes a user a `Super Admin`. This means they can access the `Se
|
||||
|
||||
### Dashboard & Folder Permissions
|
||||
|
||||
> Introduced in Grafana v5.0
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}}
|
||||
|
||||
For dashboards and dashboard folders there is a **Permissions** page that make it possible to
|
||||
|
@ -71,6 +71,7 @@ Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/pupp
|
||||
Ansible | [https://github.com/cloudalchemy/ansible-grafana](https://github.com/cloudalchemy/ansible-grafana)
|
||||
Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana)
|
||||
Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana)
|
||||
Jsonnet | [https://github.com/grafana/grafonnet-lib/](https://github.com/grafana/grafonnet-lib/)
|
||||
|
||||
## Datasources
|
||||
|
||||
@ -154,8 +155,8 @@ Since not all datasources have the same configuration settings we only have the
|
||||
| tlsAuthWithCACert | boolean | *All* | Enable TLS authentication using CA cert |
|
||||
| tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
|
||||
| graphiteVersion | string | Graphite | Graphite version |
|
||||
| timeInterval | string | Elastic, InfluxDB & Prometheus | Lowest interval/step value that should be used for this data source |
|
||||
| esVersion | string | Elastic | Elasticsearch version as an number (2/5/56) |
|
||||
| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source |
|
||||
| esVersion | number | Elastic | Elasticsearch version as a number (2/5/56) |
|
||||
| timeField | string | Elastic | Which field that should be used as timestamp |
|
||||
| interval | string | Elastic | Index date time format |
|
||||
| authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
|
||||
@ -165,6 +166,8 @@ Since not all datasources have the same configuration settings we only have the
|
||||
| tsdbVersion | string | OpenTSDB | Version |
|
||||
| tsdbResolution | string | OpenTSDB | Resolution |
|
||||
| sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' |
|
||||
| postgresVersion | number | PostgreSQL | Postgres version as a number (903/904/905/906/1000) meaning v9.3, v9.4, ..., v10 |
|
||||
| timescaledb | boolean | PostgreSQL | Enable usage of TimescaleDB extension |
|
||||
|
||||
#### Secure Json Data
|
||||
|
||||
|
@ -16,12 +16,11 @@ weight = 2
|
||||
|
||||
When an alert changes state, it sends out notifications. Each alert rule can have
|
||||
multiple notifications. In order to add a notification to an alert rule you first need
|
||||
to add and configure a `notification` channel (can be email, PagerDuty or other integration). This is done from the Notification Channels page.
|
||||
to add and configure a `notification` channel (can be email, PagerDuty or other integration).
|
||||
This is done from the Notification Channels page.
|
||||
|
||||
## Notification Channel Setup
|
||||
|
||||
{{< imgbox max-width="30%" img="/img/docs/v50/alerts_notifications_menu.png" caption="Alerting Notification Channels" >}}
|
||||
|
||||
On the Notification Channels page hit the `New Channel` button to go the page where you
|
||||
can configure and setup a new Notification Channel.
|
||||
|
||||
@ -30,7 +29,31 @@ sure it's setup correctly.
|
||||
|
||||
### Send on all alerts
|
||||
|
||||
When checked, this option will nofity for all alert rules - existing and new.
|
||||
When checked, this option will notify for all alert rules - existing and new.
|
||||
|
||||
### Send reminders
|
||||
|
||||
> Only available in Grafana v5.3 and above.
|
||||
|
||||
{{< docs-imagebox max-width="600px" img="/img/docs/v53/alerting_notification_reminders.png" class="docs-image--right" caption="Alerting notification reminders setup" >}}
|
||||
|
||||
When this option is checked additional notifications (reminders) will be sent for triggered alerts. You can specify how often reminders
|
||||
should be sent using number of seconds (s), minutes (m) or hours (h), for example `30s`, `3m`, `5m` or `1h` etc.
|
||||
|
||||
**Important:** Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently than a configured [alert rule evaluation interval](/alerting/rules/#name-evaluation-interval).
|
||||
|
||||
These examples show how often and when reminders are sent for a triggered alert.
|
||||
|
||||
Alert rule evaluation interval | Send reminders every | Reminder sent every (after last alert notification)
|
||||
---------- | ----------- | -----------
|
||||
`30s` | `15s` | ~30 seconds
|
||||
`1m` | `5m` | ~5 minutes
|
||||
`5m` | `15m` | ~15 minutes
|
||||
`6m` | `20m` | ~24 minutes
|
||||
`1h` | `15m` | ~1 hour
|
||||
`1h` | `2h` | ~2 hours
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
## Supported Notification Types
|
||||
|
||||
@ -132,23 +155,23 @@ Once these two properties are set, you can send the alerts to Kafka for further
|
||||
|
||||
### All supported notifiers
|
||||
|
||||
Name | Type |Support images
|
||||
-----|------------ | ------
|
||||
Slack | `slack` | yes
|
||||
Pagerduty | `pagerduty` | yes
|
||||
Email | `email` | yes
|
||||
Webhook | `webhook` | link
|
||||
Kafka | `kafka` | no
|
||||
Hipchat | `hipchat` | yes
|
||||
VictorOps | `victorops` | yes
|
||||
Sensu | `sensu` | yes
|
||||
OpsGenie | `opsgenie` | yes
|
||||
Threema | `threema` | yes
|
||||
Pushover | `pushover` | no
|
||||
Telegram | `telegram` | no
|
||||
Line | `line` | no
|
||||
Prometheus Alertmanager | `prometheus-alertmanager` | no
|
||||
Microsoft Teams | `teams` | yes
|
||||
Name | Type |Support images | Support reminders
|
||||
-----|------------ | ------ | ------ |
|
||||
Slack | `slack` | yes | yes
|
||||
Pagerduty | `pagerduty` | yes | yes
|
||||
Email | `email` | yes | yes
|
||||
Webhook | `webhook` | link | yes
|
||||
Kafka | `kafka` | no | yes
|
||||
Hipchat | `hipchat` | yes | yes
|
||||
VictorOps | `victorops` | yes | yes
|
||||
Sensu | `sensu` | yes | yes
|
||||
OpsGenie | `opsgenie` | yes | yes
|
||||
Threema | `threema` | yes | yes
|
||||
Pushover | `pushover` | no | yes
|
||||
Telegram | `telegram` | no | yes
|
||||
Line | `line` | no | yes
|
||||
Microsoft Teams | `teams` | yes | yes
|
||||
Prometheus Alertmanager | `prometheus-alertmanager` | no | no
|
||||
|
||||
|
||||
|
||||
|
@ -88,6 +88,11 @@ So as you can see from the above scenario Grafana will not send out notification
|
||||
to fire if the rule already is in state `Alerting`. To improve support for queries that return multiple series
|
||||
we plan to track state **per series** in a future release.
|
||||
|
||||
> Starting with Grafana v5.3 you can configure reminders to be sent for triggered alerts. This will send additional notifications
|
||||
> when an alert continues to fire. If other series (like server2 in the example above) also cause the alert rule to fire they will
|
||||
> be included in the reminder notification. Depending on what notification channel you're using you may be able to take advantage
|
||||
> of this feature for identifying new/existing series causing alert to fire. [Read more about notification reminders here](/alerting/notifications/#send-reminders).
|
||||
|
||||
### No Data / Null values
|
||||
|
||||
Below your conditions you can configure how the rule evaluation engine should handle queries that return no data or only null values.
|
||||
|
@ -1,42 +1,43 @@
|
||||
+++
|
||||
title = "Grafana Authproxy"
|
||||
title = "Auth Proxy"
|
||||
description = "Grafana Auth Proxy Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "proxy"]
|
||||
type = "docs"
|
||||
keywords = ["grafana", "tutorials", "authproxy"]
|
||||
aliases = ["/tutorials/authproxy/"]
|
||||
[menu.docs]
|
||||
parent = "tutorials"
|
||||
weight = 10
|
||||
name = "Auth Proxy"
|
||||
identifier = "auth-proxy"
|
||||
parent = "authentication"
|
||||
weight = 2
|
||||
+++
|
||||
|
||||
# Grafana Authproxy
|
||||
# Auth Proxy Authentication
|
||||
|
||||
AuthProxy allows you to offload the authentication of users to a web server (there are many reasons why you’d want to run a web server in front of a production version of Grafana, especially if it’s exposed to the Internet).
|
||||
You can configure Grafana to let a http reverse proxy handling authentication. Popular web servers have a very
|
||||
extensive list of pluggable authentication modules, and any of them can be used with the AuthProxy feature.
|
||||
Below we detail the configuration options for auth proxy.
|
||||
|
||||
Popular web servers have a very extensive list of pluggable authentication modules, and any of them can be used with the AuthProxy feature.
|
||||
|
||||
The Grafana AuthProxy feature is very simple in design, but it is this simplicity that makes it so powerful.
|
||||
|
||||
## Interacting with Grafana’s AuthProxy via curl
|
||||
|
||||
The AuthProxy feature can be configured through the Grafana configuration file with the following options:
|
||||
|
||||
```js
|
||||
```bash
|
||||
[auth.proxy]
|
||||
# Defaults to false, but set to true to enable this feature
|
||||
enabled = true
|
||||
# HTTP Header name that will contain the username or email
|
||||
header_name = X-WEBAUTH-USER
|
||||
# HTTP Header property, defaults to `username` but can also be `email`
|
||||
header_property = username
|
||||
# Set to `true` to enable auto sign up of users who do not exist in Grafana DB. Defaults to `true`.
|
||||
auto_sign_up = true
|
||||
# If combined with Grafana LDAP integration define sync interval
|
||||
ldap_sync_ttl = 60
|
||||
# Limit where auth proxy requests come from by configuring a list of IP addresses.
|
||||
# This can be used to prevent users spoofing the X-WEBAUTH-USER header.
|
||||
whitelist =
|
||||
# Optionally define more headers to sync other user attributes
|
||||
# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL``
|
||||
headers =
|
||||
```
|
||||
|
||||
* **enabled**: this is to toggle the feature on or off
|
||||
* **header_name**: this is the HTTP header name that passes the username or email address of the authenticated user to Grafana. Grafana will trust what ever username is contained in this header and automatically log the user in.
|
||||
* **header_property**: this tells Grafana whether the value in the header_name is a username or an email address. (In Grafana you can log in using your account username or account email)
|
||||
* **auto_sign_up**: If set to true, Grafana will automatically create user accounts in the Grafana DB if one does not exist. If set to false, users who do not exist in the GrafanaDB won’t be able to log in, even though their username and password are valid.
|
||||
* **ldap_sync_ttl**: When both auth.proxy and auth.ldap are enabled, user's organisation and role are synchronised from ldap after the http proxy authentication. You can force ldap re-synchronisation after `ldap_sync_ttl` minutes.
|
||||
* **whitelist**: Comma separated list of trusted authentication proxies IP.
|
||||
|
||||
With a fresh install of Grafana, using the above configuration for the authProxy feature, we can send a simple API call to list all users. The only user that will be present is the default “Admin” user that is added the first time Grafana starts up. As you can see all we need to do to authenticate the request is to provide the “X-WEBAUTH-USER” header.
|
||||
## Interacting with Grafana’s AuthProxy via curl
|
||||
|
||||
```bash
|
||||
curl -H "X-WEBAUTH-USER: admin" http://localhost:3000/api/users
|
||||
@ -71,7 +72,8 @@ I’ll demonstrate how to use Apache for authenticating users. In this example w
|
||||
|
||||
### Apache BasicAuth
|
||||
|
||||
In this example we use Apache as a reverseProxy in front of Grafana. Apache handles the Authentication of users before forwarding requests to the Grafana backend service.
|
||||
In this example we use Apache as a reverse proxy in front of Grafana. Apache handles the Authentication of users before forwarding requests to the Grafana backend service.
|
||||
|
||||
|
||||
#### Apache configuration
|
||||
|
||||
@ -116,38 +118,7 @@ In this example we use Apache as a reverseProxy in front of Grafana. Apache hand
|
||||
|
||||
* The last 3 lines are then just standard reverse proxy configuration to direct all authenticated requests to our Grafana server running on port 3000.
|
||||
|
||||
#### Grafana configuration
|
||||
|
||||
```bash
|
||||
############# Users ################
|
||||
[users]
|
||||
# disable user signup / registration
|
||||
allow_sign_up = false
|
||||
|
||||
# Set to true to automatically assign new users to the default organization (id 1)
|
||||
auto_assign_org = true
|
||||
|
||||
# Default role new users will be automatically assigned (if auto_assign_org above is set to true)
|
||||
auto_assign_org_role = Editor
|
||||
|
||||
|
||||
############ Auth Proxy ########
|
||||
[auth.proxy]
|
||||
enabled = true
|
||||
|
||||
# the Header name that contains the authenticated user.
|
||||
header_name = X-WEBAUTH-USER
|
||||
|
||||
# does the user authenticate against the proxy using a 'username' or an 'email'
|
||||
header_property = username
|
||||
|
||||
# automatically add the user to the system if they don't already exist.
|
||||
auto_sign_up = true
|
||||
```
|
||||
|
||||
#### Full walk through using Docker.
|
||||
|
||||
##### Grafana Container
|
||||
## Full walk through using Docker.
|
||||
|
||||
For this example, we use the official Grafana docker image available at [Docker Hub](https://hub.docker.com/r/grafana/grafana/)
|
||||
|
||||
@ -166,7 +137,8 @@ header_property = username
|
||||
auto_sign_up = true
|
||||
```
|
||||
|
||||
* Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We don't expose any ports for this container as it will only be connected to by our Apache container.
|
||||
Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We don't expose
|
||||
any ports for this container as it will only be connected to by our Apache container.
|
||||
|
||||
```bash
|
||||
docker run -i -v $(pwd)/grafana.ini:/etc/grafana/grafana.ini --name grafana grafana/grafana
|
179
docs/sources/auth/generic-oauth.md
Normal file
179
docs/sources/auth/generic-oauth.md
Normal file
@ -0,0 +1,179 @@
|
||||
+++
|
||||
title = "OAuth authentication"
|
||||
description = "Grafana OAuthentication Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "oauth"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Generic OAuth"
|
||||
identifier = "generic_oauth"
|
||||
parent = "authentication"
|
||||
weight = 3
|
||||
+++
|
||||
|
||||
# Generic OAuth Authentication
|
||||
|
||||
You can configure many different oauth2 authentication services with Grafana using the generic oauth2 feature. Below you
|
||||
can find examples using Okta, BitBucket, OneLogin and Azure.
|
||||
|
||||
This callback URL must match the full HTTP address that you use in your browser to access Grafana, but with the prefix path of `/login/generic_oauth`.
|
||||
|
||||
Example config:
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
enabled = true
|
||||
client_id = YOUR_APP_CLIENT_ID
|
||||
client_secret = YOUR_APP_CLIENT_SECRET
|
||||
scopes =
|
||||
auth_url =
|
||||
token_url =
|
||||
api_url =
|
||||
allowed_domains = mycompany.com mycompany.org
|
||||
allow_sign_up = true
|
||||
```
|
||||
|
||||
Set `api_url` to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information.
|
||||
|
||||
Grafana will attempt to determine the user's e-mail address by querying the OAuth provider as described below in the following order until an e-mail address is found:
|
||||
|
||||
1. Check for the presence of an e-mail address via the `email` field encoded in the OAuth `id_token` parameter.
|
||||
2. Check for the presence of an e-mail address in the `attributes` map encoded in the OAuth `id_token` parameter. By default Grafana will perform a lookup into the attributes map using the `email:primary` key, however, this is configurable and can be adjusted by using the `email_attribute_name` configuration option.
|
||||
3. Query the `/emails` endpoint of the OAuth provider's API (configured with `api_url`) and check for the presence of an e-mail address marked as a primary address.
|
||||
4. If no e-mail address is found in steps (1-3), then the e-mail address of the user is set to the empty string.
|
||||
|
||||
## Set up OAuth2 with Okta
|
||||
|
||||
First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https://<grafana domain>/` and set the Login redirect URIs to `https://<grafana domain>/login/generic_oauth`.
|
||||
|
||||
Finally set up the generic oauth module like this:
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
name = Okta
|
||||
enabled = true
|
||||
scopes = openid profile email
|
||||
client_id = <okta application Client ID>
|
||||
client_secret = <okta application Client Secret>
|
||||
auth_url = https://<okta domain>/oauth2/v1/authorize
|
||||
token_url = https://<okta domain>/oauth2/v1/token
|
||||
api_url = https://<okta domain>/oauth2/v1/userinfo
|
||||
```
|
||||
|
||||
## Set up OAuth2 with Bitbucket
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
name = BitBucket
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = <client id>
|
||||
client_secret = <client secret>
|
||||
scopes = account email
|
||||
auth_url = https://bitbucket.org/site/oauth2/authorize
|
||||
token_url = https://bitbucket.org/site/oauth2/access_token
|
||||
api_url = https://api.bitbucket.org/2.0/user
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
```
|
||||
|
||||
## Set up OAuth2 with OneLogin
|
||||
|
||||
1. Create a new Custom Connector with the following settings:
|
||||
- Name: Grafana
|
||||
- Sign On Method: OpenID Connect
|
||||
- Redirect URI: `https://<grafana domain>/login/generic_oauth`
|
||||
- Signing Algorithm: RS256
|
||||
- Login URL: `https://<grafana domain>/login/generic_oauth`
|
||||
|
||||
then:
|
||||
2. Add an App to the Grafana Connector:
|
||||
- Display Name: Grafana
|
||||
|
||||
then:
|
||||
3. Under the SSO tab on the Grafana App details page you'll find the Client ID and Client Secret.
|
||||
|
||||
Your OneLogin Domain will match the url you use to access OneLogin.
|
||||
|
||||
Configure Grafana as follows:
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
name = OneLogin
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = <client id>
|
||||
client_secret = <client secret>
|
||||
scopes = openid email name
|
||||
auth_url = https://<onelogin domain>.onelogin.com/oidc/auth
|
||||
token_url = https://<onelogin domain>.onelogin.com/oidc/token
|
||||
api_url = https://<onelogin domain>.onelogin.com/oidc/me
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
```
|
||||
|
||||
### Set up OAuth2 with Auth0
|
||||
|
||||
1. Create a new Client in Auth0
|
||||
- Name: Grafana
|
||||
- Type: Regular Web Application
|
||||
|
||||
2. Go to the Settings tab and set:
|
||||
- Allowed Callback URLs: `https://<grafana domain>/login/generic_oauth`
|
||||
|
||||
3. Click Save Changes, then use the values at the top of the page to configure Grafana:
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
name = Auth0
|
||||
client_id = <client id>
|
||||
client_secret = <client secret>
|
||||
scopes = openid profile email
|
||||
auth_url = https://<domain>/authorize
|
||||
token_url = https://<domain>/oauth/token
|
||||
api_url = https://<domain>/userinfo
|
||||
```
|
||||
|
||||
### Set up OAuth2 with Azure Active Directory
|
||||
|
||||
1. Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item.
|
||||
|
||||
2. Copy the "Directory ID", this is needed for setting URLs later
|
||||
|
||||
3. Click "App Registrations" and add a new application registration:
|
||||
- Name: Grafana
|
||||
- Application type: Web app / API
|
||||
- Sign-on URL: `https://<grafana domain>/login/generic_oauth`
|
||||
|
||||
4. Click the name of the new application to open the application details page.
|
||||
|
||||
5. Note down the "Application ID", this will be the OAuth client id.
|
||||
|
||||
6. Click "Settings", then click "Keys" and add a new entry under Passwords
|
||||
- Key Description: Grafana OAuth
|
||||
- Duration: Never Expires
|
||||
|
||||
7. Click Save then copy the key value, this will be the OAuth client secret.
|
||||
|
||||
8. Configure Grafana as follows:
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
name = Azure AD
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = <application id>
|
||||
client_secret = <key value>
|
||||
scopes = openid email name
|
||||
auth_url = https://login.microsoftonline.com/<directory id>/oauth2/authorize
|
||||
token_url = https://login.microsoftonline.com/<directory id>/oauth2/token
|
||||
api_url =
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
|
98
docs/sources/auth/github.md
Normal file
98
docs/sources/auth/github.md
Normal file
@ -0,0 +1,98 @@
|
||||
+++
|
||||
title = "Google OAuth2 Authentication"
|
||||
description = "Grafana OAuthentication Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "oauth"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "GitHub"
|
||||
identifier = "github_oauth2"
|
||||
parent = "authentication"
|
||||
weight = 4
|
||||
+++
|
||||
|
||||
# GitHub OAuth2 Authentication
|
||||
|
||||
To enable the GitHub OAuth2 you must register your application with GitHub. GitHub will generate a client ID and secret key for you to use.
|
||||
|
||||
## Configure GitHub OAuth application
|
||||
|
||||
You need to create a GitHub OAuth application (you find this under the GitHub
|
||||
settings page). When you create the application you will need to specify
|
||||
a callback URL. Specify this as callback:
|
||||
|
||||
```bash
|
||||
http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/github
|
||||
```
|
||||
|
||||
This callback URL must match the full HTTP address that you use in your
|
||||
browser to access Grafana, but with the prefix path of `/login/github`.
|
||||
When the GitHub OAuth application is created you will get a Client ID and a
|
||||
Client Secret. Specify these in the Grafana configuration file. For
|
||||
example:
|
||||
|
||||
## Enable GitHub in Grafana
|
||||
|
||||
```bash
|
||||
[auth.github]
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = YOUR_GITHUB_APP_CLIENT_ID
|
||||
client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
|
||||
scopes = user:email,read:org
|
||||
auth_url = https://github.com/login/oauth/authorize
|
||||
token_url = https://github.com/login/oauth/access_token
|
||||
api_url = https://api.github.com/user
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
```
|
||||
|
||||
Restart the Grafana back-end. You should now see a GitHub login button
|
||||
on the login page. You can now login or sign up with your GitHub
|
||||
accounts.
|
||||
|
||||
You may allow users to sign-up via GitHub authentication by setting the
|
||||
`allow_sign_up` option to `true`. When this option is set to `true`, any
|
||||
user successfully authenticating via GitHub authentication will be
|
||||
automatically signed up.
|
||||
|
||||
### team_ids
|
||||
|
||||
Require an active team membership for at least one of the given teams on
|
||||
GitHub. If the authenticated user isn't a member of at least one of the
|
||||
teams they will not be able to register or authenticate with your
|
||||
Grafana instance. For example:
|
||||
|
||||
```bash
|
||||
[auth.github]
|
||||
enabled = true
|
||||
client_id = YOUR_GITHUB_APP_CLIENT_ID
|
||||
client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
|
||||
scopes = user:email,read:org
|
||||
team_ids = 150,300
|
||||
auth_url = https://github.com/login/oauth/authorize
|
||||
token_url = https://github.com/login/oauth/access_token
|
||||
api_url = https://api.github.com/user
|
||||
allow_sign_up = true
|
||||
```
|
||||
|
||||
### allowed_organizations
|
||||
|
||||
Require an active organization membership for at least one of the given
|
||||
organizations on GitHub. If the authenticated user isn't a member of at least
|
||||
one of the organizations they will not be able to register or authenticate with
|
||||
your Grafana instance. For example
|
||||
|
||||
```bash
|
||||
[auth.github]
|
||||
enabled = true
|
||||
client_id = YOUR_GITHUB_APP_CLIENT_ID
|
||||
client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
|
||||
scopes = user:email,read:org
|
||||
auth_url = https://github.com/login/oauth/authorize
|
||||
token_url = https://github.com/login/oauth/access_token
|
||||
api_url = https://api.github.com/user
|
||||
allow_sign_up = true
|
||||
# space-delimited organization names
|
||||
allowed_organizations = github google
|
||||
```
|
||||
|
115
docs/sources/auth/gitlab.md
Normal file
115
docs/sources/auth/gitlab.md
Normal file
@ -0,0 +1,115 @@
|
||||
+++
|
||||
title = "Google OAuth2 Authentication"
|
||||
description = "Grafana OAuthentication Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "oauth"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "GitLab"
|
||||
identifier = "gitlab_oauth"
|
||||
parent = "authentication"
|
||||
weight = 5
|
||||
+++
|
||||
|
||||
# GitLab OAuth2 Authentication
|
||||
|
||||
To enable the GitLab OAuth2 you must register an application in GitLab. GitLab will generate a client ID and secret key for you to use.
|
||||
|
||||
## Create GitLab OAuth keys
|
||||
|
||||
You need to [create a GitLab OAuth application](https://docs.gitlab.com/ce/integration/oauth_provider.html).
|
||||
Choose a descriptive *Name*, and use the following *Redirect URI*:
|
||||
|
||||
```
|
||||
https://grafana.example.com/login/gitlab
|
||||
```
|
||||
|
||||
where `https://grafana.example.com` is the URL you use to connect to Grafana.
|
||||
Adjust it as needed if you don't use HTTPS or if you use a different port; for
|
||||
instance, if you access Grafana at `http://203.0.113.31:3000`, you should use
|
||||
|
||||
```
|
||||
http://203.0.113.31:3000/login/gitlab
|
||||
```
|
||||
|
||||
Finally, select *api* as the *Scope* and submit the form. Note that if you're
|
||||
not going to use GitLab groups for authorization (i.e. not setting
|
||||
`allowed_groups`, see below), you can select *read_user* instead of *api* as
|
||||
the *Scope*, thus giving a more restricted access to your GitLab API.
|
||||
|
||||
You'll get an *Application Id* and a *Secret* in return; we'll call them
|
||||
`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this
|
||||
section.
|
||||
|
||||
## Enable GitLab in Grafana
|
||||
|
||||
Add the following to your Grafana configuration file to enable GitLab
|
||||
authentication:
|
||||
|
||||
```bash
|
||||
[auth.gitlab]
|
||||
enabled = false
|
||||
allow_sign_up = false
|
||||
client_id = GITLAB_APPLICATION_ID
|
||||
client_secret = GITLAB_SECRET
|
||||
scopes = api
|
||||
auth_url = https://gitlab.com/oauth/authorize
|
||||
token_url = https://gitlab.com/oauth/token
|
||||
api_url = https://gitlab.com/api/v4
|
||||
allowed_groups =
|
||||
```
|
||||
|
||||
Restart the Grafana backend for your changes to take effect.
|
||||
|
||||
If you use your own instance of GitLab instead of `gitlab.com`, adjust
|
||||
`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com`
|
||||
hostname with your own.
|
||||
|
||||
With `allow_sign_up` set to `false`, only existing users will be able to login
|
||||
using their GitLab account, but with `allow_sign_up` set to `true`, *any* user
|
||||
who can authenticate on GitLab will be able to login on your Grafana instance;
|
||||
if you use the public `gitlab.com`, it means anyone in the world would be able
|
||||
to login on your Grafana instance.
|
||||
|
||||
You can can however limit access to only members of a given group or list of
|
||||
groups by setting the `allowed_groups` option.
|
||||
|
||||
### allowed_groups
|
||||
|
||||
To limit access to authenticated users that are members of one or more [GitLab
|
||||
groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups`
|
||||
to a comma- or space-separated list of groups. For instance, if you want to
|
||||
only give access to members of the `example` group, set
|
||||
|
||||
|
||||
```ini
|
||||
allowed_groups = example
|
||||
```
|
||||
|
||||
If you want to also give access to members of the subgroup `bar`, which is in
|
||||
the group `foo`, set
|
||||
|
||||
```ini
|
||||
allowed_groups = example, foo/bar
|
||||
```
|
||||
|
||||
Note that in GitLab, the group or subgroup name doesn't always match its
|
||||
display name, especially if the display name contains spaces or special
|
||||
characters. Make sure you always use the group or subgroup name as it appears
|
||||
in the URL of the group or subgroup.
|
||||
|
||||
Here's a complete example with `alloed_sign_up` enabled, and access limited to
|
||||
the `example` and `foo/bar` groups:
|
||||
|
||||
```ini
|
||||
[auth.gitlab]
|
||||
enabled = false
|
||||
allow_sign_up = true
|
||||
client_id = GITLAB_APPLICATION_ID
|
||||
client_secret = GITLAB_SECRET
|
||||
scopes = api
|
||||
auth_url = https://gitlab.com/oauth/authorize
|
||||
token_url = https://gitlab.com/oauth/token
|
||||
api_url = https://gitlab.com/api/v4
|
||||
allowed_groups = example, foo/bar
|
||||
```
|
||||
|
55
docs/sources/auth/google.md
Normal file
55
docs/sources/auth/google.md
Normal file
@ -0,0 +1,55 @@
|
||||
+++
|
||||
title = "Google OAuth2 Authentication"
|
||||
description = "Grafana OAuthentication Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "oauth"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Google"
|
||||
identifier = "ggogle_oauth2"
|
||||
parent = "authentication"
|
||||
weight = 3
|
||||
+++
|
||||
|
||||
# Google OAuth2 Authentication
|
||||
|
||||
To enable the Google OAuth2 you must register your application with Google. Google will generate a client ID and secret key for you to use.
|
||||
|
||||
## Create Google OAuth keys
|
||||
|
||||
First, you need to create a Google OAuth Client:
|
||||
|
||||
1. Go to https://console.developers.google.com/apis/credentials
|
||||
2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the menu that drops down
|
||||
3. Enter the following:
|
||||
- Application Type: Web Application
|
||||
- Name: Grafana
|
||||
- Authorized Javascript Origins: https://grafana.mycompany.com
|
||||
- Authorized Redirect URLs: https://grafana.mycompany.com/login/google
|
||||
- Replace https://grafana.mycompany.com with the URL of your Grafana instance.
|
||||
4. Click Create
|
||||
5. Copy the Client ID and Client Secret from the 'OAuth Client' modal
|
||||
|
||||
## Enable Google OAuth in Grafana
|
||||
|
||||
Specify the Client ID and Secret in the [Grafana configuration file]({{< relref "installation/configuration.md#config-file-locations" >}}). For example:
|
||||
|
||||
```bash
|
||||
[auth.google]
|
||||
enabled = true
|
||||
client_id = CLIENT_ID
|
||||
client_secret = CLIENT_SECRET
|
||||
scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
|
||||
auth_url = https://accounts.google.com/o/oauth2/auth
|
||||
token_url = https://accounts.google.com/o/oauth2/token
|
||||
allowed_domains = mycompany.com mycompany.org
|
||||
allow_sign_up = true
|
||||
```
|
||||
|
||||
Restart the Grafana back-end. You should now see a Google login button
|
||||
on the login page. You can now login or sign up with your Google
|
||||
accounts. The `allowed_domains` option is optional, and domains were separated by space.
|
||||
|
||||
You may allow users to sign-up via Google authentication by setting the
|
||||
`allow_sign_up` option to `true`. When this option is set to `true`, any
|
||||
user successfully authenticating via Google authentication will be
|
||||
automatically signed up.
|
12
docs/sources/auth/index.md
Normal file
12
docs/sources/auth/index.md
Normal file
@ -0,0 +1,12 @@
|
||||
+++
|
||||
title = "Authentication"
|
||||
description = "Authentication"
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Authentication"
|
||||
identifier = "authentication"
|
||||
parent = "admin"
|
||||
weight = 3
|
||||
+++
|
||||
|
||||
|
@ -4,25 +4,37 @@ description = "Grafana LDAP Authentication Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "ldap"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "LDAP Authentication"
|
||||
name = "LDAP"
|
||||
identifier = "ldap"
|
||||
parent = "admin"
|
||||
parent = "authentication"
|
||||
weight = 2
|
||||
+++
|
||||
|
||||
# LDAP Authentication
|
||||
# LDAP
|
||||
|
||||
Grafana (2.1 and newer) ships with a strong LDAP integration feature. The LDAP integration in Grafana allows your
|
||||
Grafana users to login with their LDAP credentials. You can also specify mappings between LDAP
|
||||
group memberships and Grafana Organization user roles.
|
||||
The LDAP integration in Grafana allows your Grafana users to login with their LDAP credentials. You can also specify mappings between LDAP
|
||||
group memberships and Grafana Organization user roles. Below we detail grafana.ini config file
|
||||
settings and ldap.toml config file options.
|
||||
|
||||
## Configuration
|
||||
You turn on LDAP in the [main config file]({{< relref "configuration.md#auth-ldap" >}}) as well as specify the path to the LDAP
|
||||
## Enable LDAP
|
||||
|
||||
You turn on LDAP in the [main config file]({{< relref "installation/configuration.md" >}}) as well as specify the path to the LDAP
|
||||
specific configuration file (default: `/etc/grafana/ldap.toml`).
|
||||
|
||||
### Example config
|
||||
```bash
|
||||
[auth.ldap]
|
||||
# Set to `true` to enable LDAP integration (default: `false`)
|
||||
enabled = true
|
||||
# Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`)
|
||||
config_file = /etc/grafana/ldap.toml`
|
||||
# Allow sign up should almost always be true (default) to allow new Grafana users to be created (if ldap authentication is ok). If set to
|
||||
# false only pre-existing Grafana users will be able to login (if ldap authentication is ok).
|
||||
allow_sign_up = true
|
||||
```
|
||||
|
||||
```toml
|
||||
## LDAP Configuration
|
||||
|
||||
```bash
|
||||
# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
|
||||
# [log]
|
||||
# filters = ldap:debug
|
||||
@ -119,7 +131,7 @@ The search filter and search bases settings are still needed to perform the LDAP
|
||||
## POSIX schema (no memberOf attribute)
|
||||
If your ldap server does not support the memberOf attribute add these options:
|
||||
|
||||
```toml
|
||||
```bash
|
||||
## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
|
||||
group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
|
||||
## An array of the base DNs to search through for groups. Typically uses ou=groups
|
87
docs/sources/auth/overview.md
Normal file
87
docs/sources/auth/overview.md
Normal file
@ -0,0 +1,87 @@
|
||||
+++
|
||||
title = "Overview"
|
||||
description = "Overview for auth"
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Overview"
|
||||
identifier = "overview-auth"
|
||||
parent = "authentication"
|
||||
weight = 1
|
||||
+++
|
||||
|
||||
# User Authentication Overview
|
||||
|
||||
Grafana provides many ways to authenticate users. Some authentication integrations also enable syncing user
|
||||
permissions and org memberships.
|
||||
|
||||
## OAuth Integrations
|
||||
|
||||
- [Google OAuth]({{< relref "auth/google.md" >}})
|
||||
- [GitHub OAuth]({{< relref "auth/github.md" >}})
|
||||
- [Gitlab OAuth]({{< relref "auth/gitlab.md" >}})
|
||||
- [Generic OAuth]({{< relref "auth/generic-oauth.md" >}}) (Okta2, BitBucket, Azure, OneLogin, Auth0)
|
||||
|
||||
## LDAP integrations
|
||||
|
||||
- [LDAP Authentication]({{< relref "auth/ldap.md" >}}) (OpenLDAP, ActiveDirectory, etc)
|
||||
|
||||
## Auth proxy
|
||||
|
||||
- [Auth Proxy]({{< relref "auth/auth-proxy.md" >}}) If you want to handle authentication outside Grafana using a reverse
|
||||
proxy.
|
||||
|
||||
## Grafana Auth
|
||||
|
||||
Grafana of course has a built in user authentication system with password authenticaten enabled by default. You can
|
||||
disable authentication by enabling anonymous access. You can also hide login form and only allow login through an auth
|
||||
provider (listed above). There is also options for allowing self sign up.
|
||||
|
||||
### Anonymous authenticaten
|
||||
|
||||
You can make Grafana accessible without any login required by enabling anonymous access in the configuration file.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
[auth.anonymous]
|
||||
enabled = true
|
||||
|
||||
# Organization name that should be used for unauthenticated users
|
||||
org_name = Main Org.
|
||||
|
||||
# Role for unauthenticated users, other valid values are `Editor` and `Admin`
|
||||
org_role = Viewer
|
||||
```
|
||||
|
||||
If you change your organization name in the Grafana UI this setting needs to be updated to match the new name.
|
||||
|
||||
### Basic authentication
|
||||
|
||||
Basic auth is enabled by default and works with the built in Grafana user password authentication system and LDAP
|
||||
authenticaten integration.
|
||||
|
||||
To disable basic auth:
|
||||
|
||||
```bash
|
||||
[auth.basic]
|
||||
enabled = false
|
||||
```
|
||||
|
||||
### Disable login form
|
||||
|
||||
You can hide the Grafana login form using the below configuration settings.
|
||||
|
||||
```bash
|
||||
[auth]
|
||||
disable_login_form ⁼ true
|
||||
```
|
||||
|
||||
### Hide sign-out menu
|
||||
|
||||
Set to the option detailed below to true to hide sign-out menu link. Useful if you use an auth proxy.
|
||||
|
||||
```bash
|
||||
[auth]
|
||||
disable_signout_menu = true
|
||||
```
|
||||
|
@ -6,7 +6,7 @@ type = "docs"
|
||||
[menu.docs]
|
||||
name = "Microsoft SQL Server"
|
||||
parent = "datasources"
|
||||
weight = 7
|
||||
weight = 8
|
||||
+++
|
||||
|
||||
# Using Microsoft SQL Server in Grafana
|
||||
@ -33,6 +33,24 @@ Name | Description
|
||||
*User* | Database user's login/username
|
||||
*Password* | Database user's password
|
||||
|
||||
### Min time interval
|
||||
|
||||
A lower limit for the [$__interval](/reference/templating/#the-interval-variable) and [$__interval_ms](/reference/templating/#the-interval-ms-variable) variables.
|
||||
Recommended to be set to write frequency, for example `1m` if your data is written every minute.
|
||||
This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
|
||||
number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
|
||||
|
||||
Identifier | Description
|
||||
------------ | -------------
|
||||
`y` | year
|
||||
`M` | month
|
||||
`w` | week
|
||||
`d` | day
|
||||
`h` | hour
|
||||
`m` | minute
|
||||
`s` | second
|
||||
`ms` | millisecond
|
||||
|
||||
### Database User Permissions (Important!)
|
||||
|
||||
The database user you specify when you add the data source should only be granted SELECT permissions on
|
||||
@ -156,6 +174,8 @@ The resulting table panel:
|
||||
If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, the name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric.
|
||||
If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
|
||||
|
||||
Resultsets of time series queries need to be sorted by time.
|
||||
|
||||
**Example database table:**
|
||||
|
||||
```sql
|
||||
|
@ -36,6 +36,24 @@ Name | Description
|
||||
*User* | Database user's login/username
|
||||
*Password* | Database user's password
|
||||
|
||||
### Min time interval
|
||||
|
||||
A lower limit for the [$__interval](/reference/templating/#the-interval-variable) and [$__interval_ms](/reference/templating/#the-interval-ms-variable) variables.
|
||||
Recommended to be set to write frequency, for example `1m` if your data is written every minute.
|
||||
This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
|
||||
number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
|
||||
|
||||
Identifier | Description
|
||||
------------ | -------------
|
||||
`y` | year
|
||||
`M` | month
|
||||
`w` | week
|
||||
`d` | day
|
||||
`h` | hour
|
||||
`m` | minute
|
||||
`s` | second
|
||||
`ms` | millisecond
|
||||
|
||||
### Database User Permissions (Important!)
|
||||
|
||||
The database user you specify when you add the data source should only be granted SELECT permissions on
|
||||
@ -111,6 +129,8 @@ Any column except `time` and `metric` is treated as a value column.
|
||||
You may return a column named `metric` that is used as metric name for the value column.
|
||||
If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
|
||||
|
||||
Resultsets of time series queries need to be sorted by time.
|
||||
|
||||
**Example with `metric` column:**
|
||||
|
||||
```sql
|
||||
|
@ -31,7 +31,26 @@ Name | Description
|
||||
*User* | Database user's login/username
|
||||
*Password* | Database user's password
|
||||
*SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
|
||||
*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time (only available in Grafana 5.3+).
|
||||
*Version* | This option determines which functions are available in the query builder (only available in Grafana 5.3+).
|
||||
*TimescaleDB* | TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use `time_bucket` in the `$__timeGroup` macro and display TimescaleDB specific aggregate functions in the query builder (only available in Grafana 5.3+).
|
||||
|
||||
### Min time interval
|
||||
|
||||
A lower limit for the [$__interval](/reference/templating/#the-interval-variable) and [$__interval_ms](/reference/templating/#the-interval-ms-variable) variables.
|
||||
Recommended to be set to write frequency, for example `1m` if your data is written every minute.
|
||||
This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
|
||||
number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
|
||||
|
||||
Identifier | Description
|
||||
------------ | -------------
|
||||
`y` | year
|
||||
`M` | month
|
||||
`w` | week
|
||||
`d` | day
|
||||
`h` | hour
|
||||
`m` | minute
|
||||
`s` | second
|
||||
`ms` | millisecond
|
||||
|
||||
### Database User Permissions (Important!)
|
||||
|
||||
@ -110,6 +129,8 @@ Any column except `time` and `metric` is treated as a value column.
|
||||
You may return a column named `metric` that is used as metric name for the value column.
|
||||
If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+).
|
||||
|
||||
Resultsets of time series queries need to be sorted by time.
|
||||
|
||||
**Example with `metric` column:**
|
||||
|
||||
```sql
|
||||
@ -292,5 +313,6 @@ datasources:
|
||||
password: "Password!"
|
||||
jsonData:
|
||||
sslmode: "disable" # disable/require/verify-ca/verify-full
|
||||
postgresVersion: 903 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10
|
||||
timescaledb: false
|
||||
```
|
||||
|
@ -13,7 +13,35 @@ weight = 1
|
||||
|
||||
# Getting started
|
||||
|
||||
This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running and have added at least one [Data Source](/features/datasources/).
|
||||
This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running. If not please read the [installation guide](/installation/).
|
||||
|
||||
## Logging in for the first time
|
||||
|
||||
To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
|
||||
|
||||
There you will see the login page. Default username is admin and default password is admin. When you log in for the first time you will be asked to change your password. We strongly encourage you to
|
||||
follow Grafana’s best practices and change the default administrator password. You can later go to user preferences and change your user name.
|
||||
|
||||
|
||||
## How to add a data source
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v52/sidemenu-datasource.png" max-width="250px" class="docs-image--right docs-image--no-shadow">}}
|
||||
|
||||
Before you create your first dashboard you need to add your data source.
|
||||
|
||||
First move your cursor to the cog on the side menu which will show you the configuration menu. If the side menu is not visible click the Grafana icon in the upper left corner. The first item on the configuration menu is data sources, click on that and you'll be taken to the data sources page where you can add and edit data sources. You can also simply click the cog.
|
||||
|
||||
|
||||
Click Add data source and you will come to the settings page of your new data source.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v52/add-datasource.png" max-width="700px" class="docs-image--no-shadow">}}
|
||||
|
||||
First, give the data source a Name and then select which Type of data source you'll want to create, see [Supported data sources](/features/datasources/#supported-data-sources/) for more information and how to configure your data source.
|
||||
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v52/datasource-settings.png" max-width="700px" class="docs-image--no-shadow">}}
|
||||
|
||||
After you have configuered your data source you are ready to save and test.
|
||||
|
||||
## Beginner guides
|
||||
|
||||
|
18
docs/sources/guides/whats-new-in-v5-3.md
Normal file
18
docs/sources/guides/whats-new-in-v5-3.md
Normal file
@ -0,0 +1,18 @@
|
||||
+++
|
||||
title = "What's New in Grafana v5.3"
|
||||
description = "Feature & improvement highlights for Grafana v5.3"
|
||||
keywords = ["grafana", "new", "documentation", "5.3"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Version 5.3"
|
||||
identifier = "v5.3"
|
||||
parent = "whatsnew"
|
||||
weight = -9
|
||||
+++
|
||||
|
||||
# What's New in Grafana v5.3
|
||||
|
||||
## 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.
|
@ -50,6 +50,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
@ -86,6 +87,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"dashboardId": 1,
|
||||
@ -146,6 +148,7 @@ JSON Body Schema:
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"alertId": 1,
|
||||
"state": "Paused",
|
||||
@ -177,6 +180,7 @@ JSON Body Schema:
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"state": "Paused",
|
||||
"message": "alert paused",
|
||||
@ -204,14 +208,21 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
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"
|
||||
}
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Team A",
|
||||
"type": "email",
|
||||
"isDefault": false,
|
||||
"sendReminder": false,
|
||||
"settings": {
|
||||
"addresses": "carl@grafana.com;dev@grafana.com"
|
||||
},
|
||||
"created": "2018-04-23T14:44:09+02:00",
|
||||
"updated": "2018-08-20T15:47:49+02:00"
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
## Create alert notification
|
||||
@ -232,6 +243,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
"name": "new alert notification", //Required
|
||||
"type": "email", //Required
|
||||
"isDefault": false,
|
||||
"sendReminder": false,
|
||||
"settings": {
|
||||
"addresses": "carl@grafana.com;dev@grafana.com"
|
||||
}
|
||||
@ -243,14 +255,18 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```http
|
||||
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"
|
||||
"sendReminder": false,
|
||||
"settings": {
|
||||
"addresses": "carl@grafana.com;dev@grafana.com"
|
||||
},
|
||||
"created": "2018-04-23T14:44:09+02:00",
|
||||
"updated": "2018-08-20T15:47:49+02:00"
|
||||
}
|
||||
```
|
||||
|
||||
@ -271,6 +287,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
"name": "new alert notification", //Required
|
||||
"type": "email", //Required
|
||||
"isDefault": false,
|
||||
"sendReminder": true,
|
||||
"frequency": "15m",
|
||||
"settings": {
|
||||
"addresses: "carl@grafana.com;dev@grafana.com"
|
||||
}
|
||||
@ -282,12 +300,17 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```http
|
||||
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"} }
|
||||
"sendReminder": true,
|
||||
"frequency": "15m",
|
||||
"settings": {
|
||||
"addresses": "carl@grafana.com;dev@grafana.com"
|
||||
},
|
||||
"created": "2017-01-01 12:34",
|
||||
"updated": "2017-01-01 12:34"
|
||||
}
|
||||
@ -311,6 +334,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "Notification deleted"
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ keywords = ["grafana", "http", "documentation", "api", "authentication"]
|
||||
aliases = ["/http_api/authentication/"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Authentication"
|
||||
name = "Authentication HTTP API"
|
||||
parent = "http_api"
|
||||
+++
|
||||
|
||||
|
@ -266,7 +266,8 @@ The number of days the keep me logged in / remember me cookie lasts.
|
||||
|
||||
### secret_key
|
||||
|
||||
Used for signing keep me logged in / remember me cookies.
|
||||
Used for signing some datasource settings like secrets and passwords. Cannot be changed without requiring an update
|
||||
to datasource settings to re-encode them.
|
||||
|
||||
### disable_gravatar
|
||||
|
||||
@ -321,470 +322,17 @@ Defaults to `false`.
|
||||
|
||||
## [auth]
|
||||
|
||||
### disable_login_form
|
||||
|
||||
Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false.
|
||||
|
||||
### disable_signout_menu
|
||||
|
||||
Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false.
|
||||
|
||||
<hr>
|
||||
|
||||
## [auth.anonymous]
|
||||
|
||||
### enabled
|
||||
|
||||
Set to `true` to enable anonymous access. Defaults to `false`
|
||||
|
||||
### org_name
|
||||
|
||||
Set the organization name that should be used for anonymous users. If
|
||||
you change your organization name in the Grafana UI this setting needs
|
||||
to be updated to match the new name.
|
||||
|
||||
### org_role
|
||||
|
||||
Specify role for anonymous users. Defaults to `Viewer`, other valid
|
||||
options are `Editor` and `Admin`.
|
||||
|
||||
## [auth.github]
|
||||
|
||||
You need to create a GitHub OAuth application (you find this under the GitHub
|
||||
settings page). When you create the application you will need to specify
|
||||
a callback URL. Specify this as callback:
|
||||
|
||||
```bash
|
||||
http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/github
|
||||
```
|
||||
|
||||
This callback URL must match the full HTTP address that you use in your
|
||||
browser to access Grafana, but with the prefix path of `/login/github`.
|
||||
When the GitHub OAuth application is created you will get a Client ID and a
|
||||
Client Secret. Specify these in the Grafana configuration file. For
|
||||
example:
|
||||
|
||||
```bash
|
||||
[auth.github]
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = YOUR_GITHUB_APP_CLIENT_ID
|
||||
client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
|
||||
scopes = user:email,read:org
|
||||
auth_url = https://github.com/login/oauth/authorize
|
||||
token_url = https://github.com/login/oauth/access_token
|
||||
api_url = https://api.github.com/user
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
```
|
||||
|
||||
Restart the Grafana back-end. You should now see a GitHub login button
|
||||
on the login page. You can now login or sign up with your GitHub
|
||||
accounts.
|
||||
|
||||
You may allow users to sign-up via GitHub authentication by setting the
|
||||
`allow_sign_up` option to `true`. When this option is set to `true`, any
|
||||
user successfully authenticating via GitHub authentication will be
|
||||
automatically signed up.
|
||||
|
||||
### team_ids
|
||||
|
||||
Require an active team membership for at least one of the given teams on
|
||||
GitHub. If the authenticated user isn't a member of at least one of the
|
||||
teams they will not be able to register or authenticate with your
|
||||
Grafana instance. For example:
|
||||
|
||||
```bash
|
||||
[auth.github]
|
||||
enabled = true
|
||||
client_id = YOUR_GITHUB_APP_CLIENT_ID
|
||||
client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
|
||||
scopes = user:email,read:org
|
||||
team_ids = 150,300
|
||||
auth_url = https://github.com/login/oauth/authorize
|
||||
token_url = https://github.com/login/oauth/access_token
|
||||
api_url = https://api.github.com/user
|
||||
allow_sign_up = true
|
||||
```
|
||||
|
||||
### allowed_organizations
|
||||
|
||||
Require an active organization membership for at least one of the given
|
||||
organizations on GitHub. If the authenticated user isn't a member of at least
|
||||
one of the organizations they will not be able to register or authenticate with
|
||||
your Grafana instance. For example
|
||||
|
||||
```bash
|
||||
[auth.github]
|
||||
enabled = true
|
||||
client_id = YOUR_GITHUB_APP_CLIENT_ID
|
||||
client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
|
||||
scopes = user:email,read:org
|
||||
auth_url = https://github.com/login/oauth/authorize
|
||||
token_url = https://github.com/login/oauth/access_token
|
||||
api_url = https://api.github.com/user
|
||||
allow_sign_up = true
|
||||
# space-delimited organization names
|
||||
allowed_organizations = github google
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
## [auth.gitlab]
|
||||
|
||||
> Only available in Grafana v5.3+.
|
||||
|
||||
You need to [create a GitLab OAuth
|
||||
application](https://docs.gitlab.com/ce/integration/oauth_provider.html).
|
||||
Choose a descriptive *Name*, and use the following *Redirect URI*:
|
||||
|
||||
```
|
||||
https://grafana.example.com/login/gitlab
|
||||
```
|
||||
|
||||
where `https://grafana.example.com` is the URL you use to connect to Grafana.
|
||||
Adjust it as needed if you don't use HTTPS or if you use a different port; for
|
||||
instance, if you access Grafana at `http://203.0.113.31:3000`, you should use
|
||||
|
||||
```
|
||||
http://203.0.113.31:3000/login/gitlab
|
||||
```
|
||||
|
||||
Finally, select *api* as the *Scope* and submit the form. Note that if you're
|
||||
not going to use GitLab groups for authorization (i.e. not setting
|
||||
`allowed_groups`, see below), you can select *read_user* instead of *api* as
|
||||
the *Scope*, thus giving a more restricted access to your GitLab API.
|
||||
|
||||
You'll get an *Application Id* and a *Secret* in return; we'll call them
|
||||
`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this
|
||||
section.
|
||||
|
||||
Add the following to your Grafana configuration file to enable GitLab
|
||||
authentication:
|
||||
|
||||
```ini
|
||||
[auth.gitlab]
|
||||
enabled = false
|
||||
allow_sign_up = false
|
||||
client_id = GITLAB_APPLICATION_ID
|
||||
client_secret = GITLAB_SECRET
|
||||
scopes = api
|
||||
auth_url = https://gitlab.com/oauth/authorize
|
||||
token_url = https://gitlab.com/oauth/token
|
||||
api_url = https://gitlab.com/api/v4
|
||||
allowed_groups =
|
||||
```
|
||||
|
||||
Restart the Grafana backend for your changes to take effect.
|
||||
|
||||
If you use your own instance of GitLab instead of `gitlab.com`, adjust
|
||||
`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com`
|
||||
hostname with your own.
|
||||
|
||||
With `allow_sign_up` set to `false`, only existing users will be able to login
|
||||
using their GitLab account, but with `allow_sign_up` set to `true`, *any* user
|
||||
who can authenticate on GitLab will be able to login on your Grafana instance;
|
||||
if you use the public `gitlab.com`, it means anyone in the world would be able
|
||||
to login on your Grafana instance.
|
||||
|
||||
You can can however limit access to only members of a given group or list of
|
||||
groups by setting the `allowed_groups` option.
|
||||
|
||||
### allowed_groups
|
||||
|
||||
To limit access to authenticated users that are members of one or more [GitLab
|
||||
groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups`
|
||||
to a comma- or space-separated list of groups. For instance, if you want to
|
||||
only give access to members of the `example` group, set
|
||||
|
||||
|
||||
```ini
|
||||
allowed_groups = example
|
||||
```
|
||||
|
||||
If you want to also give access to members of the subgroup `bar`, which is in
|
||||
the group `foo`, set
|
||||
|
||||
```ini
|
||||
allowed_groups = example, foo/bar
|
||||
```
|
||||
|
||||
Note that in GitLab, the group or subgroup name doesn't always match its
|
||||
display name, especially if the display name contains spaces or special
|
||||
characters. Make sure you always use the group or subgroup name as it appears
|
||||
in the URL of the group or subgroup.
|
||||
|
||||
Here's a complete example with `alloed_sign_up` enabled, and access limited to
|
||||
the `example` and `foo/bar` groups:
|
||||
|
||||
```ini
|
||||
[auth.gitlab]
|
||||
enabled = false
|
||||
allow_sign_up = true
|
||||
client_id = GITLAB_APPLICATION_ID
|
||||
client_secret = GITLAB_SECRET
|
||||
scopes = api
|
||||
auth_url = https://gitlab.com/oauth/authorize
|
||||
token_url = https://gitlab.com/oauth/token
|
||||
api_url = https://gitlab.com/api/v4
|
||||
allowed_groups = example, foo/bar
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
## [auth.google]
|
||||
|
||||
First, you need to create a Google OAuth Client:
|
||||
|
||||
1. Go to https://console.developers.google.com/apis/credentials
|
||||
|
||||
2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the
|
||||
menu that drops down
|
||||
|
||||
3. Enter the following:
|
||||
|
||||
- Application Type: Web Application
|
||||
- Name: Grafana
|
||||
- Authorized Javascript Origins: https://grafana.mycompany.com
|
||||
- Authorized Redirect URLs: https://grafana.mycompany.com/login/google
|
||||
|
||||
Replace https://grafana.mycompany.com with the URL of your Grafana instance.
|
||||
|
||||
4. Click Create
|
||||
|
||||
5. Copy the Client ID and Client Secret from the 'OAuth Client' modal
|
||||
|
||||
Specify the Client ID and Secret in the Grafana configuration file. For example:
|
||||
|
||||
```bash
|
||||
[auth.google]
|
||||
enabled = true
|
||||
client_id = CLIENT_ID
|
||||
client_secret = CLIENT_SECRET
|
||||
scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
|
||||
auth_url = https://accounts.google.com/o/oauth2/auth
|
||||
token_url = https://accounts.google.com/o/oauth2/token
|
||||
allowed_domains = mycompany.com mycompany.org
|
||||
allow_sign_up = true
|
||||
```
|
||||
|
||||
Restart the Grafana back-end. You should now see a Google login button
|
||||
on the login page. You can now login or sign up with your Google
|
||||
accounts. The `allowed_domains` option is optional, and domains were separated by space.
|
||||
|
||||
You may allow users to sign-up via Google authentication by setting the
|
||||
`allow_sign_up` option to `true`. When this option is set to `true`, any
|
||||
user successfully authenticating via Google authentication will be
|
||||
automatically signed up.
|
||||
|
||||
## [auth.generic_oauth]
|
||||
|
||||
This option could be used if have your own oauth service.
|
||||
|
||||
This callback URL must match the full HTTP address that you use in your
|
||||
browser to access Grafana, but with the prefix path of `/login/generic_oauth`.
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
enabled = true
|
||||
client_id = YOUR_APP_CLIENT_ID
|
||||
client_secret = YOUR_APP_CLIENT_SECRET
|
||||
scopes =
|
||||
auth_url =
|
||||
token_url =
|
||||
api_url =
|
||||
allowed_domains = mycompany.com mycompany.org
|
||||
allow_sign_up = true
|
||||
```
|
||||
|
||||
Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information.
|
||||
|
||||
### Set up oauth2 with Okta
|
||||
|
||||
First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https://<grafana domain>/` and set the Login redirect URIs to `https://<grafana domain>/login/generic_oauth`.
|
||||
|
||||
Finally set up the generic oauth module like this:
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
name = Okta
|
||||
enabled = true
|
||||
scopes = openid profile email
|
||||
client_id = <okta application Client ID>
|
||||
client_secret = <okta application Client Secret>
|
||||
auth_url = https://<okta domain>/oauth2/v1/authorize
|
||||
token_url = https://<okta domain>/oauth2/v1/token
|
||||
api_url = https://<okta domain>/oauth2/v1/userinfo
|
||||
```
|
||||
|
||||
### Set up oauth2 with Bitbucket
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
name = BitBucket
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = <client id>
|
||||
client_secret = <client secret>
|
||||
scopes = account email
|
||||
auth_url = https://bitbucket.org/site/oauth2/authorize
|
||||
token_url = https://bitbucket.org/site/oauth2/access_token
|
||||
api_url = https://api.bitbucket.org/2.0/user
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
```
|
||||
|
||||
### Set up oauth2 with OneLogin
|
||||
|
||||
1. Create a new Custom Connector with the following settings:
|
||||
- Name: Grafana
|
||||
- Sign On Method: OpenID Connect
|
||||
- Redirect URI: `https://<grafana domain>/login/generic_oauth`
|
||||
- Signing Algorithm: RS256
|
||||
- Login URL: `https://<grafana domain>/login/generic_oauth`
|
||||
|
||||
then:
|
||||
2. Add an App to the Grafana Connector:
|
||||
- Display Name: Grafana
|
||||
|
||||
then:
|
||||
3. Under the SSO tab on the Grafana App details page you'll find the Client ID and Client Secret.
|
||||
|
||||
Your OneLogin Domain will match the url you use to access OneLogin.
|
||||
|
||||
Configure Grafana as follows:
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
name = OneLogin
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = <client id>
|
||||
client_secret = <client secret>
|
||||
scopes = openid email name
|
||||
auth_url = https://<onelogin domain>.onelogin.com/oidc/auth
|
||||
token_url = https://<onelogin domain>.onelogin.com/oidc/token
|
||||
api_url = https://<onelogin domain>.onelogin.com/oidc/me
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
```
|
||||
|
||||
### Set up oauth2 with Auth0
|
||||
|
||||
1. Create a new Client in Auth0
|
||||
- Name: Grafana
|
||||
- Type: Regular Web Application
|
||||
|
||||
2. Go to the Settings tab and set:
|
||||
- Allowed Callback URLs: `https://<grafana domain>/login/generic_oauth`
|
||||
|
||||
3. Click Save Changes, then use the values at the top of the page to configure Grafana:
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
name = Auth0
|
||||
client_id = <client id>
|
||||
client_secret = <client secret>
|
||||
scopes = openid profile email
|
||||
auth_url = https://<domain>/authorize
|
||||
token_url = https://<domain>/oauth/token
|
||||
api_url = https://<domain>/userinfo
|
||||
```
|
||||
|
||||
### Set up oauth2 with Azure Active Directory
|
||||
|
||||
1. Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item.
|
||||
|
||||
2. Copy the "Directory ID", this is needed for setting URLs later
|
||||
|
||||
3. Click "App Registrations" and add a new application registration:
|
||||
- Name: Grafana
|
||||
- Application type: Web app / API
|
||||
- Sign-on URL: `https://<grafana domain>/login/generic_oauth`
|
||||
|
||||
4. Click the name of the new application to open the application details page.
|
||||
|
||||
5. Note down the "Application ID", this will be the OAuth client id.
|
||||
|
||||
6. Click "Settings", then click "Keys" and add a new entry under Passwords
|
||||
- Key Description: Grafana OAuth
|
||||
- Duration: Never Expires
|
||||
|
||||
7. Click Save then copy the key value, this will be the OAuth client secret.
|
||||
|
||||
8. Configure Grafana as follows:
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
name = Azure AD
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = <application id>
|
||||
client_secret = <key value>
|
||||
scopes = openid email name
|
||||
auth_url = https://login.microsoftonline.com/<directory id>/oauth2/authorize
|
||||
token_url = https://login.microsoftonline.com/<directory id>/oauth2/token
|
||||
api_url =
|
||||
team_ids =
|
||||
allowed_organizations =
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
## [auth.basic]
|
||||
### enabled
|
||||
When enabled is `true` (default) the http api will accept basic authentication.
|
||||
|
||||
<hr>
|
||||
|
||||
## [auth.ldap]
|
||||
### enabled
|
||||
Set to `true` to enable LDAP integration (default: `false`)
|
||||
|
||||
### config_file
|
||||
Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`)
|
||||
|
||||
### allow_sign_up
|
||||
|
||||
Allow sign up should almost always be true (default) to allow new Grafana users to be created (if ldap authentication is ok). If set to
|
||||
false only pre-existing Grafana users will be able to login (if ldap authentication is ok).
|
||||
|
||||
> For details on LDAP Configuration, go to the [LDAP Integration]({{< relref "ldap.md" >}}) page.
|
||||
|
||||
<hr>
|
||||
|
||||
## [auth.proxy]
|
||||
|
||||
This feature allows you to handle authentication in a http reverse proxy.
|
||||
|
||||
### enabled
|
||||
|
||||
Defaults to `false`
|
||||
|
||||
### header_name
|
||||
|
||||
Defaults to X-WEBAUTH-USER
|
||||
|
||||
#### header_property
|
||||
|
||||
Defaults to username but can also be set to email
|
||||
|
||||
### auto_sign_up
|
||||
|
||||
Set to `true` to enable auto sign up of users who do not exist in Grafana DB. Defaults to `true`.
|
||||
|
||||
### whitelist
|
||||
|
||||
Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header.
|
||||
|
||||
### headers
|
||||
|
||||
Used to define additional headers for `Name`, `Email` and/or `Login`, for example if the user's name is sent in the X-WEBAUTH-NAME header and their email address in the X-WEBAUTH-EMAIL header, set `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`.
|
||||
|
||||
<hr>
|
||||
Grafana provides many ways to authenticate users. The docs for authentication has been split in to many different pages
|
||||
below.
|
||||
|
||||
- [Authentication Overview]({{< relref "auth/overview.md" >}}) (anonymous access options, hide login and more)
|
||||
- [Google OAuth]({{< relref "auth/google.md" >}}) (auth.google)
|
||||
- [GitHub OAuth]({{< relref "auth/github.md" >}}) (auth.github)
|
||||
- [Gitlab OAuth]({{< relref "auth/gitlab.md" >}}) (auth.gitlab)
|
||||
- [Generic OAuth]({{< relref "auth/generic-oauth.md" >}}) (auth.generic_oauth, okta2, auth0, bitbucket, azure)
|
||||
- [Basic Authentication]({{< relref "auth/overview.md" >}}) (auth.basic)
|
||||
- [LDAP Authentication]({{< relref "auth/ldap.md" >}}) (auth.ldap)
|
||||
- [Auth Proxy]({{< relref "auth/auth-proxy.md" >}}) (auth.proxy)
|
||||
|
||||
## [session]
|
||||
|
||||
@ -1008,3 +556,13 @@ Defaults to true. Set to false to disable alerting engine and hide Alerting from
|
||||
### execute_alerts
|
||||
|
||||
Makes it possible to turn off alert rule execution.
|
||||
|
||||
### error_or_timeout
|
||||
> Available in 5.3 and above
|
||||
|
||||
Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
|
||||
|
||||
### nodata_or_nullvalues
|
||||
> Available in 5.3 and above
|
||||
|
||||
Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
|
||||
|
@ -166,3 +166,8 @@ To configure Grafana add a configuration file named `custom.ini` to the
|
||||
Start Grafana by executing `./bin/grafana-server web`. The `grafana-server`
|
||||
binary needs the working directory to be the root install directory (where the
|
||||
binary and the `public` folder is located).
|
||||
|
||||
## Logging in for the first time
|
||||
|
||||
To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
|
||||
Then follow the instructions [here](/guides/getting_started/).
|
@ -20,7 +20,7 @@ $ docker run -d -p 3000:3000 grafana/grafana
|
||||
|
||||
## Configuration
|
||||
|
||||
All options defined in conf/grafana.ini can be overridden using environment
|
||||
All options defined in `conf/grafana.ini` can be overridden using environment
|
||||
variables by using the syntax `GF_<SectionName>_<KeyName>`.
|
||||
For example:
|
||||
|
||||
@ -40,6 +40,19 @@ those options.
|
||||
|
||||
> For any changes to `conf/grafana.ini` (or corresponding environment variables) to take effect you need to restart Grafana by restarting the Docker container.
|
||||
|
||||
### Default Paths
|
||||
|
||||
The following settings are hard-coded when launching the Grafana Docker container and can only be overridden using environment variables, not in `conf/grafana.ini`.
|
||||
|
||||
Setting | Default value
|
||||
----------------------|---------------------------
|
||||
GF_PATHS_CONFIG | /etc/grafana/grafana.ini
|
||||
GF_PATHS_DATA | /var/lib/grafana
|
||||
GF_PATHS_HOME | /usr/share/grafana
|
||||
GF_PATHS_LOGS | /var/log/grafana
|
||||
GF_PATHS_PLUGINS | /var/lib/grafana/plugins
|
||||
GF_PATHS_PROVISIONING | /etc/grafana/provisioning
|
||||
|
||||
## Running a Specific Version of Grafana
|
||||
|
||||
```bash
|
||||
@ -217,3 +230,8 @@ chown -R root:root /etc/grafana && \
|
||||
chown -R grafana:grafana /var/lib/grafana && \
|
||||
chown -R grafana:grafana /usr/share/grafana
|
||||
```
|
||||
|
||||
## Logging in for the first time
|
||||
|
||||
To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
|
||||
Then follow the instructions [here](/guides/getting_started/).
|
@ -92,3 +92,7 @@ Start Grafana by executing `./bin/grafana-server web`. The `grafana-server`
|
||||
binary needs the working directory to be the root install directory (where the
|
||||
binary and the `public` folder is located).
|
||||
|
||||
## Logging in for the first time
|
||||
|
||||
To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
|
||||
Then follow the instructions [here](/guides/getting_started/).
|
@ -193,3 +193,7 @@ Start Grafana by executing `./bin/grafana-server web`. The `grafana-server`
|
||||
binary needs the working directory to be the root install directory (where the
|
||||
binary and the `public` folder is located).
|
||||
|
||||
## Logging in for the first time
|
||||
|
||||
To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port).
|
||||
Then follow the instructions [here](/guides/getting_started/).
|
@ -109,3 +109,11 @@ positioning system when you load them in v5. Dashboards saved in v5 will not wor
|
||||
external panel plugins might need to be updated to work properly.
|
||||
|
||||
For more details on the new panel positioning system, [click here]({{< relref "reference/dashboard.md#panel-size-position" >}})
|
||||
|
||||
## Upgrading to v5.2
|
||||
|
||||
One of the database migrations included in this release will update all annotation timestamps from second to millisecond precision. If you have a large amount of annotations the database migration may take a long time to complete which may cause problems if you use systemd to run Grafana.
|
||||
|
||||
We've got one report where using systemd, PostgreSQL and a large amount of annotations (table size 1645mb) took 8-20 minutes for the database migration to complete. However, the grafana-server process was killed after 90 seconds by systemd. Any database migration queries in progress when systemd kills the grafana-server process continues to execute in database until finished.
|
||||
|
||||
If you're using systemd and have a large amount of annotations consider temporary adjusting the systemd `TimeoutStartSec` setting to something high like `30m` before upgrading.
|
||||
|
@ -38,6 +38,11 @@ service using that tool.
|
||||
|
||||
Read more about the [configuration options]({{< relref "configuration.md" >}}).
|
||||
|
||||
## Logging in for the first time
|
||||
|
||||
To run Grafana open your browser and go to the port you configured above, e.g. http://localhost:8080/.
|
||||
Then follow the instructions [here](/guides/getting_started/).
|
||||
|
||||
## Building on Windows
|
||||
|
||||
The Grafana backend includes Sqlite3 which requires GCC to compile. So
|
||||
|
@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [Go 1.10](https://golang.org/dl/)
|
||||
- [Go 1.11](https://golang.org/dl/)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [NodeJS LTS](https://nodejs.org/download/)
|
||||
- node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details.
|
||||
@ -141,3 +141,8 @@ Please contribute to the Grafana project and submit a pull request! Build new fe
|
||||
**Problem**: On Windows, getting errors about a tool not being installed even though you just installed that tool.
|
||||
|
||||
**Solution**: It is usually because it got added to the path and you have to restart your command prompt to use it.
|
||||
|
||||
## Logging in for the first time
|
||||
|
||||
To run Grafana open your browser and go to the default port http://localhost:3000 or the port you have configured.
|
||||
Then follow the instructions [here](/guides/getting_started/).
|
@ -245,7 +245,7 @@ Grafana has global built-in variables that can be used in expressions in the que
|
||||
|
||||
### The $__interval Variable
|
||||
|
||||
This $__interval variable is similar to the `auto` interval variable that is described above. It can be used as a parameter to group by time (for InfluxDB), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite).
|
||||
This $__interval variable is similar to the `auto` interval variable that is described above. It can be used as a parameter to group by time (for InfluxDB, MySQL, Postgres, MSSQL), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite).
|
||||
|
||||
Grafana automatically calculates an interval that can be used to group by time in queries. When there are more data points than can be shown on a graph then queries can be made more efficient by grouping by a larger interval. It is more efficient to group by 1 day than by 10s when looking at 3 months of data and the graph will look the same and the query will be faster. The `$__interval` is calculated using the time range and the width of the graph (the number of pixels).
|
||||
|
||||
|
@ -27,7 +27,7 @@ Grafana will now persist all long term data in the database. How to configure th
|
||||
## User sessions
|
||||
|
||||
The second thing to consider is how to deal with user sessions and how to configure your load balancer infront of Grafana.
|
||||
Grafana support two says of storing session data locally on disk or in a database/cache-server.
|
||||
Grafana supports two ways of storing session data: locally on disk or in a database/cache-server.
|
||||
If you want to store sessions on disk you can use `sticky sessions` in your load balanacer. If you prefer to store session data in a database/cache-server
|
||||
you can use any stateless routing strategy in your load balancer (ex round robin or least connections).
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "5.2.0",
|
||||
"testing": "5.2.0"
|
||||
"stable": "5.2.4",
|
||||
"testing": "5.2.4"
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
"@types/jest": "^21.1.4",
|
||||
"@types/node": "^8.0.31",
|
||||
"@types/react": "^16.0.25",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
"@types/react-dom": "^16.0.3",
|
||||
"angular-mocks": "1.6.6",
|
||||
"autoprefixer": "^6.4.0",
|
||||
@ -101,7 +102,7 @@
|
||||
"build": "grunt build",
|
||||
"test": "grunt test",
|
||||
"test:coverage": "grunt test --coverage=true",
|
||||
"lint": "tslint -c tslint.json --project tsconfig.json --type-check",
|
||||
"lint": "tslint -c tslint.json --project tsconfig.json",
|
||||
"jest": "jest --notify --watch",
|
||||
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
|
||||
"precommit": "lint-staged && grunt precommit"
|
||||
@ -154,6 +155,7 @@
|
||||
"prop-types": "^15.6.0",
|
||||
"rc-cascader": "^0.14.0",
|
||||
"react": "^16.2.0",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-grid-layout": "0.16.6",
|
||||
"react-highlight-words": "^0.10.0",
|
||||
|
@ -192,14 +192,7 @@ func GetAlertNotifications(c *m.ReqContext) Response {
|
||||
result := make([]*dtos.AlertNotification, 0)
|
||||
|
||||
for _, notification := range query.Result {
|
||||
result = append(result, &dtos.AlertNotification{
|
||||
Id: notification.Id,
|
||||
Name: notification.Name,
|
||||
Type: notification.Type,
|
||||
IsDefault: notification.IsDefault,
|
||||
Created: notification.Created,
|
||||
Updated: notification.Updated,
|
||||
})
|
||||
result = append(result, dtos.NewAlertNotification(notification))
|
||||
}
|
||||
|
||||
return JSON(200, result)
|
||||
@ -215,7 +208,7 @@ func GetAlertNotificationByID(c *m.ReqContext) Response {
|
||||
return Error(500, "Failed to get alert notifications", err)
|
||||
}
|
||||
|
||||
return JSON(200, query.Result)
|
||||
return JSON(200, dtos.NewAlertNotification(query.Result))
|
||||
}
|
||||
|
||||
func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response {
|
||||
@ -225,7 +218,7 @@ func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationComma
|
||||
return Error(500, "Failed to create alert notification", err)
|
||||
}
|
||||
|
||||
return JSON(200, cmd.Result)
|
||||
return JSON(200, dtos.NewAlertNotification(cmd.Result))
|
||||
}
|
||||
|
||||
func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response {
|
||||
@ -235,7 +228,7 @@ func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationComma
|
||||
return Error(500, "Failed to update alert notification", err)
|
||||
}
|
||||
|
||||
return JSON(200, cmd.Result)
|
||||
return JSON(200, dtos.NewAlertNotification(cmd.Result))
|
||||
}
|
||||
|
||||
func DeleteAlertNotification(c *m.ReqContext) Response {
|
||||
|
@ -1,35 +1,76 @@
|
||||
package dtos
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type AlertRule struct {
|
||||
Id int64 `json:"id"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
Name string `json:"name"`
|
||||
Message string `json:"message"`
|
||||
State m.AlertStateType `json:"state"`
|
||||
NewStateDate time.Time `json:"newStateDate"`
|
||||
EvalDate time.Time `json:"evalDate"`
|
||||
EvalData *simplejson.Json `json:"evalData"`
|
||||
ExecutionError string `json:"executionError"`
|
||||
Url string `json:"url"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
Id int64 `json:"id"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
Name string `json:"name"`
|
||||
Message string `json:"message"`
|
||||
State models.AlertStateType `json:"state"`
|
||||
NewStateDate time.Time `json:"newStateDate"`
|
||||
EvalDate time.Time `json:"evalDate"`
|
||||
EvalData *simplejson.Json `json:"evalData"`
|
||||
ExecutionError string `json:"executionError"`
|
||||
Url string `json:"url"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
}
|
||||
|
||||
func formatShort(interval time.Duration) string {
|
||||
var result string
|
||||
|
||||
hours := interval / time.Hour
|
||||
if hours > 0 {
|
||||
result += fmt.Sprintf("%dh", hours)
|
||||
}
|
||||
|
||||
remaining := interval - (hours * time.Hour)
|
||||
mins := remaining / time.Minute
|
||||
if mins > 0 {
|
||||
result += fmt.Sprintf("%dm", mins)
|
||||
}
|
||||
|
||||
remaining = remaining - (mins * time.Minute)
|
||||
seconds := remaining / time.Second
|
||||
if seconds > 0 {
|
||||
result += fmt.Sprintf("%ds", seconds)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
|
||||
return &AlertNotification{
|
||||
Id: notification.Id,
|
||||
Name: notification.Name,
|
||||
Type: notification.Type,
|
||||
IsDefault: notification.IsDefault,
|
||||
Created: notification.Created,
|
||||
Updated: notification.Updated,
|
||||
Frequency: formatShort(notification.Frequency),
|
||||
SendReminder: notification.SendReminder,
|
||||
Settings: notification.Settings,
|
||||
}
|
||||
}
|
||||
|
||||
type AlertNotification struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
Frequency string `json:"frequency"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
}
|
||||
|
||||
type AlertTestCommand struct {
|
||||
@ -39,7 +80,7 @@ type AlertTestCommand struct {
|
||||
|
||||
type AlertTestResult struct {
|
||||
Firing bool `json:"firing"`
|
||||
State m.AlertStateType `json:"state"`
|
||||
State models.AlertStateType `json:"state"`
|
||||
ConditionEvals string `json:"conditionEvals"`
|
||||
TimeMs string `json:"timeMs"`
|
||||
Error string `json:"error,omitempty"`
|
||||
@ -59,9 +100,11 @@ type EvalMatch struct {
|
||||
}
|
||||
|
||||
type NotificationTestCommand struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
Frequency string `json:"frequency"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
}
|
||||
|
||||
type PauseAlertCommand struct {
|
||||
|
35
pkg/api/dtos/alerting_test.go
Normal file
35
pkg/api/dtos/alerting_test.go
Normal file
@ -0,0 +1,35 @@
|
||||
package dtos
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFormatShort(t *testing.T) {
|
||||
tcs := []struct {
|
||||
interval time.Duration
|
||||
expected string
|
||||
}{
|
||||
{interval: time.Hour, expected: "1h"},
|
||||
{interval: time.Hour + time.Minute, expected: "1h1m"},
|
||||
{interval: (time.Hour * 10) + time.Minute, expected: "10h1m"},
|
||||
{interval: (time.Hour * 10) + (time.Minute * 10) + time.Second, expected: "10h10m1s"},
|
||||
{interval: time.Minute * 10, expected: "10m"},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
got := formatShort(tc.interval)
|
||||
if got != tc.expected {
|
||||
t.Errorf("expected %s got %s interval: %v", tc.expected, got, tc.interval)
|
||||
}
|
||||
|
||||
parsed, err := time.ParseDuration(tc.expected)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse expected duration")
|
||||
}
|
||||
|
||||
if parsed != tc.interval {
|
||||
t.Errorf("expectes the parsed duration to equal the interval. Got %v expected: %v", parsed, tc.interval)
|
||||
}
|
||||
}
|
||||
}
|
@ -132,20 +132,22 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
jsonObj := map[string]interface{}{
|
||||
"defaultDatasource": defaultDatasource,
|
||||
"datasources": datasources,
|
||||
"panels": panels,
|
||||
"appSubUrl": setting.AppSubUrl,
|
||||
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
|
||||
"authProxyEnabled": setting.AuthProxyEnabled,
|
||||
"ldapEnabled": setting.LdapEnabled,
|
||||
"alertingEnabled": setting.AlertingEnabled,
|
||||
"exploreEnabled": setting.ExploreEnabled,
|
||||
"googleAnalyticsId": setting.GoogleAnalyticsId,
|
||||
"disableLoginForm": setting.DisableLoginForm,
|
||||
"externalUserMngInfo": setting.ExternalUserMngInfo,
|
||||
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
|
||||
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
|
||||
"defaultDatasource": defaultDatasource,
|
||||
"datasources": datasources,
|
||||
"panels": panels,
|
||||
"appSubUrl": setting.AppSubUrl,
|
||||
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
|
||||
"authProxyEnabled": setting.AuthProxyEnabled,
|
||||
"ldapEnabled": setting.LdapEnabled,
|
||||
"alertingEnabled": setting.AlertingEnabled,
|
||||
"alertingErrorOrTimeout": setting.AlertingErrorOrTimeout,
|
||||
"alertingNoDataOrNullValues": setting.AlertingNoDataOrNullValues,
|
||||
"exploreEnabled": setting.ExploreEnabled,
|
||||
"googleAnalyticsId": setting.GoogleAnalyticsId,
|
||||
"disableLoginForm": setting.DisableLoginForm,
|
||||
"externalUserMngInfo": setting.ExternalUserMngInfo,
|
||||
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
|
||||
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
|
||||
"buildInfo": map[string]interface{}{
|
||||
"version": setting.BuildVersion,
|
||||
"commit": setting.BuildCommit,
|
||||
|
@ -70,7 +70,7 @@ func (c *connection) readPump() {
|
||||
func (c *connection) handleMessage(message []byte) {
|
||||
json, err := simplejson.NewJson(message)
|
||||
if err != nil {
|
||||
log.Error(3, "Unreadable message on websocket channel:", err)
|
||||
log.Error(3, "Unreadable message on websocket channel. error: %v", err)
|
||||
}
|
||||
|
||||
msgType := json.Get("action").MustString()
|
||||
|
@ -152,7 +152,7 @@ func downloadFile(pluginName, filePath, url string) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := zip.NewReader(bytes.NewReader(body), resp.ContentLength)
|
||||
r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ func ListAllPlugins(repoUrl string) (m.PluginRepo, error) {
|
||||
var data m.PluginRepo
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
logger.Info("Failed to unmarshal graphite response error: %v", err)
|
||||
logger.Info("Failed to unmarshal graphite response error:", err)
|
||||
return m.PluginRepo{}, err
|
||||
}
|
||||
|
||||
@ -140,7 +140,7 @@ func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) {
|
||||
var data m.Plugin
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
logger.Info("Failed to unmarshal graphite response error: %v", err)
|
||||
logger.Info("Failed to unmarshal graphite response error:", err)
|
||||
return m.Plugin{}, err
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,8 @@ func returnOsDefault(currentOs string) string {
|
||||
return "/usr/local/var/lib/grafana/plugins"
|
||||
case "freebsd":
|
||||
return "/var/db/grafana/plugins"
|
||||
case "openbsd":
|
||||
return "/var/grafana/plugins"
|
||||
default: //"linux"
|
||||
return "/var/lib/grafana/plugins"
|
||||
}
|
||||
|
@ -96,13 +96,17 @@ func main() {
|
||||
|
||||
func listenToSystemSignals(server *GrafanaServerImpl) {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
ignoreChan := make(chan os.Signal, 1)
|
||||
sighupChan := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(ignoreChan, syscall.SIGHUP)
|
||||
signal.Notify(sighupChan, syscall.SIGHUP)
|
||||
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case sig := <-signalChan:
|
||||
server.Shutdown(fmt.Sprintf("System signal: %s", sig))
|
||||
for {
|
||||
select {
|
||||
case _ = <-sighupChan:
|
||||
log.Reload()
|
||||
case sig := <-signalChan:
|
||||
server.Shutdown(fmt.Sprintf("System signal: %s", sig))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
|
||||
s3_endpoint, _ := endpoints.DefaultResolver().EndpointFor("s3", u.region)
|
||||
key := u.path + util.GetRandomString(20) + ".png"
|
||||
image_url := s3_endpoint.URL + "/" + u.bucket + "/" + key
|
||||
log.Debug("Uploading image to s3", "url = ", image_url)
|
||||
log.Debug("Uploading image to s3. url = %s", image_url)
|
||||
|
||||
file, err := os.Open(imageDiskPath)
|
||||
if err != nil {
|
||||
|
@ -236,3 +236,20 @@ func (w *FileLogWriter) Close() {
|
||||
func (w *FileLogWriter) Flush() {
|
||||
w.mw.fd.Sync()
|
||||
}
|
||||
|
||||
// Reload file logger
|
||||
func (w *FileLogWriter) Reload() {
|
||||
// block Logger's io.Writer
|
||||
w.mw.Lock()
|
||||
defer w.mw.Unlock()
|
||||
|
||||
// Close
|
||||
fd := w.mw.fd
|
||||
fd.Close()
|
||||
|
||||
// Open again
|
||||
err := w.StartLogger()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Reload StartLogger: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
@ -3,3 +3,7 @@ package log
|
||||
type DisposableHandler interface {
|
||||
Close()
|
||||
}
|
||||
|
||||
type ReloadableHandler interface {
|
||||
Reload()
|
||||
}
|
||||
|
@ -21,10 +21,12 @@ import (
|
||||
|
||||
var Root log15.Logger
|
||||
var loggersToClose []DisposableHandler
|
||||
var loggersToReload []ReloadableHandler
|
||||
var filters map[string]log15.Lvl
|
||||
|
||||
func init() {
|
||||
loggersToClose = make([]DisposableHandler, 0)
|
||||
loggersToReload = make([]ReloadableHandler, 0)
|
||||
Root = log15.Root()
|
||||
Root.SetHandler(log15.DiscardHandler())
|
||||
}
|
||||
@ -103,7 +105,7 @@ func Critical(skip int, format string, v ...interface{}) {
|
||||
}
|
||||
|
||||
func Fatal(skip int, format string, v ...interface{}) {
|
||||
Root.Crit(fmt.Sprintf(format, v))
|
||||
Root.Crit(fmt.Sprintf(format, v...))
|
||||
Close()
|
||||
os.Exit(1)
|
||||
}
|
||||
@ -115,6 +117,12 @@ func Close() {
|
||||
loggersToClose = make([]DisposableHandler, 0)
|
||||
}
|
||||
|
||||
func Reload() {
|
||||
for _, logger := range loggersToReload {
|
||||
logger.Reload()
|
||||
}
|
||||
}
|
||||
|
||||
func GetLogLevelFor(name string) Lvl {
|
||||
if level, ok := filters[name]; ok {
|
||||
switch level {
|
||||
@ -230,6 +238,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
|
||||
fileHandler.Init()
|
||||
|
||||
loggersToClose = append(loggersToClose, fileHandler)
|
||||
loggersToReload = append(loggersToReload, fileHandler)
|
||||
handler = fileHandler
|
||||
case "syslog":
|
||||
sysLogHandler := NewSyslog(sec, format)
|
||||
|
@ -2,7 +2,6 @@ package login
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
@ -14,6 +13,7 @@ var (
|
||||
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
|
||||
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
|
||||
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
|
||||
ErrPasswordEmpty = errors.New("No password provided.")
|
||||
ErrUsersQuotaReached = errors.New("Users quota reached")
|
||||
ErrGettingUserQuota = errors.New("Error getting user quota")
|
||||
)
|
||||
@ -28,6 +28,10 @@ func AuthenticateUser(query *m.LoginUserQuery) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validatePasswordSet(query.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := loginUsingGrafanaDB(query)
|
||||
if err == nil || (err != m.ErrUserNotFound && err != ErrInvalidCredentials) {
|
||||
return err
|
||||
@ -52,3 +56,10 @@ func AuthenticateUser(query *m.LoginUserQuery) error {
|
||||
|
||||
return err
|
||||
}
|
||||
func validatePasswordSet(password string) error {
|
||||
if len(password) == 0 {
|
||||
return ErrPasswordEmpty
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -10,6 +10,24 @@ import (
|
||||
|
||||
func TestAuthenticateUser(t *testing.T) {
|
||||
Convey("Authenticate user", t, func() {
|
||||
authScenario("When a user authenticates without setting a password", func(sc *authScenarioContext) {
|
||||
mockLoginAttemptValidation(nil, sc)
|
||||
mockLoginUsingGrafanaDB(nil, sc)
|
||||
mockLoginUsingLdap(false, nil, sc)
|
||||
|
||||
loginQuery := m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "",
|
||||
}
|
||||
err := AuthenticateUser(&loginQuery)
|
||||
|
||||
Convey("login should fail", func() {
|
||||
So(sc.grafanaLoginWasCalled, ShouldBeFalse)
|
||||
So(sc.ldapLoginWasCalled, ShouldBeFalse)
|
||||
So(err, ShouldEqual, ErrPasswordEmpty)
|
||||
})
|
||||
})
|
||||
|
||||
authScenario("When a user authenticates having too many login attempts", func(sc *authScenarioContext) {
|
||||
mockLoginAttemptValidation(ErrTooManyLoginAttempts, sc)
|
||||
mockLoginUsingGrafanaDB(nil, sc)
|
||||
|
@ -35,7 +35,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||
|
||||
limitReached, err := quota.QuotaReached(cmd.ReqContext, "user")
|
||||
if err != nil {
|
||||
log.Warn("Error getting user quota", "err", err)
|
||||
log.Warn("Error getting user quota. error: %v", err)
|
||||
return ErrGettingUserQuota
|
||||
}
|
||||
if limitReached {
|
||||
@ -135,7 +135,7 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug("Syncing user info", "id", user.Id, "update", updateCmd)
|
||||
log.Debug2("Syncing user info", "id", user.Id, "update", updateCmd)
|
||||
return bus.Dispatch(updateCmd)
|
||||
}
|
||||
|
||||
|
@ -440,6 +440,16 @@ func sendUsageStats() {
|
||||
metrics["stats.ds_access.other."+access+".count"] = count
|
||||
}
|
||||
|
||||
anStats := models.GetAlertNotifierUsageStatsQuery{}
|
||||
if err := bus.Dispatch(&anStats); err != nil {
|
||||
metricsLogger.Error("Failed to get alert notification stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, stats := range anStats.Result {
|
||||
metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
|
||||
}
|
||||
|
||||
out, _ := json.MarshalIndent(report, "", " ")
|
||||
data := bytes.NewBuffer(out)
|
||||
|
||||
|
@ -115,6 +115,24 @@ func TestMetrics(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
var getAlertNotifierUsageStatsQuery *models.GetAlertNotifierUsageStatsQuery
|
||||
bus.AddHandler("test", func(query *models.GetAlertNotifierUsageStatsQuery) error {
|
||||
query.Result = []*models.NotifierUsageStats{
|
||||
{
|
||||
Type: "slack",
|
||||
Count: 1,
|
||||
},
|
||||
{
|
||||
Type: "webhook",
|
||||
Count: 2,
|
||||
},
|
||||
}
|
||||
|
||||
getAlertNotifierUsageStatsQuery = query
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var responseBuffer *bytes.Buffer
|
||||
var req *http.Request
|
||||
@ -157,6 +175,7 @@ func TestMetrics(t *testing.T) {
|
||||
So(getSystemStatsQuery, ShouldNotBeNil)
|
||||
So(getDataSourceStatsQuery, ShouldNotBeNil)
|
||||
So(getDataSourceAccessStatsQuery, ShouldNotBeNil)
|
||||
So(getAlertNotifierUsageStatsQuery, ShouldNotBeNil)
|
||||
So(req, ShouldNotBeNil)
|
||||
So(req.Method, ShouldEqual, http.MethodPost)
|
||||
So(req.Header.Get("Content-Type"), ShouldEqual, "application/json")
|
||||
@ -198,6 +217,9 @@ func TestMetrics(t *testing.T) {
|
||||
So(metrics.Get("stats.ds_access."+models.DS_PROMETHEUS+".proxy.count").MustInt(), ShouldEqual, 3)
|
||||
So(metrics.Get("stats.ds_access.other.direct.count").MustInt(), ShouldEqual, 6+7)
|
||||
So(metrics.Get("stats.ds_access.other.proxy.count").MustInt(), ShouldEqual, 4+8)
|
||||
|
||||
So(metrics.Get("stats.alert_notifiers.slack.count").MustInt(), ShouldEqual, 1)
|
||||
So(metrics.Get("stats.alert_notifiers.webhook.count").MustInt(), ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -36,7 +36,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
||||
|
||||
// initialize session
|
||||
if err := ctx.Session.Start(ctx.Context); err != nil {
|
||||
log.Error(3, "Failed to start session", err)
|
||||
log.Error(3, "Failed to start session. error %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -146,12 +146,12 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
||||
if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId {
|
||||
// remove session
|
||||
if err := ctx.Session.Destory(ctx.Context); err != nil {
|
||||
log.Error(3, "Failed to destroy session, err")
|
||||
log.Error(3, "Failed to destroy session. error: %v", err)
|
||||
}
|
||||
|
||||
// initialize a new session
|
||||
if err := ctx.Session.Start(ctx.Context); err != nil {
|
||||
log.Error(3, "Failed to start session", err)
|
||||
log.Error(3, "Failed to start session. error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,38 +1,50 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified")
|
||||
ErrJournalingNotFound = errors.New("alert notification journaling not found")
|
||||
)
|
||||
|
||||
type AlertNotification struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
Frequency time.Duration `json:"frequency"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
type CreateAlertNotificationCommand struct {
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Type string `json:"type" binding:"Required"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Type string `json:"type" binding:"Required"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
Frequency string `json:"frequency"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
|
||||
OrgId int64 `json:"-"`
|
||||
Result *AlertNotification
|
||||
}
|
||||
|
||||
type UpdateAlertNotificationCommand struct {
|
||||
Id int64 `json:"id" binding:"Required"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Type string `json:"type" binding:"Required"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings" binding:"Required"`
|
||||
Id int64 `json:"id" binding:"Required"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Type string `json:"type" binding:"Required"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
Frequency string `json:"frequency"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings" binding:"Required"`
|
||||
|
||||
OrgId int64 `json:"-"`
|
||||
Result *AlertNotification
|
||||
@ -63,3 +75,34 @@ type GetAllAlertNotificationsQuery struct {
|
||||
|
||||
Result []*AlertNotification
|
||||
}
|
||||
|
||||
type AlertNotificationJournal struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
AlertId int64
|
||||
NotifierId int64
|
||||
SentAt int64
|
||||
Success bool
|
||||
}
|
||||
|
||||
type RecordNotificationJournalCommand struct {
|
||||
OrgId int64
|
||||
AlertId int64
|
||||
NotifierId int64
|
||||
SentAt int64
|
||||
Success bool
|
||||
}
|
||||
|
||||
type GetLatestNotificationQuery struct {
|
||||
OrgId int64
|
||||
AlertId int64
|
||||
NotifierId int64
|
||||
|
||||
Result *AlertNotificationJournal
|
||||
}
|
||||
|
||||
type CleanNotificationJournalCommand struct {
|
||||
OrgId int64
|
||||
AlertId int64
|
||||
NotifierId int64
|
||||
}
|
||||
|
@ -40,6 +40,15 @@ type GetDataSourceAccessStatsQuery struct {
|
||||
Result []*DataSourceAccessStats
|
||||
}
|
||||
|
||||
type NotifierUsageStats struct {
|
||||
Type string
|
||||
Count int64
|
||||
}
|
||||
|
||||
type GetAlertNotifierUsageStatsQuery struct {
|
||||
Result []*NotifierUsageStats
|
||||
}
|
||||
|
||||
type AdminStats struct {
|
||||
Users int `json:"users"`
|
||||
Orgs int `json:"orgs"`
|
||||
|
@ -1,6 +1,9 @@
|
||||
package alerting
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EvalHandler interface {
|
||||
Eval(evalContext *EvalContext)
|
||||
@ -15,10 +18,14 @@ type Notifier interface {
|
||||
Notify(evalContext *EvalContext) error
|
||||
GetType() string
|
||||
NeedsImage() bool
|
||||
ShouldNotify(evalContext *EvalContext) bool
|
||||
|
||||
// ShouldNotify checks this evaluation should send an alert notification
|
||||
ShouldNotify(ctx context.Context, evalContext *EvalContext) bool
|
||||
|
||||
GetNotifierId() int64
|
||||
GetIsDefault() bool
|
||||
GetSendReminder() bool
|
||||
GetFrequency() time.Duration
|
||||
}
|
||||
|
||||
type NotifierSlice []Notifier
|
||||
|
@ -1,10 +1,10 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/imguploader"
|
||||
@ -58,17 +58,47 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error {
|
||||
return n.sendNotifications(context, notifiers)
|
||||
}
|
||||
|
||||
func (n *notificationService) sendNotifications(context *EvalContext, notifiers []Notifier) error {
|
||||
g, _ := errgroup.WithContext(context.Ctx)
|
||||
|
||||
func (n *notificationService) sendNotifications(evalContext *EvalContext, notifiers []Notifier) error {
|
||||
for _, notifier := range notifiers {
|
||||
not := notifier //avoid updating scope variable in go routine
|
||||
n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
|
||||
metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc()
|
||||
g.Go(func() error { return not.Notify(context) })
|
||||
not := notifier
|
||||
|
||||
err := bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error {
|
||||
n.log.Debug("trying to send notification", "id", not.GetNotifierId())
|
||||
|
||||
// Verify that we can send the notification again
|
||||
// but this time within the same transaction.
|
||||
if !evalContext.IsTestRun && !not.ShouldNotify(context.Background(), evalContext) {
|
||||
return nil
|
||||
}
|
||||
|
||||
n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
|
||||
metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc()
|
||||
|
||||
//send notification
|
||||
success := not.Notify(evalContext) == nil
|
||||
|
||||
if evalContext.IsTestRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
//write result to db.
|
||||
cmd := &m.RecordNotificationJournalCommand{
|
||||
OrgId: evalContext.Rule.OrgId,
|
||||
AlertId: evalContext.Rule.Id,
|
||||
NotifierId: not.GetNotifierId(),
|
||||
SentAt: time.Now().Unix(),
|
||||
Success: success,
|
||||
}
|
||||
|
||||
return bus.DispatchCtx(ctx, cmd)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
n.log.Error("failed to send notification", "id", not.GetNotifierId())
|
||||
}
|
||||
}
|
||||
|
||||
return g.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
@ -110,7 +140,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
|
||||
func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (NotifierSlice, error) {
|
||||
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
|
||||
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
@ -123,7 +153,8 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if not.ShouldNotify(context) {
|
||||
|
||||
if not.ShouldNotify(evalContext.Ctx, evalContext) {
|
||||
result = append(result, not)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@ -33,7 +34,7 @@ func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, err
|
||||
}
|
||||
|
||||
return &AlertmanagerNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.prometheus-alertmanager"),
|
||||
}, nil
|
||||
@ -45,7 +46,7 @@ type AlertmanagerNotifier struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool {
|
||||
func (this *AlertmanagerNotifier) ShouldNotify(ctx context.Context, evalContext *alerting.EvalContext) bool {
|
||||
this.log.Debug("Should notify", "ruleId", evalContext.Rule.Id, "state", evalContext.Rule.State, "previousState", evalContext.PrevAlertState)
|
||||
|
||||
// Do not notify when we become OK for the first time.
|
||||
|
@ -1,50 +1,94 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
type NotifierBase struct {
|
||||
Name string
|
||||
Type string
|
||||
Id int64
|
||||
IsDeault bool
|
||||
UploadImage bool
|
||||
Name string
|
||||
Type string
|
||||
Id int64
|
||||
IsDeault bool
|
||||
UploadImage bool
|
||||
SendReminder bool
|
||||
Frequency time.Duration
|
||||
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase {
|
||||
func NewNotifierBase(model *models.AlertNotification) NotifierBase {
|
||||
uploadImage := true
|
||||
value, exist := model.CheckGet("uploadImage")
|
||||
value, exist := model.Settings.CheckGet("uploadImage")
|
||||
if exist {
|
||||
uploadImage = value.MustBool()
|
||||
}
|
||||
|
||||
return NotifierBase{
|
||||
Id: id,
|
||||
Name: name,
|
||||
IsDeault: isDefault,
|
||||
Type: notifierType,
|
||||
UploadImage: uploadImage,
|
||||
Id: model.Id,
|
||||
Name: model.Name,
|
||||
IsDeault: model.IsDefault,
|
||||
Type: model.Type,
|
||||
UploadImage: uploadImage,
|
||||
SendReminder: model.SendReminder,
|
||||
Frequency: model.Frequency,
|
||||
log: log.New("alerting.notifier." + model.Name),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultShouldNotify(context *alerting.EvalContext) bool {
|
||||
func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, lastNotify time.Time) bool {
|
||||
// Only notify on state change.
|
||||
if context.PrevAlertState == context.Rule.State {
|
||||
if context.PrevAlertState == context.Rule.State && !sendReminder {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notify if interval has not elapsed
|
||||
if sendReminder && !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notify if alert state if OK or pending even on repeated notify
|
||||
if sendReminder && (context.Rule.State == models.AlertStateOK || context.Rule.State == models.AlertStatePending) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Do not notify when we become OK for the first time.
|
||||
if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) {
|
||||
if (context.PrevAlertState == models.AlertStatePending) && (context.Rule.State == models.AlertStateOK) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
// ShouldNotify checks this evaluation should send an alert notification
|
||||
func (n *NotifierBase) ShouldNotify(ctx context.Context, c *alerting.EvalContext) bool {
|
||||
cmd := &models.GetLatestNotificationQuery{
|
||||
OrgId: c.Rule.OrgId,
|
||||
AlertId: c.Rule.Id,
|
||||
NotifierId: n.Id,
|
||||
}
|
||||
|
||||
err := bus.DispatchCtx(ctx, cmd)
|
||||
if err == models.ErrJournalingNotFound {
|
||||
return true
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if !cmd.Result.Success {
|
||||
return true
|
||||
}
|
||||
|
||||
return defaultShouldNotify(c, n.SendReminder, n.Frequency, time.Unix(cmd.Result.SentAt, 0))
|
||||
}
|
||||
|
||||
func (n *NotifierBase) GetType() string {
|
||||
@ -62,3 +106,11 @@ func (n *NotifierBase) GetNotifierId() int64 {
|
||||
func (n *NotifierBase) GetIsDefault() bool {
|
||||
return n.IsDeault
|
||||
}
|
||||
|
||||
func (n *NotifierBase) GetSendReminder() bool {
|
||||
return n.SendReminder
|
||||
}
|
||||
|
||||
func (n *NotifierBase) GetFrequency() time.Duration {
|
||||
return n.Frequency
|
||||
}
|
||||
|
@ -2,7 +2,11 @@ package notifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@ -10,47 +14,129 @@ import (
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestBaseNotifier(t *testing.T) {
|
||||
Convey("Base notifier tests", t, func() {
|
||||
Convey("default constructor for notifiers", func() {
|
||||
bJson := simplejson.New()
|
||||
func TestShouldSendAlertNotification(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
prevState m.AlertStateType
|
||||
newState m.AlertStateType
|
||||
expected bool
|
||||
sendReminder bool
|
||||
}{
|
||||
{
|
||||
name: "pending -> ok should not trigger an notification",
|
||||
newState: m.AlertStatePending,
|
||||
prevState: m.AlertStateOK,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "ok -> alerting should trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateAlerting,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "ok -> pending should not trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStatePending,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "ok -> ok should not trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateOK,
|
||||
expected: false,
|
||||
sendReminder: false,
|
||||
},
|
||||
{
|
||||
name: "ok -> alerting should not trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateAlerting,
|
||||
expected: true,
|
||||
sendReminder: true,
|
||||
},
|
||||
{
|
||||
name: "ok -> ok with reminder should not trigger an notification",
|
||||
newState: m.AlertStateOK,
|
||||
prevState: m.AlertStateOK,
|
||||
expected: false,
|
||||
sendReminder: true,
|
||||
},
|
||||
}
|
||||
|
||||
Convey("can parse false value", func() {
|
||||
bJson.Set("uploadImage", false)
|
||||
|
||||
base := NewNotifierBase(1, false, "name", "email", bJson)
|
||||
So(base.UploadImage, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("can parse true value", func() {
|
||||
bJson.Set("uploadImage", true)
|
||||
|
||||
base := NewNotifierBase(1, false, "name", "email", bJson)
|
||||
So(base.UploadImage, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("default value should be true for backwards compatibility", func() {
|
||||
base := NewNotifierBase(1, false, "name", "email", bJson)
|
||||
So(base.UploadImage, ShouldBeTrue)
|
||||
})
|
||||
for _, tc := range tcs {
|
||||
evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
||||
State: tc.newState,
|
||||
})
|
||||
|
||||
Convey("should notify", func() {
|
||||
Convey("pending -> ok", func() {
|
||||
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
||||
State: m.AlertStatePending,
|
||||
})
|
||||
context.Rule.State = m.AlertStateOK
|
||||
So(defaultShouldNotify(context), ShouldBeFalse)
|
||||
evalContext.Rule.State = tc.prevState
|
||||
if defaultShouldNotify(evalContext, true, 0, time.Now()) != tc.expected {
|
||||
t.Errorf("failed %s. expected %+v to return %v", tc.name, tc, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) {
|
||||
Convey("base notifier", t, func() {
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
notifier := NewNotifierBase(&m.AlertNotification{
|
||||
Id: 1,
|
||||
Name: "name",
|
||||
Type: "email",
|
||||
Settings: simplejson.New(),
|
||||
})
|
||||
evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{})
|
||||
|
||||
Convey("should notify if no journaling is found", func() {
|
||||
bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error {
|
||||
return m.ErrJournalingNotFound
|
||||
})
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
context.Rule.State = m.AlertStateAlerting
|
||||
So(defaultShouldNotify(context), ShouldBeTrue)
|
||||
if !notifier.ShouldNotify(context.Background(), evalContext) {
|
||||
t.Errorf("should send notifications when ErrJournalingNotFound is returned")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("should not notify query returns error", func() {
|
||||
bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error {
|
||||
return errors.New("some kind of error unknown error")
|
||||
})
|
||||
|
||||
if notifier.ShouldNotify(context.Background(), evalContext) {
|
||||
t.Errorf("should not send notifications when query returns error")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBaseNotifier(t *testing.T) {
|
||||
Convey("default constructor for notifiers", t, func() {
|
||||
bJson := simplejson.New()
|
||||
|
||||
model := &m.AlertNotification{
|
||||
Id: 1,
|
||||
Name: "name",
|
||||
Type: "email",
|
||||
Settings: bJson,
|
||||
}
|
||||
|
||||
Convey("can parse false value", func() {
|
||||
bJson.Set("uploadImage", false)
|
||||
|
||||
base := NewNotifierBase(model)
|
||||
So(base.UploadImage, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("can parse true value", func() {
|
||||
bJson.Set("uploadImage", true)
|
||||
|
||||
base := NewNotifierBase(model)
|
||||
So(base.UploadImage, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("default value should be true for backwards compatibility", func() {
|
||||
base := NewNotifierBase(model)
|
||||
So(base.UploadImage, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
}
|
||||
|
||||
return &DingDingNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.dingding"),
|
||||
}, nil
|
||||
|
@ -39,7 +39,7 @@ func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}
|
||||
|
||||
return &DiscordNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
WebhookURL: url,
|
||||
log: log.New("alerting.notifier.discord"),
|
||||
}, nil
|
||||
|
@ -52,7 +52,7 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
})
|
||||
|
||||
return &EmailNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Addresses: addresses,
|
||||
log: log.New("alerting.notifier.email"),
|
||||
}, nil
|
||||
|
@ -59,7 +59,7 @@ func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, err
|
||||
roomId := model.Settings.Get("roomid").MustString()
|
||||
|
||||
return &HipChatNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Url: url,
|
||||
ApiKey: apikey,
|
||||
RoomId: roomId,
|
||||
|
@ -43,7 +43,7 @@ func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}
|
||||
|
||||
return &KafkaNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Endpoint: endpoint,
|
||||
Topic: topic,
|
||||
log: log.New("alerting.notifier.kafka"),
|
||||
|
@ -39,7 +39,7 @@ func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}
|
||||
|
||||
return &LineNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Token: token,
|
||||
log: log.New("alerting.notifier.line"),
|
||||
}, nil
|
||||
|
@ -56,7 +56,7 @@ func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
}
|
||||
|
||||
return &OpsGenieNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
ApiKey: apiKey,
|
||||
ApiUrl: apiUrl,
|
||||
AutoClose: autoClose,
|
||||
|
@ -51,7 +51,7 @@ func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
}
|
||||
|
||||
return &PagerdutyNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Key: key,
|
||||
AutoResolve: autoResolve,
|
||||
log: log.New("alerting.notifier.pagerduty"),
|
||||
|
@ -99,7 +99,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
return nil, alerting.ValidationError{Reason: "API token not given"}
|
||||
}
|
||||
return &PushoverNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
UserKey: userKey,
|
||||
ApiToken: apiToken,
|
||||
Priority: priority,
|
||||
|
@ -51,7 +51,7 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}
|
||||
|
||||
return &SensuNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Url: url,
|
||||
User: model.Settings.Get("username").MustString(),
|
||||
Source: model.Settings.Get("source").MustString(),
|
||||
|
@ -78,7 +78,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
uploadImage := model.Settings.Get("uploadImage").MustBool(true)
|
||||
|
||||
return &SlackNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Url: url,
|
||||
Recipient: recipient,
|
||||
Mention: mention,
|
||||
|
@ -33,7 +33,7 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}
|
||||
|
||||
return &TeamsNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.teams"),
|
||||
}, nil
|
||||
@ -96,14 +96,26 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
},
|
||||
},
|
||||
"text": message,
|
||||
"potentialAction": []map[string]interface{}{
|
||||
},
|
||||
},
|
||||
"potentialAction": []map[string]interface{}{
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "OpenUri",
|
||||
"name": "View Rule",
|
||||
"targets": []map[string]interface{}{
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "ViewAction",
|
||||
"name": "View Rule",
|
||||
"target": []string{
|
||||
ruleUrl,
|
||||
},
|
||||
"os": "default", "uri": ruleUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "OpenUri",
|
||||
"name": "View Graph",
|
||||
"targets": []map[string]interface{}{
|
||||
{
|
||||
"os": "default", "uri": evalContext.ImagePublicUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -78,7 +78,7 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
}
|
||||
|
||||
return &TelegramNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
BotToken: botToken,
|
||||
ChatID: chatId,
|
||||
UploadImage: uploadImage,
|
||||
@ -216,7 +216,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string {
|
||||
if len(extra)+len(message) <= sizeLimit {
|
||||
return message + extra
|
||||
}
|
||||
log.Debug("Line too long for image caption.", "value", extra)
|
||||
log.Debug("Line too long for image caption. value: %s", extra)
|
||||
return message
|
||||
}
|
||||
|
||||
|
@ -106,7 +106,7 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}
|
||||
|
||||
return &ThreemaNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
GatewayID: gatewayID,
|
||||
RecipientID: recipientID,
|
||||
APISecret: apiSecret,
|
||||
|
@ -51,7 +51,7 @@ func NewVictoropsNotifier(model *models.AlertNotification) (alerting.Notifier, e
|
||||
}
|
||||
|
||||
return &VictoropsNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
URL: url,
|
||||
AutoResolve: autoResolve,
|
||||
log: log.New("alerting.notifier.victorops"),
|
||||
|
@ -47,7 +47,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}
|
||||
|
||||
return &WebhookNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
Url: url,
|
||||
User: model.Settings.Get("username").MustString(),
|
||||
Password: model.Settings.Get("password").MustString(),
|
||||
|
@ -88,6 +88,18 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
}
|
||||
}
|
||||
|
||||
if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK {
|
||||
for _, notifierId := range evalContext.Rule.Notifications {
|
||||
cmd := &m.CleanNotificationJournalCommand{
|
||||
AlertId: evalContext.Rule.Id,
|
||||
NotifierId: notifierId,
|
||||
OrgId: evalContext.Rule.OrgId,
|
||||
}
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
handler.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.notifier.SendIfNeeded(evalContext)
|
||||
|
||||
return nil
|
||||
|
@ -2,6 +2,7 @@ package rendering
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -20,14 +21,13 @@ var netTransport = &http.Transport{
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
var netClient = &http.Client{
|
||||
Transport: netTransport,
|
||||
}
|
||||
|
||||
func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*RenderResult, error) {
|
||||
filePath := rs.getFilePathForNewImage()
|
||||
|
||||
var netClient = &http.Client{
|
||||
Timeout: opts.Timeout,
|
||||
Transport: netTransport,
|
||||
}
|
||||
|
||||
rendererUrl, err := url.Parse(rs.Cfg.RendererUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -35,10 +35,10 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend
|
||||
|
||||
queryParams := rendererUrl.Query()
|
||||
queryParams.Add("url", rs.getURL(opts.Path))
|
||||
queryParams.Add("renderKey", rs.getRenderKey(opts.UserId, opts.OrgId, opts.OrgRole))
|
||||
queryParams.Add("renderKey", rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole))
|
||||
queryParams.Add("width", strconv.Itoa(opts.Width))
|
||||
queryParams.Add("height", strconv.Itoa(opts.Height))
|
||||
queryParams.Add("domain", rs.getLocalDomain())
|
||||
queryParams.Add("domain", rs.domain)
|
||||
queryParams.Add("timezone", isoTimeOffsetToPosixTz(opts.Timezone))
|
||||
queryParams.Add("encoding", opts.Encoding)
|
||||
queryParams.Add("timeout", strconv.Itoa(int(opts.Timeout.Seconds())))
|
||||
@ -49,20 +49,48 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
|
||||
defer cancel()
|
||||
|
||||
req = req.WithContext(reqContext)
|
||||
|
||||
// make request to renderer server
|
||||
resp, err := netClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
rs.log.Error("Failed to send request to remote rendering service.", "error", err)
|
||||
return nil, fmt.Errorf("Failed to send request to remote rendering service. %s", err)
|
||||
}
|
||||
|
||||
// save response to file
|
||||
defer resp.Body.Close()
|
||||
|
||||
// check for timeout first
|
||||
if reqContext.Err() == context.DeadlineExceeded {
|
||||
rs.log.Info("Rendering timed out")
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
|
||||
// if we didnt get a 200 response, something went wrong.
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
rs.log.Error("Remote rendering request failed", "error", resp.Status)
|
||||
return nil, fmt.Errorf("Remote rendering request failed. %d: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
out, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer out.Close()
|
||||
io.Copy(out, resp.Body)
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
// check that we didnt timeout while receiving the response.
|
||||
if reqContext.Err() == context.DeadlineExceeded {
|
||||
rs.log.Info("Rendering timed out")
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
rs.log.Error("Remote rendering request failed", "error", err)
|
||||
return nil, fmt.Errorf("Remote rendering request failed. %s", err)
|
||||
}
|
||||
|
||||
return &RenderResult{FilePath: filePath}, err
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
|
||||
fmt.Sprintf("width=%v", opts.Width),
|
||||
fmt.Sprintf("height=%v", opts.Height),
|
||||
fmt.Sprintf("png=%v", pngPath),
|
||||
fmt.Sprintf("domain=%v", rs.getLocalDomain()),
|
||||
fmt.Sprintf("domain=%v", rs.domain),
|
||||
fmt.Sprintf("timeout=%v", opts.Timeout.Seconds()),
|
||||
fmt.Sprintf("renderKey=%v", renderKey),
|
||||
}
|
||||
|
@ -77,10 +77,10 @@ func (rs *RenderingService) renderViaPlugin(ctx context.Context, opts Opts) (*Re
|
||||
Height: int32(opts.Height),
|
||||
FilePath: pngPath,
|
||||
Timeout: int32(opts.Timeout.Seconds()),
|
||||
RenderKey: rs.getRenderKey(opts.UserId, opts.OrgId, opts.OrgRole),
|
||||
RenderKey: rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole),
|
||||
Encoding: opts.Encoding,
|
||||
Timezone: isoTimeOffsetToPosixTz(opts.Timezone),
|
||||
Domain: rs.getLocalDomain(),
|
||||
Domain: rs.domain,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
@ -3,6 +3,8 @@ package rendering
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
plugin "github.com/hashicorp/go-plugin"
|
||||
@ -27,12 +29,31 @@ type RenderingService struct {
|
||||
grpcPlugin pluginModel.RendererPlugin
|
||||
pluginInfo *plugins.RendererPlugin
|
||||
renderAction renderFunc
|
||||
domain string
|
||||
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
}
|
||||
|
||||
func (rs *RenderingService) Init() error {
|
||||
rs.log = log.New("rendering")
|
||||
|
||||
// ensure ImagesDir exists
|
||||
err := os.MkdirAll(rs.Cfg.ImagesDir, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set value used for domain attribute of renderKey cookie
|
||||
if rs.Cfg.RendererUrl != "" {
|
||||
// RendererCallbackUrl has already been passed, it wont generate an error.
|
||||
u, _ := url.Parse(rs.Cfg.RendererCallbackUrl)
|
||||
rs.domain = u.Hostname()
|
||||
} else if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR {
|
||||
rs.domain = setting.HttpAddr
|
||||
} else {
|
||||
rs.domain = "localhost"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -82,16 +103,17 @@ func (rs *RenderingService) getFilePathForNewImage() string {
|
||||
}
|
||||
|
||||
func (rs *RenderingService) getURL(path string) string {
|
||||
// &render=1 signals to the legacy redirect layer to
|
||||
return fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, rs.getLocalDomain(), setting.HttpPort, path)
|
||||
}
|
||||
if rs.Cfg.RendererUrl != "" {
|
||||
// The backend rendering service can potentially be remote.
|
||||
// So we need to use the root_url to ensure the rendering service
|
||||
// can reach this Grafana instance.
|
||||
|
||||
// &render=1 signals to the legacy redirect layer to
|
||||
return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path)
|
||||
|
||||
func (rs *RenderingService) getLocalDomain() string {
|
||||
if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR {
|
||||
return setting.HttpAddr
|
||||
}
|
||||
|
||||
return "localhost"
|
||||
// &render=1 signals to the legacy redirect layer to
|
||||
return fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, rs.domain, setting.HttpPort, path)
|
||||
}
|
||||
|
||||
func (rs *RenderingService) getRenderKey(orgId, userId int64, orgRole models.RoleType) string {
|
||||
|
@ -2,6 +2,7 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@ -17,6 +18,9 @@ func init() {
|
||||
bus.AddHandler("sql", DeleteAlertNotification)
|
||||
bus.AddHandler("sql", GetAlertNotificationsToSend)
|
||||
bus.AddHandler("sql", GetAllAlertNotifications)
|
||||
bus.AddHandlerCtx("sql", RecordNotificationJournal)
|
||||
bus.AddHandlerCtx("sql", GetLatestNotification)
|
||||
bus.AddHandlerCtx("sql", CleanNotificationJournal)
|
||||
}
|
||||
|
||||
func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
|
||||
@ -53,7 +57,9 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro
|
||||
alert_notification.created,
|
||||
alert_notification.updated,
|
||||
alert_notification.settings,
|
||||
alert_notification.is_default
|
||||
alert_notification.is_default,
|
||||
alert_notification.send_reminder,
|
||||
alert_notification.frequency
|
||||
FROM alert_notification
|
||||
`)
|
||||
|
||||
@ -91,7 +97,9 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS
|
||||
alert_notification.created,
|
||||
alert_notification.updated,
|
||||
alert_notification.settings,
|
||||
alert_notification.is_default
|
||||
alert_notification.is_default,
|
||||
alert_notification.send_reminder,
|
||||
alert_notification.frequency
|
||||
FROM alert_notification
|
||||
`)
|
||||
|
||||
@ -137,17 +145,31 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
|
||||
return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
|
||||
}
|
||||
|
||||
alertNotification := &m.AlertNotification{
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
Type: cmd.Type,
|
||||
Settings: cmd.Settings,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
IsDefault: cmd.IsDefault,
|
||||
var frequency time.Duration
|
||||
if cmd.SendReminder {
|
||||
if cmd.Frequency == "" {
|
||||
return m.ErrNotificationFrequencyNotFound
|
||||
}
|
||||
|
||||
frequency, err = time.ParseDuration(cmd.Frequency)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = sess.Insert(alertNotification); err != nil {
|
||||
alertNotification := &m.AlertNotification{
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
Type: cmd.Type,
|
||||
Settings: cmd.Settings,
|
||||
SendReminder: cmd.SendReminder,
|
||||
Frequency: frequency,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
IsDefault: cmd.IsDefault,
|
||||
}
|
||||
|
||||
if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -179,16 +201,77 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
|
||||
current.Name = cmd.Name
|
||||
current.Type = cmd.Type
|
||||
current.IsDefault = cmd.IsDefault
|
||||
current.SendReminder = cmd.SendReminder
|
||||
|
||||
sess.UseBool("is_default")
|
||||
if current.SendReminder {
|
||||
if cmd.Frequency == "" {
|
||||
return m.ErrNotificationFrequencyNotFound
|
||||
}
|
||||
|
||||
frequency, err := time.ParseDuration(cmd.Frequency)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
current.Frequency = frequency
|
||||
}
|
||||
|
||||
sess.UseBool("is_default", "send_reminder")
|
||||
|
||||
if affected, err := sess.ID(cmd.Id).Update(current); err != nil {
|
||||
return err
|
||||
} else if affected == 0 {
|
||||
return fmt.Errorf("Could not find alert notification")
|
||||
return fmt.Errorf("Could not update alert notification")
|
||||
}
|
||||
|
||||
cmd.Result = ¤t
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJournalCommand) error {
|
||||
return inTransactionCtx(ctx, func(sess *DBSession) error {
|
||||
journalEntry := &m.AlertNotificationJournal{
|
||||
OrgId: cmd.OrgId,
|
||||
AlertId: cmd.AlertId,
|
||||
NotifierId: cmd.NotifierId,
|
||||
SentAt: cmd.SentAt,
|
||||
Success: cmd.Success,
|
||||
}
|
||||
|
||||
if _, err := sess.Insert(journalEntry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error {
|
||||
return inTransactionCtx(ctx, func(sess *DBSession) error {
|
||||
nj := &m.AlertNotificationJournal{}
|
||||
|
||||
_, err := sess.Desc("alert_notification_journal.sent_at").
|
||||
Limit(1).
|
||||
Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(nj)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if nj.AlertId == 0 && nj.Id == 0 && nj.NotifierId == 0 && nj.OrgId == 0 {
|
||||
return m.ErrJournalingNotFound
|
||||
}
|
||||
|
||||
cmd.Result = nj
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CleanNotificationJournal(ctx context.Context, cmd *m.CleanNotificationJournalCommand) error {
|
||||
return inTransactionCtx(ctx, func(sess *DBSession) error {
|
||||
sql := "DELETE FROM alert_notification_journal WHERE alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?"
|
||||
_, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@ -11,7 +13,48 @@ import (
|
||||
func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
Convey("Testing Alert notification sql access", t, func() {
|
||||
InitTestDB(t)
|
||||
var err error
|
||||
|
||||
Convey("Alert notification journal", func() {
|
||||
var alertId int64 = 5
|
||||
var orgId int64 = 5
|
||||
var notifierId int64 = 5
|
||||
|
||||
Convey("Getting last journal should raise error if no one exists", func() {
|
||||
query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId}
|
||||
err := GetLatestNotification(context.Background(), query)
|
||||
So(err, ShouldEqual, m.ErrJournalingNotFound)
|
||||
|
||||
Convey("shoulbe be able to record two journaling events", func() {
|
||||
createCmd := &m.RecordNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId, Success: true, SentAt: 1}
|
||||
|
||||
err := RecordNotificationJournal(context.Background(), createCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
createCmd.SentAt += 1000 //increase epoch
|
||||
|
||||
err = RecordNotificationJournal(context.Background(), createCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("get last journaling event", func() {
|
||||
err := GetLatestNotification(context.Background(), query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.SentAt, ShouldEqual, 1001)
|
||||
|
||||
Convey("be able to clear all journaling for an notifier", func() {
|
||||
cmd := &m.CleanNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId}
|
||||
err := CleanNotificationJournal(context.Background(), cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("querying for last junaling should raise error", func() {
|
||||
query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId}
|
||||
err := GetLatestNotification(context.Background(), query)
|
||||
So(err, ShouldEqual, m.ErrJournalingNotFound)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Alert notifications should be empty", func() {
|
||||
cmd := &m.GetAlertNotificationsQuery{
|
||||
@ -24,19 +67,75 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
So(cmd.Result, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Can save Alert Notification", func() {
|
||||
Convey("Cannot save alert notifier with send reminder = true", func() {
|
||||
cmd := &m.CreateAlertNotificationCommand{
|
||||
Name: "ops",
|
||||
Type: "email",
|
||||
OrgId: 1,
|
||||
Settings: simplejson.New(),
|
||||
Name: "ops",
|
||||
Type: "email",
|
||||
OrgId: 1,
|
||||
SendReminder: true,
|
||||
Settings: simplejson.New(),
|
||||
}
|
||||
|
||||
err = CreateAlertNotificationCommand(cmd)
|
||||
Convey("and missing frequency", func() {
|
||||
err := CreateAlertNotificationCommand(cmd)
|
||||
So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound)
|
||||
})
|
||||
|
||||
Convey("invalid frequency", func() {
|
||||
cmd.Frequency = "invalid duration"
|
||||
|
||||
err := CreateAlertNotificationCommand(cmd)
|
||||
So(err.Error(), ShouldEqual, "time: invalid duration invalid duration")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Cannot update alert notifier with send reminder = false", func() {
|
||||
cmd := &m.CreateAlertNotificationCommand{
|
||||
Name: "ops update",
|
||||
Type: "email",
|
||||
OrgId: 1,
|
||||
SendReminder: false,
|
||||
Settings: simplejson.New(),
|
||||
}
|
||||
|
||||
err := CreateAlertNotificationCommand(cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
updateCmd := &m.UpdateAlertNotificationCommand{
|
||||
Id: cmd.Result.Id,
|
||||
SendReminder: true,
|
||||
}
|
||||
|
||||
Convey("and missing frequency", func() {
|
||||
err := UpdateAlertNotification(updateCmd)
|
||||
So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound)
|
||||
})
|
||||
|
||||
Convey("invalid frequency", func() {
|
||||
updateCmd.Frequency = "invalid duration"
|
||||
|
||||
err := UpdateAlertNotification(updateCmd)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldEqual, "time: invalid duration invalid duration")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can save Alert Notification", func() {
|
||||
cmd := &m.CreateAlertNotificationCommand{
|
||||
Name: "ops",
|
||||
Type: "email",
|
||||
OrgId: 1,
|
||||
SendReminder: true,
|
||||
Frequency: "10s",
|
||||
Settings: simplejson.New(),
|
||||
}
|
||||
|
||||
err := CreateAlertNotificationCommand(cmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result.Id, ShouldNotEqual, 0)
|
||||
So(cmd.Result.OrgId, ShouldNotEqual, 0)
|
||||
So(cmd.Result.Type, ShouldEqual, "email")
|
||||
So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
|
||||
|
||||
Convey("Cannot save Alert Notification with the same name", func() {
|
||||
err = CreateAlertNotificationCommand(cmd)
|
||||
@ -45,25 +144,42 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
|
||||
Convey("Can update alert notification", func() {
|
||||
newCmd := &m.UpdateAlertNotificationCommand{
|
||||
Name: "NewName",
|
||||
Type: "webhook",
|
||||
OrgId: cmd.Result.OrgId,
|
||||
Settings: simplejson.New(),
|
||||
Id: cmd.Result.Id,
|
||||
Name: "NewName",
|
||||
Type: "webhook",
|
||||
OrgId: cmd.Result.OrgId,
|
||||
SendReminder: true,
|
||||
Frequency: "60s",
|
||||
Settings: simplejson.New(),
|
||||
Id: cmd.Result.Id,
|
||||
}
|
||||
err := UpdateAlertNotification(newCmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(newCmd.Result.Name, ShouldEqual, "NewName")
|
||||
So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second)
|
||||
})
|
||||
|
||||
Convey("Can update alert notification to disable sending of reminders", func() {
|
||||
newCmd := &m.UpdateAlertNotificationCommand{
|
||||
Name: "NewName",
|
||||
Type: "webhook",
|
||||
OrgId: cmd.Result.OrgId,
|
||||
SendReminder: false,
|
||||
Settings: simplejson.New(),
|
||||
Id: cmd.Result.Id,
|
||||
}
|
||||
err := UpdateAlertNotification(newCmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(newCmd.Result.SendReminder, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can search using an array of ids", func() {
|
||||
cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
|
||||
cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
|
||||
cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()}
|
||||
cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, Settings: simplejson.New()}
|
||||
cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
|
||||
otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, Settings: simplejson.New()}
|
||||
otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
|
||||
|
||||
So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil)
|
||||
So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil)
|
||||
|
@ -65,6 +65,13 @@ func addAlertMigrations(mg *Migrator) {
|
||||
mg.AddMigration("Add column is_default", NewAddColumnMigration(alert_notification, &Column{
|
||||
Name: "is_default", Type: DB_Bool, Nullable: false, Default: "0",
|
||||
}))
|
||||
mg.AddMigration("Add column frequency", NewAddColumnMigration(alert_notification, &Column{
|
||||
Name: "frequency", Type: DB_BigInt, Nullable: true,
|
||||
}))
|
||||
mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{
|
||||
Name: "send_reminder", Type: DB_Bool, Nullable: true, Default: "0",
|
||||
}))
|
||||
|
||||
mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
|
||||
|
||||
mg.AddMigration("Update alert table charset", NewTableCharsetMigration("alert", []*Column{
|
||||
@ -82,4 +89,22 @@ func addAlertMigrations(mg *Migrator) {
|
||||
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "settings", Type: DB_Text, Nullable: false},
|
||||
}))
|
||||
|
||||
notification_journal := Table{
|
||||
Name: "alert_notification_journal",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "alert_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "notifier_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "sent_at", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "success", Type: DB_Bool, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType},
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal))
|
||||
mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0]))
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ func (ss *SqlStore) inTransactionWithRetryCtx(ctx context.Context, callback dbTr
|
||||
if len(sess.events) > 0 {
|
||||
for _, e := range sess.events {
|
||||
if err = bus.Publish(e); err != nil {
|
||||
log.Error(3, "Failed to publish event after commit", err)
|
||||
log.Error(3, "Failed to publish event after commit. error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,11 +13,19 @@ func init() {
|
||||
bus.AddHandler("sql", GetDataSourceStats)
|
||||
bus.AddHandler("sql", GetDataSourceAccessStats)
|
||||
bus.AddHandler("sql", GetAdminStats)
|
||||
bus.AddHandlerCtx("sql", GetAlertNotifiersUsageStats)
|
||||
bus.AddHandlerCtx("sql", GetSystemUserCountStats)
|
||||
}
|
||||
|
||||
var activeUserTimeLimit = time.Hour * 24 * 30
|
||||
|
||||
func GetAlertNotifiersUsageStats(ctx context.Context, query *m.GetAlertNotifierUsageStatsQuery) error {
|
||||
var rawSql = `SELECT COUNT(*) as count, type FROM alert_notification GROUP BY type`
|
||||
query.Result = make([]*m.NotifierUsageStats, 0)
|
||||
err := x.SQL(rawSql).Find(&query.Result)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
|
||||
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
|
||||
query.Result = make([]*m.DataSourceStats, 0)
|
||||
|
@ -36,5 +36,11 @@ func TestStatsDataAccess(t *testing.T) {
|
||||
err := GetDataSourceAccessStats(&query)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Get alert notifier stats should not results in error", func() {
|
||||
query := m.GetAlertNotifierUsageStatsQuery{}
|
||||
err := GetAlertNotifiersUsageStats(context.Background(), &query)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ func inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc,
|
||||
if len(sess.events) > 0 {
|
||||
for _, e := range sess.events {
|
||||
if err = bus.Publish(e); err != nil {
|
||||
log.Error(3, "Failed to publish event after commit", err)
|
||||
log.Error(3, "Failed to publish event after commit. error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -164,8 +164,10 @@ var (
|
||||
Quota QuotaSettings
|
||||
|
||||
// Alerting
|
||||
AlertingEnabled bool
|
||||
ExecuteAlerts bool
|
||||
AlertingEnabled bool
|
||||
ExecuteAlerts bool
|
||||
AlertingErrorOrTimeout string
|
||||
AlertingNoDataOrNullValues string
|
||||
|
||||
// Explore UI
|
||||
ExploreEnabled bool
|
||||
@ -197,6 +199,7 @@ type Cfg struct {
|
||||
ImagesDir string
|
||||
PhantomDir string
|
||||
RendererUrl string
|
||||
RendererCallbackUrl string
|
||||
DisableBruteForceLoginProtection bool
|
||||
|
||||
TempDataLifetime time.Duration
|
||||
@ -324,7 +327,7 @@ func getCommandLineProperties(args []string) map[string]string {
|
||||
trimmed := strings.TrimPrefix(arg, "cfg:")
|
||||
parts := strings.Split(trimmed, "=")
|
||||
if len(parts) != 2 {
|
||||
log.Fatal(3, "Invalid command line argument", arg)
|
||||
log.Fatal(3, "Invalid command line argument. argument: %v", arg)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -641,6 +644,18 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
// Rendering
|
||||
renderSec := iniFile.Section("rendering")
|
||||
cfg.RendererUrl = renderSec.Key("server_url").String()
|
||||
cfg.RendererCallbackUrl = renderSec.Key("callback_url").String()
|
||||
if cfg.RendererCallbackUrl == "" {
|
||||
cfg.RendererCallbackUrl = AppUrl
|
||||
} else {
|
||||
if cfg.RendererCallbackUrl[len(cfg.RendererCallbackUrl)-1] != '/' {
|
||||
cfg.RendererCallbackUrl += "/"
|
||||
}
|
||||
_, err := url.Parse(cfg.RendererCallbackUrl)
|
||||
if err != nil {
|
||||
log.Fatal(4, "Invalid callback_url(%s): %s", cfg.RendererCallbackUrl, err)
|
||||
}
|
||||
}
|
||||
cfg.ImagesDir = filepath.Join(DataPath, "png")
|
||||
cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
|
||||
cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
|
||||
@ -659,6 +674,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
alerting := iniFile.Section("alerting")
|
||||
AlertingEnabled = alerting.Key("enabled").MustBool(true)
|
||||
ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true)
|
||||
AlertingErrorOrTimeout = alerting.Key("error_or_timeout").MustString("alerting")
|
||||
AlertingNoDataOrNullValues = alerting.Key("nodata_or_nullvalues").MustString("no_data")
|
||||
|
||||
explore := iniFile.Section("explore")
|
||||
ExploreEnabled = explore.Key("enabled").MustBool(false)
|
||||
|
@ -5,6 +5,7 @@ type OAuthInfo struct {
|
||||
Scopes []string
|
||||
AuthUrl, TokenUrl string
|
||||
Enabled bool
|
||||
EmailAttributeName string
|
||||
AllowedDomains []string
|
||||
HostedDomain string
|
||||
ApiUrl string
|
||||
|
@ -20,6 +20,7 @@ func TestLoadingSettings(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(AdminUser, ShouldEqual, "admin")
|
||||
So(cfg.RendererCallbackUrl, ShouldEqual, "http://localhost:3000/")
|
||||
})
|
||||
|
||||
Convey("Should be able to override via environment variables", func() {
|
||||
@ -178,5 +179,15 @@ func TestLoadingSettings(t *testing.T) {
|
||||
So(InstanceName, ShouldEqual, hostname)
|
||||
})
|
||||
|
||||
Convey("Reading callback_url should add trailing slash", func() {
|
||||
cfg := NewCfg()
|
||||
cfg.Load(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:rendering.callback_url=http://myserver/renderer"},
|
||||
})
|
||||
|
||||
So(cfg.RendererCallbackUrl, ShouldEqual, "http://myserver/renderer/")
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ type SocialGenericOAuth struct {
|
||||
allowedOrganizations []string
|
||||
apiUrl string
|
||||
allowSignup bool
|
||||
emailAttributeName string
|
||||
teamIds []int
|
||||
}
|
||||
|
||||
@ -264,8 +265,9 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
|
||||
return data.Email
|
||||
}
|
||||
|
||||
if data.Attributes["email:primary"] != nil {
|
||||
return data.Attributes["email:primary"][0]
|
||||
emails, ok := data.Attributes[s.emailAttributeName]
|
||||
if ok && len(emails) != 0 {
|
||||
return emails[0]
|
||||
}
|
||||
|
||||
if data.Upn != "" {
|
||||
|
@ -60,21 +60,22 @@ func NewOAuthService() {
|
||||
for _, name := range allOauthes {
|
||||
sec := setting.Raw.Section("auth." + name)
|
||||
info := &setting.OAuthInfo{
|
||||
ClientId: sec.Key("client_id").String(),
|
||||
ClientSecret: sec.Key("client_secret").String(),
|
||||
Scopes: util.SplitString(sec.Key("scopes").String()),
|
||||
AuthUrl: sec.Key("auth_url").String(),
|
||||
TokenUrl: sec.Key("token_url").String(),
|
||||
ApiUrl: sec.Key("api_url").String(),
|
||||
Enabled: sec.Key("enabled").MustBool(),
|
||||
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
|
||||
HostedDomain: sec.Key("hosted_domain").String(),
|
||||
AllowSignup: sec.Key("allow_sign_up").MustBool(),
|
||||
Name: sec.Key("name").MustString(name),
|
||||
TlsClientCert: sec.Key("tls_client_cert").String(),
|
||||
TlsClientKey: sec.Key("tls_client_key").String(),
|
||||
TlsClientCa: sec.Key("tls_client_ca").String(),
|
||||
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
|
||||
ClientId: sec.Key("client_id").String(),
|
||||
ClientSecret: sec.Key("client_secret").String(),
|
||||
Scopes: util.SplitString(sec.Key("scopes").String()),
|
||||
AuthUrl: sec.Key("auth_url").String(),
|
||||
TokenUrl: sec.Key("token_url").String(),
|
||||
ApiUrl: sec.Key("api_url").String(),
|
||||
Enabled: sec.Key("enabled").MustBool(),
|
||||
EmailAttributeName: sec.Key("email_attribute_name").String(),
|
||||
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
|
||||
HostedDomain: sec.Key("hosted_domain").String(),
|
||||
AllowSignup: sec.Key("allow_sign_up").MustBool(),
|
||||
Name: sec.Key("name").MustString(name),
|
||||
TlsClientCert: sec.Key("tls_client_cert").String(),
|
||||
TlsClientKey: sec.Key("tls_client_key").String(),
|
||||
TlsClientCa: sec.Key("tls_client_ca").String(),
|
||||
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
|
||||
}
|
||||
|
||||
if !info.Enabled {
|
||||
@ -153,6 +154,7 @@ func NewOAuthService() {
|
||||
allowedDomains: info.AllowedDomains,
|
||||
apiUrl: info.ApiUrl,
|
||||
allowSignup: info.AllowSignup,
|
||||
emailAttributeName: info.EmailAttributeName,
|
||||
teamIds: sec.Key("team_ids").Ints(","),
|
||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
// add move to lodash for backward compatabiltiy
|
||||
_.move = function(array, fromIndex, toIndex) {
|
||||
_.move = (array, fromIndex, toIndex) => {
|
||||
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
|
||||
return array;
|
||||
};
|
||||
@ -76,9 +76,9 @@ export class GrafanaApp {
|
||||
$provide.decorator('$http', [
|
||||
'$delegate',
|
||||
'$templateCache',
|
||||
function($delegate, $templateCache) {
|
||||
($delegate, $templateCache) => {
|
||||
const get = $delegate.get;
|
||||
$delegate.get = function(url, config) {
|
||||
$delegate.get = (url, config) => {
|
||||
if (url.match(/\.html$/)) {
|
||||
// some template's already exist in the cache
|
||||
if (!$templateCache.get(url)) {
|
||||
@ -105,9 +105,9 @@ export class GrafanaApp {
|
||||
'react',
|
||||
];
|
||||
|
||||
const module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
|
||||
const moduleTypes = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
|
||||
|
||||
_.each(module_types, type => {
|
||||
_.each(moduleTypes, type => {
|
||||
const moduleName = 'grafana.' + type;
|
||||
this.useModule(angular.module(moduleName, []));
|
||||
});
|
||||
@ -135,7 +135,7 @@ export class GrafanaApp {
|
||||
this.preBootModules = null;
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
.catch(err => {
|
||||
console.log('Application boot failed:', err);
|
||||
});
|
||||
}
|
||||
|
@ -173,6 +173,12 @@ export class Explore extends React.Component<any, ExploreState> {
|
||||
datasource.init();
|
||||
}
|
||||
|
||||
// Keep queries but reset edit state
|
||||
const nextQueries = this.state.queries.map(q => ({
|
||||
...q,
|
||||
edited: false,
|
||||
}));
|
||||
|
||||
this.setState(
|
||||
{
|
||||
datasource,
|
||||
@ -182,6 +188,7 @@ export class Explore extends React.Component<any, ExploreState> {
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
datasourceLoading: false,
|
||||
queries: nextQueries,
|
||||
},
|
||||
() => datasourceError === null && this.onSubmit()
|
||||
);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user