diff --git a/.bra.toml b/.bra.toml
index aa7a1680adc..5be42ceebbf 100644
--- a/.bra.toml
+++ b/.bra.toml
@@ -1,7 +1,7 @@
[run]
init_cmds = [
["go", "run", "build.go", "-dev", "build-server"],
- ["./bin/grafana-server", "cfg:app_mode=development"]
+ ["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"]
]
watch_all = true
follow_symlinks = true
@@ -14,5 +14,5 @@ watch_exts = [".go", ".ini", ".toml", ".template.html"]
build_delay = 1500
cmds = [
["go", "run", "build.go", "-dev", "build-server"],
- ["./bin/grafana-server", "cfg:app_mode=development"]
+ ["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"]
]
diff --git a/.circleci/config.yml b/.circleci/config.yml
index b173dbff481..3dd8f800b94 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -85,12 +85,13 @@ jobs:
- run: 'go get -u github.com/tsenart/deadcode'
- run: 'go get -u github.com/jgautheron/goconst/cmd/goconst'
- run: 'go get -u github.com/gordonklaus/ineffassign'
+ - run: 'go get -u honnef.co/go/tools/cmd/megacheck'
- run: 'go get -u github.com/opennota/check/cmd/structcheck'
- run: 'go get -u github.com/mdempsky/unconvert'
- run: 'go get -u github.com/opennota/check/cmd/varcheck'
- run:
name: run linters
- command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=goconst --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
+ command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=goconst --enable=gofmt --enable=ineffassign --enable=megacheck --enable=structcheck --enable=unconvert --enable=varcheck ./...'
- run:
name: run go vet
command: 'go vet ./pkg/...'
@@ -126,7 +127,7 @@ jobs:
build-all:
docker:
- - image: grafana/build-container:1.2.0
+ - image: grafana/build-container:1.2.1
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -161,8 +162,8 @@ jobs:
name: Build Grafana.com master publisher
command: 'go build -o scripts/publish scripts/build/publish.go'
- run:
- name: Build Grafana.com release publisher
- command: 'cd scripts/build/release_publisher && go build -o release_publisher .'
+ name: Test and build Grafana.com release publisher
+ command: 'cd scripts/build/release_publisher && go test . && go build -o release_publisher .'
- persist_to_workspace:
root: .
paths:
@@ -174,7 +175,7 @@ jobs:
build:
docker:
- - image: grafana/build-container:1.2.0
+ - image: grafana/build-container:1.2.1
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -190,6 +191,9 @@ jobs:
- run:
name: sha-sum packages
command: 'go run build.go sha-dist'
+ - run:
+ name: Test Grafana.com release publisher
+ command: 'cd scripts/build/release_publisher && go test .'
- persist_to_workspace:
root: .
paths:
@@ -240,7 +244,7 @@ jobs:
build-enterprise:
docker:
- - image: grafana/build-container:1.2.0
+ - image: grafana/build-container:1.2.1
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -272,7 +276,7 @@ jobs:
build-all-enterprise:
docker:
- - image: grafana/build-container:1.2.0
+ - image: grafana/build-container:1.2.1
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -358,6 +362,9 @@ jobs:
- run:
name: deploy to gcp
command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/release'
+ - run:
+ name: Deploy to Grafana.com
+ command: './scripts/build/publish.sh --enterprise'
deploy-master:
docker:
@@ -506,6 +513,7 @@ workflows:
- grafana-docker-release:
requires:
- build-all
+ - build-all-enterprise
- test-backend
- test-frontend
- codespell
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2461faf8cac..9fe9edf30eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,37 +1,72 @@
# 5.4.0 (unreleased)
+* **Cloudwatch**: Fix invalid time range causes segmentation fault [#14150](https://github.com/grafana/grafana/issues/14150)
+
+### 5.4.0-beta1 fixes
+* **Graph**: Fix legend always visible even if configured to be hidden [#14144](https://github.com/grafana/grafana/issues/14144)
+* **Elasticsearch**: Fix regression when using datasource version 6.0+ and alerting [#14175](https://github.com/grafana/grafana/pull/14175)
+
+# 5.4.0-beta1 (2018-11-20)
+
### New Features
+* **Alerting**: Introduce alert debouncing with the `FOR` setting. [#7886](https://github.com/grafana/grafana/issues/7886) & [#6202](https://github.com/grafana/grafana/issues/6202)
* **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat)
* **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
* **MySQL**: Graphical query builder [#13762](https://github.com/grafana/grafana/issues/13762), thx [svenklemm](https://github.com/svenklemm)
* **MySQL**: Support connecting thru Unix socket for MySQL datasource [#12342](https://github.com/grafana/grafana/issues/12342), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
* **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
* **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
+* **Teams**: Team preferences (theme, home dashboard, timezone) support [#12550](https://github.com/grafana/grafana/issues/12550)
+* **Graph**: Time regions support enabling highlight of weekdays and/or certain timespans [#5930](https://github.com/grafana/grafana/issues/5930)
+* **OAuth**: Automatic redirect to sign-in with OAuth [#11893](https://github.com/grafana/grafana/issues/11893), thx [@Nick-Triller](https://github.com/Nick-Triller)
+* **Stackdriver**: Template query editor [#13561](https://github.com/grafana/grafana/issues/13561)
### Minor
+* **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043)
* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
* **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy)
+* **Cloudwatch**: CloudHSM metrics and dimensions [#14129](https://github.com/grafana/grafana/pull/14129), thx [@daktari](https://github.com/daktari)
+* **Cloudwatch**: Enable using variables in the stats field [#13810](https://github.com/grafana/grafana/issues/13810), thx [@mtanda](https://github.com/mtanda)
* **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
-* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
-* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
-* **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
-* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
-* **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
+* **Elasticsearch**: Fix switching to/from es raw document metric query [#6367](https://github.com/grafana/grafana/issues/6367)
+* **Elasticsearch**: Fix deprecation warning about terms aggregation order key in Elasticsearch 6.x [#11977](https://github.com/grafana/grafana/issues/11977)
+* **Graph**: Render dots when no connecting line can be made [#13605](https://github.com/grafana/grafana/issues/13605), thx [@jsferrei](https://github.com/jsferrei)
* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme)
+* **Singlestat**: Fix XSS in prefix/postfix [#13946](https://github.com/grafana/grafana/issues/13946), thx [@cinaglia](https://github.com/cinaglia)
+* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
+* **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
* **Alerting**: More options for the Slack Alert notifier [#13993](https://github.com/grafana/grafana/issues/13993), thx [@andreykaipov](https://github.com/andreykaipov)
+* **Alerting**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
+* **Alerting**: Increase Telegram captions length limit [#13876](https://github.com/grafana/grafana/pull/13876), thx [@skgsergio](https://github.com/skgsergio)
+* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
+* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
+* **OAuth**: Fix Google OAuth relies on email, not google account id [#13924](https://github.com/grafana/grafana/issues/13924), thx [@vinicyusmacedo](https://github.com/vinicyusmacedo)
+* **Dashboard**: Toggle legend using keyboard shortcut [#13655](https://github.com/grafana/grafana/issues/13655), thx [@davewat](https://github.com/davewat)
+* **Dashboard**: Fix render dashboard row drag handle only in edit mode [#13555](https://github.com/grafana/grafana/issues/13555), thx [@praveensastry](https://github.com/praveensastry)
+* **Teams**: Fix cannot select team if not included in initial search [#13425](https://github.com/grafana/grafana/issues/13425)
+* **Render**: Support full height screenshots using phantomjs render script [#13352](https://github.com/grafana/grafana/pull/13352), thx [@amuraru](https://github.com/amuraru)
+* **HTTP API**: Support retrieving teams by user [#14120](https://github.com/grafana/grafana/pull/14120), thx [@supercharlesliu](https://github.com/supercharlesliu)
+* **Metrics**: Add basic authentication to metrics endpoint [#13577](https://github.com/grafana/grafana/issues/13577), thx [@bobmshannon](https://github.com/bobmshannon)
### Breaking changes
-* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
+* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited).
-# 5.3.3 (unreleased)
+# 5.3.4 (2018-11-13)
* **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322)
* **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769)
* **Dashboard**: Fix datasource selection in panel by enter key [#13932](https://github.com/grafana/grafana/issues/13932)
* **Graph**: Fix table legend height when positioned below graph and using Internet Explorer 11 [#13903](https://github.com/grafana/grafana/issues/13903)
+* **Dataproxy**: Drop origin and referer http headers [#13328](https://github.com/grafana/grafana/issues/13328) [#13949](https://github.com/grafana/grafana/issues/13949), thx [@roidelapluie](https://github.com/roidelapluie)
+
+# 5.3.3 (2018-11-13)
+
+### File Exfiltration vulnerability Security fix
+
+See [security announcement](https://community.grafana.com/t/grafana-5-3-3-and-4-6-5-security-update/11961) for details.
# 5.3.2 (2018-10-24)
diff --git a/Dockerfile b/Dockerfile
index 28dd71952af..65260e1a6a8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -50,7 +50,8 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
WORKDIR $GF_PATHS_HOME
-RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
+RUN apt-get update && apt-get upgrade -y && \
+ apt-get install -qq -y libfontconfig ca-certificates && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
diff --git a/Gopkg.lock b/Gopkg.lock
index 4286add847d..1e06d4cbc5d 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -2,30 +2,39 @@
[[projects]]
+ digest = "1:f8ad8a53fa865a70efbe215b0ca34735523f50ea39e0efde319ab6fc80089b44"
name = "cloud.google.com/go"
packages = ["compute/metadata"]
+ pruneopts = "NUT"
revision = "056a55f54a6cc77b440b31a56a5e7c3982d32811"
version = "v0.22.0"
[[projects]]
+ digest = "1:167b6f65a6656de568092189ae791253939f076df60231fdd64588ac703892a1"
name = "github.com/BurntSushi/toml"
packages = ["."]
+ pruneopts = "NUT"
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
version = "v0.3.0"
[[projects]]
branch = "master"
+ digest = "1:7d23e6e1889b8bb4bbb37a564708fdab4497ce232c3a99d66406c975b642a6ff"
name = "github.com/Unknwon/com"
packages = ["."]
+ pruneopts = "NUT"
revision = "7677a1d7c1137cd3dd5ba7a076d0c898a1ef4520"
[[projects]]
branch = "master"
+ digest = "1:1610787cd9726e29d8fecc2a80e43e4fced008a1f560fec6688fc4d946f17835"
name = "github.com/VividCortex/mysqlerr"
packages = ["."]
+ pruneopts = "NUT"
revision = "6c6b55f8796f578c870b7e19bafb16103bc40095"
[[projects]]
+ digest = "1:58294d68772aab5a8941b7d5d228eff7cccf63f895e914bc9bc38fda80471ea5"
name = "github.com/aws/aws-sdk-go"
packages = [
"aws",
@@ -61,405 +70,523 @@
"service/ec2",
"service/ec2/ec2iface",
"service/s3",
- "service/sts"
+ "service/sts",
]
+ pruneopts = "NUT"
revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
version = "v1.14.12"
[[projects]]
branch = "master"
+ digest = "1:79cad073c7be02632d3fa52f62486848b089f560db1e94536de83a408c0f4726"
name = "github.com/benbjohnson/clock"
packages = ["."]
+ pruneopts = "NUT"
revision = "7dc76406b6d3c05b5f71a86293cbcf3c4ea03b19"
[[projects]]
branch = "master"
+ digest = "1:707ebe952a8b3d00b343c01536c79c73771d100f63ec6babeaed5c79e2b8a8dd"
name = "github.com/beorn7/perks"
packages = ["quantile"]
+ pruneopts = "NUT"
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
branch = "master"
+ digest = "1:433a2ff0ef4e2f8634614aab3174783c5ff80120b487712db96cc3712f409583"
name = "github.com/bmizerany/assert"
packages = ["."]
+ pruneopts = "NUT"
revision = "b7ed37b82869576c289d7d97fb2bbd8b64a0cb28"
[[projects]]
branch = "master"
+ digest = "1:d8f9145c361920507a4f85ffb7f70b96beaedacba2ce8c00aa663adb08689d3e"
name = "github.com/bradfitz/gomemcache"
packages = ["memcache"]
+ pruneopts = "NUT"
revision = "1952afaa557dc08e8e0d89eafab110fb501c1a2b"
[[projects]]
branch = "master"
+ digest = "1:8ecb89af7dfe3ac401bdb0c9390b134ef96a97e85f732d2b0604fb7b3977839f"
name = "github.com/codahale/hdrhistogram"
packages = ["."]
+ pruneopts = "NUT"
revision = "3a0bb77429bd3a61596f5e8a3172445844342120"
[[projects]]
+ digest = "1:5dba68a1600a235630e208cb7196b24e58fcbb77bb7a6bec08fcd23f081b0a58"
name = "github.com/codegangsta/cli"
packages = ["."]
+ pruneopts = "NUT"
revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
version = "v1.20.0"
[[projects]]
+ digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
+ pruneopts = "NUT"
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
+ digest = "1:1b318d2dd6cea8a1a8d8ec70348852303bd3e491df74e8bca6e32eb5a4d06970"
name = "github.com/denisenkom/go-mssqldb"
packages = [
".",
- "internal/cp"
+ "internal/cp",
]
+ pruneopts = "NUT"
revision = "270bc3860bb94dd3a3ffd047377d746c5e276726"
[[projects]]
branch = "master"
+ digest = "1:2da5f11ad66ff01a27a5c3dba4620b7eee2327be75b32c9ee9f87c9a8001ecbf"
name = "github.com/facebookgo/inject"
packages = ["."]
+ pruneopts = "NUT"
revision = "cc1aa653e50f6a9893bcaef89e673e5b24e1e97b"
[[projects]]
branch = "master"
+ digest = "1:1108df7f658c90db041e0d6174d55be689aaeb0585913b9c3c7aab51a3a6b2b1"
name = "github.com/facebookgo/structtag"
packages = ["."]
+ pruneopts = "NUT"
revision = "217e25fb96916cc60332e399c9aa63f5c422ceed"
[[projects]]
+ digest = "1:ade392a843b2035effb4b4a2efa2c3bab3eb29b992e98bacf9c898b0ecb54e45"
name = "github.com/fatih/color"
packages = ["."]
+ pruneopts = "NUT"
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
+ digest = "1:e05711632e1515319b014e8fe4cbe1d30ab024c473403f60cf0fdeb4c586a474"
name = "github.com/go-ini/ini"
packages = ["."]
+ pruneopts = "NUT"
revision = "6529cf7c58879c08d927016dde4477f18a0634cb"
version = "v1.36.0"
[[projects]]
+ digest = "1:7e1c00b9959544fa1ccca7cf0407a5b29ac6d5201059c4fac6f599cb99bfd24d"
name = "github.com/go-ldap/ldap"
packages = ["."]
+ pruneopts = "NUT"
revision = "bb7a9ca6e4fbc2129e3db588a34bc970ffe811a9"
version = "v2.5.1"
[[projects]]
branch = "master"
+ digest = "1:682a0aca743a1a4a36697f3d7f86c0ed403c4e3a780db9935f633242855eac9c"
name = "github.com/go-macaron/binding"
packages = ["."]
+ pruneopts = "NUT"
revision = "ac54ee249c27dca7e76fad851a4a04b73bd1b183"
[[projects]]
branch = "master"
+ digest = "1:6326b27f8e0c8e135c8674ddbc619fae879664ac832e8e6fa6a23ce0d279ed4d"
name = "github.com/go-macaron/gzip"
packages = ["."]
+ pruneopts = "NUT"
revision = "cad1c6580a07c56f5f6bc52d66002a05985c5854"
[[projects]]
branch = "master"
+ digest = "1:fb8711b648d1ff03104fc1d9593a13cb1d5120be7ba2b01641c14ccae286a9e3"
name = "github.com/go-macaron/inject"
packages = ["."]
+ pruneopts = "NUT"
revision = "d8a0b8677191f4380287cfebd08e462217bac7ad"
[[projects]]
branch = "master"
+ digest = "1:21577aafe885f088e8086a3415f154c63c0b7ce956a6994df2ac5776bc01b7e3"
name = "github.com/go-macaron/session"
packages = [
".",
"memcache",
"postgres",
- "redis"
+ "redis",
]
- revision = "b8e286a0dba8f4999042d6b258daf51b31d08938"
+ pruneopts = "NUT"
+ revision = "068d408f9c54c7fa7fcc5e2bdd3241ab21280c9e"
[[projects]]
+ digest = "1:fddd4bada6100d6fc49a9f32f18ba5718db45a58e4b00aa6377e1cfbf06af34f"
name = "github.com/go-sql-driver/mysql"
packages = ["."]
+ pruneopts = "NUT"
revision = "2cc627ac8defc45d65066ae98f898166f580f9a4"
[[projects]]
+ digest = "1:a1efdbc2762667c8a41cbf02b19a0549c846bf2c1d08cad4f445e3344089f1f0"
name = "github.com/go-stack/stack"
packages = ["."]
+ pruneopts = "NUT"
revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
version = "v1.7.0"
[[projects]]
+ digest = "1:b9d4f09cdaaa9e7dca5ed0b501ca5519eb2168dd01fc5d174c54edfe42a7d5ed"
name = "github.com/go-xorm/builder"
packages = ["."]
+ pruneopts = "NUT"
revision = "bad0a612f0d6277b953910822ab5dfb30dd18237"
version = "v0.2.0"
[[projects]]
+ digest = "1:30fb106b0cd6d64ea6fccded579c8f7d788460092f885fcc8f3edd02fc2085a4"
name = "github.com/go-xorm/core"
packages = ["."]
+ pruneopts = "NUT"
revision = "da1adaf7a28ca792961721a34e6e04945200c890"
version = "v0.5.7"
[[projects]]
+ digest = "1:007d1354e4f44e6a393337e7623bcf911dfe75d6ef30fb767a6a0b65d302f5ed"
name = "github.com/go-xorm/xorm"
packages = ["."]
+ pruneopts = "NUT"
revision = "1933dd69e294c0a26c0266637067f24dbb25770c"
version = "v0.6.4"
[[projects]]
branch = "master"
+ digest = "1:ffbb19fb66f140b5ea059428d1f84246a055d1bc3d9456c1e5c3d143611f03d0"
name = "github.com/golang/protobuf"
packages = [
"proto",
"ptypes",
"ptypes/any",
"ptypes/duration",
- "ptypes/timestamp"
+ "ptypes/timestamp",
]
+ pruneopts = "NUT"
revision = "927b65914520a8b7d44f5c9057611cfec6b2e2d0"
[[projects]]
branch = "master"
+ digest = "1:f14d1b50e0075fb00177f12a96dd7addf93d1e2883c25befd17285b779549795"
name = "github.com/gopherjs/gopherjs"
packages = ["js"]
+ pruneopts = "NUT"
revision = "8dffc02ea1cb8398bb73f30424697c60fcf8d4c5"
[[projects]]
+ digest = "1:3b708ebf63bfa9ba3313bedb8526bc0bb284e51474e65e958481476a9d4a12aa"
name = "github.com/gorilla/websocket"
packages = ["."]
+ pruneopts = "NUT"
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
+ digest = "1:4e771d1c6e15ca4516ad971c34205c822b5cff2747179679d7b321e4e1bfe431"
name = "github.com/gosimple/slug"
packages = ["."]
+ pruneopts = "NUT"
revision = "e9f42fa127660e552d0ad2b589868d403a9be7c6"
version = "v1.1.1"
[[projects]]
branch = "master"
+ digest = "1:08e53c69cd267ef7d71eeae5d953153d0d2bc1b8e0b498731fe9acaead7001b6"
name = "github.com/grafana/grafana-plugin-model"
packages = [
"go/datasource",
- "go/renderer"
+ "go/renderer",
]
+ pruneopts = "NUT"
revision = "84176c64269d8060f99e750ee8aba6f062753336"
[[projects]]
branch = "master"
+ digest = "1:58ba5285227b0f635652cd4aa82c4cfd00b590191eadd823462f0c9f64e3ae07"
name = "github.com/hashicorp/go-hclog"
packages = ["."]
+ pruneopts = "NUT"
revision = "69ff559dc25f3b435631604f573a5fa1efdb6433"
[[projects]]
+ digest = "1:532090ffc3b05a7e4c0229dd2698d79149f2e0683df993224a8b202f607fb605"
name = "github.com/hashicorp/go-plugin"
packages = ["."]
+ pruneopts = "NUT"
revision = "e8d22c780116115ae5624720c9af0c97afe4f551"
[[projects]]
branch = "master"
+ digest = "1:8925116d1edcd85fc0c014e1aa69ce12892489b48ee633a605c46d893b8c151f"
name = "github.com/hashicorp/go-version"
packages = ["."]
+ pruneopts = "NUT"
revision = "23480c0665776210b5fbbac6eaaee40e3e6a96b7"
[[projects]]
branch = "master"
+ digest = "1:8deb0c5545c824dfeb0ac77ab8eb67a3d541eab76df5c85ce93064ef02d44cd0"
name = "github.com/hashicorp/yamux"
packages = ["."]
+ pruneopts = "NUT"
revision = "7221087c3d281fda5f794e28c2ea4c6e4d5c4558"
[[projects]]
+ digest = "1:efbe016b6d198cf44f1db0ed2fbdf1b36ebf1f6956cc9b76d6affa96f022d368"
name = "github.com/inconshreveable/log15"
packages = ["."]
+ pruneopts = "NUT"
revision = "0decfc6c20d9ca0ad143b0e89dcaa20f810b4fb3"
version = "v2.13"
[[projects]]
+ digest = "1:ac6d01547ec4f7f673311b4663909269bfb8249952de3279799289467837c3cc"
name = "github.com/jmespath/go-jmespath"
packages = ["."]
+ pruneopts = "NUT"
revision = "0b12d6b5"
[[projects]]
+ digest = "1:6ddab442e52381bab82fb6c07ef3f4b565ff7ec4b8fae96d8dd4b8573a460597"
name = "github.com/jtolds/gls"
packages = ["."]
+ pruneopts = "NUT"
revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064"
version = "v4.2.1"
[[projects]]
+ digest = "1:1da1796a71eb70f1e3e085984d044f67840bb0326816ec8276231aa87b1b9fc3"
name = "github.com/klauspost/compress"
packages = [
"flate",
- "gzip"
+ "gzip",
]
+ pruneopts = "NUT"
revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf"
version = "v1.2.1"
[[projects]]
+ digest = "1:5e55a8699c9ff7aba1e4c8952aeda209685d88d4cb63a8766c338e333b8e65d6"
name = "github.com/klauspost/cpuid"
packages = ["."]
+ pruneopts = "NUT"
revision = "ae7887de9fa5d2db4eaa8174a7eff2c1ac00f2da"
version = "v1.1"
[[projects]]
+ digest = "1:b95da1293525625ef6f07be79d537b9bf2ecd7901efcf9a92193edafbd55b9ef"
name = "github.com/klauspost/crc32"
packages = ["."]
+ pruneopts = "NUT"
revision = "cb6bfca970f6908083f26f39a79009d608efd5cd"
version = "v1.1"
[[projects]]
+ digest = "1:7b21c7fc5551b46d1308b4ffa9e9e49b66c7a8b0ba88c0130474b0e7a20d859f"
name = "github.com/kr/pretty"
packages = ["."]
+ pruneopts = "NUT"
revision = "73f6ac0b30a98e433b289500d779f50c1a6f0712"
version = "v0.1.0"
[[projects]]
+ digest = "1:c3a7836b5904db0f8b609595b619916a6831cb35b8b714aec39f96d00c6155d8"
name = "github.com/kr/text"
packages = ["."]
+ pruneopts = "NUT"
revision = "e2ffdb16a802fe2bb95e2e35ff34f0e53aeef34f"
version = "v0.1.0"
[[projects]]
branch = "master"
+ digest = "1:7a1e592f0349d56fac8ce47f28469e4e7f4ce637cb26f40c88da9dff25db1c98"
name = "github.com/lib/pq"
packages = [
".",
- "oid"
+ "oid",
]
+ pruneopts = "NUT"
revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
[[projects]]
+ digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061"
name = "github.com/mattn/go-colorable"
packages = ["."]
+ pruneopts = "NUT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
+ digest = "1:bc4f7eec3b7be8c6cb1f0af6c1e3333d5bb71072951aaaae2f05067b0803f287"
name = "github.com/mattn/go-isatty"
packages = ["."]
+ pruneopts = "NUT"
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
+ digest = "1:536979f1c56397dbf91c2785159b37dec37e35d3bffa3cd1cfe66d25f51f8088"
name = "github.com/mattn/go-sqlite3"
packages = ["."]
+ pruneopts = "NUT"
revision = "323a32be5a2421b8c7087225079c6c900ec397cd"
version = "v1.7.0"
[[projects]]
+ digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6"
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
+ pruneopts = "NUT"
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
branch = "master"
+ digest = "1:18b773b92ac82a451c1276bd2776c1e55ce057ee202691ab33c8d6690efcc048"
name = "github.com/mitchellh/go-testing-interface"
packages = ["."]
+ pruneopts = "NUT"
revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28"
[[projects]]
+ digest = "1:3b517122f3aad1ecce45a630ea912b3092b4729f25532a911d0cb2935a1f9352"
name = "github.com/oklog/run"
packages = ["."]
+ pruneopts = "NUT"
revision = "4dadeb3030eda0273a12382bb2348ffc7c9d1a39"
version = "v1.0.0"
[[projects]]
+ digest = "1:7da29c22bcc5c2ffb308324377dc00b5084650348c2799e573ed226d8cc9faf0"
name = "github.com/opentracing/opentracing-go"
packages = [
".",
"ext",
- "log"
+ "log",
]
+ pruneopts = "NUT"
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
version = "v1.0.2"
[[projects]]
+ digest = "1:748946761cf99c8b73cef5a3c0ee3e040859dd713a20cece0d0e0dc04e6ceca7"
name = "github.com/patrickmn/go-cache"
packages = ["."]
+ pruneopts = "NUT"
revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0"
version = "v2.1.0"
[[projects]]
+ digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121"
name = "github.com/pkg/errors"
packages = ["."]
+ pruneopts = "NUT"
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
+ digest = "1:4759bed95e3a52febc18c071db28790a5c6e9e106ee201a37add6f6a056f8f9c"
name = "github.com/prometheus/client_golang"
packages = [
"api",
"api/prometheus/v1",
"prometheus",
- "prometheus/promhttp"
+ "prometheus/promhttp",
]
+ pruneopts = "NUT"
revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
version = "v0.9.0-pre1"
[[projects]]
branch = "master"
+ digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b"
name = "github.com/prometheus/client_model"
packages = ["go"]
+ pruneopts = "NUT"
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
+ digest = "1:768b555b86742de2f28beb37f1dedce9a75f91f871d75b5717c96399c1a78c08"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
- "model"
+ "model",
]
+ pruneopts = "NUT"
revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c"
[[projects]]
branch = "master"
+ digest = "1:c4a213a8d73fbb0b13f717ba7996116602ef18ecb42b91d77405877914cb0349"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
- "xfs"
+ "xfs",
]
+ pruneopts = "NUT"
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
[[projects]]
branch = "master"
+ digest = "1:16e2136a67ec44aa2d1d6b0fd65394b3c4a8b2a1b6730c77967f7b7b06b179b2"
name = "github.com/rainycape/unidecode"
packages = ["."]
+ pruneopts = "NUT"
revision = "cb7f23ec59bec0d61b19c56cd88cee3d0cc1870c"
[[projects]]
+ digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04"
name = "github.com/sergi/go-diff"
packages = ["diffmatchpatch"]
+ pruneopts = "NUT"
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
version = "v1.0.0"
[[projects]]
+ digest = "1:1f0b284a6858827de4c27c66b49b2b25df3e16b031c2b57b7892273131e7dd2b"
name = "github.com/smartystreets/assertions"
packages = [
".",
"internal/go-render/render",
- "internal/oglematchers"
+ "internal/oglematchers",
]
+ pruneopts = "NUT"
revision = "7678a5452ebea5b7090a6b163f844c133f523da2"
version = "1.8.3"
[[projects]]
+ digest = "1:7efd0b2309cdd6468029fa30c808c50a820c9344df07e1a4bbdaf18f282907aa"
name = "github.com/smartystreets/goconvey"
packages = [
"convey",
"convey/gotest",
- "convey/reporting"
+ "convey/reporting",
]
+ pruneopts = "NUT"
revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857"
version = "1.6.3"
[[projects]]
branch = "master"
+ digest = "1:a66add8dd963bfc72649017c1b321198f596cb4958cb1a11ff91a1be8691020b"
name = "github.com/teris-io/shortid"
packages = ["."]
+ pruneopts = "NUT"
revision = "771a37caa5cf0c81f585d7b6df4dfc77e0615b5c"
[[projects]]
+ digest = "1:3d48c38e0eca8c66df62379c5ae7a83fb5cd839b94f241354c07ba077da7bc45"
name = "github.com/uber/jaeger-client-go"
packages = [
".",
@@ -477,45 +604,55 @@
"thrift-gen/jaeger",
"thrift-gen/sampling",
"thrift-gen/zipkincore",
- "utils"
+ "utils",
]
+ pruneopts = "NUT"
revision = "b043381d944715b469fd6b37addfd30145ca1758"
version = "v2.14.0"
[[projects]]
+ digest = "1:0f09db8429e19d57c8346ad76fbbc679341fa86073d3b8fb5ac919f0357d8f4c"
name = "github.com/uber/jaeger-lib"
packages = ["metrics"]
+ pruneopts = "NUT"
revision = "ed3a127ec5fef7ae9ea95b01b542c47fbd999ce5"
version = "v1.5.0"
[[projects]]
+ digest = "1:4c7d12ad3ef47bb03892a52e2609dc9a9cff93136ca9c7d31c00b79fcbc23c7b"
name = "github.com/yudai/gojsondiff"
packages = [
".",
- "formatter"
+ "formatter",
]
+ pruneopts = "NUT"
revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6"
version = "1.0.0"
[[projects]]
branch = "master"
+ digest = "1:e50cbf8eba568d59b71e08c22c2a77809ed4646ae06ef4abb32b3d3d3fdb1a77"
name = "github.com/yudai/golcs"
packages = ["."]
+ pruneopts = "NUT"
revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68"
[[projects]]
branch = "master"
+ digest = "1:758f363e0dff33cf00b234be2efb12f919d79b42d5ae3909ff9eb69ef2c3cca5"
name = "golang.org/x/crypto"
packages = [
"ed25519",
"ed25519/internal/edwards25519",
"md4",
- "pbkdf2"
+ "pbkdf2",
]
+ pruneopts = "NUT"
revision = "1a580b3eff7814fc9b40602fd35256c63b50f491"
[[projects]]
branch = "master"
+ digest = "1:0b3fee9c4472022a0982ee0d81e08b3cc3e595f50befd7a4b358b48540d9d8c5"
name = "golang.org/x/net"
packages = [
"context",
@@ -525,35 +662,43 @@
"http2/hpack",
"idna",
"internal/timeseries",
- "trace"
+ "trace",
]
+ pruneopts = "NUT"
revision = "2491c5de3490fced2f6cff376127c667efeed857"
[[projects]]
branch = "master"
+ digest = "1:46bd4e66bfce5e77f08fc2e8dcacc3676e679241ce83d9c150ff0397d686dd44"
name = "golang.org/x/oauth2"
packages = [
".",
"google",
"internal",
"jws",
- "jwt"
+ "jwt",
]
+ pruneopts = "NUT"
revision = "cdc340f7c179dbbfa4afd43b7614e8fcadde4269"
[[projects]]
branch = "master"
+ digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239"
name = "golang.org/x/sync"
packages = ["errgroup"]
+ pruneopts = "NUT"
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
[[projects]]
branch = "master"
+ digest = "1:ec21c5bf0572488865b93e30ffd9132afbf85bec0b20c2d6cbcf349cf2031ed5"
name = "golang.org/x/sys"
packages = ["unix"]
+ pruneopts = "NUT"
revision = "7c87d13f8e835d2fb3a70a2912c811ed0c1d241b"
[[projects]]
+ digest = "1:e7071ed636b5422cc51c0e3a6cebc229d6c9fffc528814b519a980641422d619"
name = "golang.org/x/text"
packages = [
"collate",
@@ -569,12 +714,14 @@
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
- "unicode/rangetable"
+ "unicode/rangetable",
]
+ pruneopts = "NUT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
+ digest = "1:dbd5568923513ee74aa626d027e2a8a352cf8f35df41d19f4e34491d1858c38b"
name = "google.golang.org/appengine"
packages = [
".",
@@ -587,18 +734,22 @@
"internal/modules",
"internal/remote_api",
"internal/urlfetch",
- "urlfetch"
+ "urlfetch",
]
+ pruneopts = "NUT"
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[[projects]]
branch = "master"
+ digest = "1:3c24554c312721e98fa6b76403e7100cf974eb46b1255ea7fc6471db9a9ce498"
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
+ pruneopts = "NUT"
revision = "7bb2a897381c9c5ab2aeb8614f758d7766af68ff"
[[projects]]
+ digest = "1:840b77b6eb539b830bb760b6e30b688ed2ff484bd83466fce2395835ed9367fe"
name = "google.golang.org/grpc"
packages = [
".",
@@ -625,72 +776,167 @@
"stats",
"status",
"tap",
- "transport"
+ "transport",
]
+ pruneopts = "NUT"
revision = "1e2570b1b19ade82d8dbb31bba4e65e9f9ef5b34"
version = "v1.11.1"
[[projects]]
branch = "v3"
+ digest = "1:1244a9b3856f70d5ffb74bbfd780fc9d47f93f2049fa265c6fb602878f507bf8"
name = "gopkg.in/alexcesaro/quotedprintable.v3"
packages = ["."]
+ pruneopts = "NUT"
revision = "2caba252f4dc53eaf6b553000885530023f54623"
[[projects]]
+ digest = "1:aea6e9483c167cc6fdf1274c442558c5dda8fd3373372be04d98c79100868da1"
name = "gopkg.in/asn1-ber.v1"
packages = ["."]
+ pruneopts = "NUT"
revision = "379148ca0225df7a432012b8df0355c2a2063ac0"
version = "v1.2"
[[projects]]
+ digest = "1:24bfc2e8bf971485cb5ba0f0e5b08a1b806cca5828134df76b32d1ea50f2ab49"
name = "gopkg.in/bufio.v1"
packages = ["."]
+ pruneopts = "NUT"
revision = "567b2bfa514e796916c4747494d6ff5132a1dfce"
version = "v1"
[[projects]]
+ digest = "1:e05711632e1515319b014e8fe4cbe1d30ab024c473403f60cf0fdeb4c586a474"
name = "gopkg.in/ini.v1"
packages = ["."]
+ pruneopts = "NUT"
revision = "6529cf7c58879c08d927016dde4477f18a0634cb"
version = "v1.36.0"
[[projects]]
+ digest = "1:3b0cf3a465fd07f76e5fc1a9d0783c662dac0de9fc73d713ebe162768fd87b5f"
name = "gopkg.in/macaron.v1"
packages = ["."]
+ pruneopts = "NUT"
revision = "c1be95e6d21e769e44e1ec33cec9da5837861c10"
version = "v1.3.1"
[[projects]]
branch = "v2"
+ digest = "1:d52332f9e9f2c6343652e13aa3fd40cfd03353520c9a48d90f21215d3012d50f"
name = "gopkg.in/mail.v2"
packages = ["."]
+ pruneopts = "NUT"
revision = "5bc5c8bb07bd8d2803831fbaf8cbd630fcde2c68"
[[projects]]
+ digest = "1:00126f697efdcab42f07c89ac8bf0095fb2328aef6464e070055154088cea859"
name = "gopkg.in/redis.v2"
packages = ["."]
+ pruneopts = "NUT"
revision = "e6179049628164864e6e84e973cfb56335748dea"
version = "v2.3.2"
[[projects]]
+ digest = "1:a50fabe7a46692dc7c656310add3d517abe7914df02afd151ef84da884605dc8"
name = "gopkg.in/square/go-jose.v2"
packages = [
".",
"cipher",
- "json"
+ "json",
]
+ pruneopts = "NUT"
revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8"
version = "v2.1.9"
[[projects]]
+ branch = "v2"
+ digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082"
name = "gopkg.in/yaml.v2"
packages = ["."]
+ pruneopts = "NUT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
- version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
- inputs-digest = "6f7f271afd27f78b7d8ebe27436fee72c9925fb82a978bdc57fde44e01f3ca51"
+ input-imports = [
+ "github.com/BurntSushi/toml",
+ "github.com/Unknwon/com",
+ "github.com/VividCortex/mysqlerr",
+ "github.com/aws/aws-sdk-go/aws",
+ "github.com/aws/aws-sdk-go/aws/awserr",
+ "github.com/aws/aws-sdk-go/aws/awsutil",
+ "github.com/aws/aws-sdk-go/aws/credentials",
+ "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds",
+ "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds",
+ "github.com/aws/aws-sdk-go/aws/defaults",
+ "github.com/aws/aws-sdk-go/aws/ec2metadata",
+ "github.com/aws/aws-sdk-go/aws/endpoints",
+ "github.com/aws/aws-sdk-go/aws/request",
+ "github.com/aws/aws-sdk-go/aws/session",
+ "github.com/aws/aws-sdk-go/service/cloudwatch",
+ "github.com/aws/aws-sdk-go/service/ec2",
+ "github.com/aws/aws-sdk-go/service/ec2/ec2iface",
+ "github.com/aws/aws-sdk-go/service/s3",
+ "github.com/aws/aws-sdk-go/service/sts",
+ "github.com/benbjohnson/clock",
+ "github.com/bmizerany/assert",
+ "github.com/codegangsta/cli",
+ "github.com/davecgh/go-spew/spew",
+ "github.com/denisenkom/go-mssqldb",
+ "github.com/facebookgo/inject",
+ "github.com/fatih/color",
+ "github.com/go-ldap/ldap",
+ "github.com/go-macaron/binding",
+ "github.com/go-macaron/gzip",
+ "github.com/go-macaron/session",
+ "github.com/go-macaron/session/memcache",
+ "github.com/go-macaron/session/postgres",
+ "github.com/go-macaron/session/redis",
+ "github.com/go-sql-driver/mysql",
+ "github.com/go-stack/stack",
+ "github.com/go-xorm/core",
+ "github.com/go-xorm/xorm",
+ "github.com/gorilla/websocket",
+ "github.com/gosimple/slug",
+ "github.com/grafana/grafana-plugin-model/go/datasource",
+ "github.com/grafana/grafana-plugin-model/go/renderer",
+ "github.com/hashicorp/go-hclog",
+ "github.com/hashicorp/go-plugin",
+ "github.com/hashicorp/go-version",
+ "github.com/inconshreveable/log15",
+ "github.com/lib/pq",
+ "github.com/mattn/go-isatty",
+ "github.com/mattn/go-sqlite3",
+ "github.com/opentracing/opentracing-go",
+ "github.com/opentracing/opentracing-go/ext",
+ "github.com/opentracing/opentracing-go/log",
+ "github.com/patrickmn/go-cache",
+ "github.com/pkg/errors",
+ "github.com/prometheus/client_golang/api",
+ "github.com/prometheus/client_golang/api/prometheus/v1",
+ "github.com/prometheus/client_golang/prometheus",
+ "github.com/prometheus/client_golang/prometheus/promhttp",
+ "github.com/prometheus/client_model/go",
+ "github.com/prometheus/common/expfmt",
+ "github.com/prometheus/common/model",
+ "github.com/smartystreets/goconvey/convey",
+ "github.com/teris-io/shortid",
+ "github.com/uber/jaeger-client-go/config",
+ "github.com/yudai/gojsondiff",
+ "github.com/yudai/gojsondiff/formatter",
+ "golang.org/x/net/context/ctxhttp",
+ "golang.org/x/oauth2",
+ "golang.org/x/oauth2/google",
+ "golang.org/x/oauth2/jwt",
+ "golang.org/x/sync/errgroup",
+ "gopkg.in/ini.v1",
+ "gopkg.in/macaron.v1",
+ "gopkg.in/mail.v2",
+ "gopkg.in/square/go-jose.v2",
+ "gopkg.in/yaml.v2",
+ ]
solver-name = "gps-cdcl"
solver-version = 1
diff --git a/Makefile b/Makefile
index fcb740d2fac..6410714d4fc 100644
--- a/Makefile
+++ b/Makefile
@@ -25,7 +25,7 @@ build: build-go build-js
build-docker-dev:
@echo "\033[92mInfo:\033[0m the frontend code is expected to be built already."
- go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest
+ go run build.go -goos linux -pkg-arch amd64 ${OPT} build pkg-archive latest
cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
cd packaging/docker && docker build --tag grafana/grafana:dev .
diff --git a/build.go b/build.go
index a2a1fb825d9..9d5216de1d0 100644
--- a/build.go
+++ b/build.go
@@ -128,6 +128,8 @@ func main() {
if goos == linux {
createLinuxPackages()
}
+ case "pkg-archive":
+ grunt(gruntBuildArg("package")...)
case "pkg-rpm":
grunt(gruntBuildArg("release")...)
@@ -639,7 +641,7 @@ func shaFile(file string) error {
func shortenBuildId(buildId string) string {
buildId = strings.Replace(buildId, "-", "", -1)
- if (len(buildId) < 9) {
+ if len(buildId) < 9 {
return buildId
}
return buildId[0:8]
diff --git a/conf/defaults.ini b/conf/defaults.ini
index 679a6a88eb7..306c625d980 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -490,6 +490,10 @@ enabled = false
enabled = true
interval_seconds = 10
+#If both are set, basic auth will be required for the metrics endpoint.
+basic_auth_username =
+basic_auth_password =
+
# Send internal Grafana metrics to graphite
[metrics.graphite]
# Enable by setting the address setting (ex localhost:2003)
diff --git a/devenv/dev-dashboards/panel_tests_graph_time_regions.json b/devenv/dev-dashboards/panel_tests_graph_time_regions.json
new file mode 100644
index 00000000000..8d0bae1221c
--- /dev/null
+++ b/devenv/dev-dashboards/panel_tests_graph_time_regions.json
@@ -0,0 +1,511 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "gnetId": null,
+ "graphTooltip": 0,
+ "links": [],
+ "panels": [
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-testdata",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 2,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "random_walk",
+ "target": ""
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [
+ {
+ "colorMode": "gray",
+ "fill": true,
+ "fillColor": "rgba(255, 255, 255, 0.03)",
+ "from": "08:30",
+ "fromDayOfWeek": 1,
+ "line": false,
+ "lineColor": "rgba(255, 255, 255, 0.2)",
+ "op": "time",
+ "to": "16:45",
+ "toDayOfWeek": 5
+ }
+ ],
+ "timeShift": null,
+ "title": "Business Hours",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-testdata",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 8
+ },
+ "id": 4,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "A",
+ "scenarioId": "random_walk",
+ "target": ""
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [
+ {
+ "colorMode": "red",
+ "fill": true,
+ "fillColor": "rgba(255, 255, 255, 0.03)",
+ "from": "20:00",
+ "fromDayOfWeek": 7,
+ "line": false,
+ "lineColor": "rgba(255, 255, 255, 0.2)",
+ "op": "time",
+ "to": "23:00",
+ "toDayOfWeek": 7
+ }
+ ],
+ "timeShift": null,
+ "title": "Sunday's 20-23",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {
+ "A-series": "#d683ce"
+ },
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-testdata",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 16
+ },
+ "id": 3,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 0.5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "random_walk",
+ "target": ""
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [
+ {
+ "colorMode": "custom",
+ "fill": true,
+ "fillColor": "rgba(255, 0, 0, 0.22)",
+ "from": "",
+ "fromDayOfWeek": 1,
+ "line": true,
+ "lineColor": "rgba(255, 0, 0, 0.32)",
+ "op": "time",
+ "to": "",
+ "toDayOfWeek": 1
+ },
+ {
+ "colorMode": "custom",
+ "fill": true,
+ "fillColor": "rgba(255, 127, 0, 0.22)",
+ "fromDayOfWeek": 2,
+ "line": true,
+ "lineColor": "rgba(255, 127, 0, 0.32)",
+ "op": "time",
+ "toDayOfWeek": 2
+ },
+ {
+ "colorMode": "custom",
+ "fill": true,
+ "fillColor": "rgba(255, 255, 0, 0.22)",
+ "fromDayOfWeek": 3,
+ "line": true,
+ "lineColor": "rgba(255, 255, 0, 0.22)",
+ "op": "time",
+ "toDayOfWeek": 3
+ },
+ {
+ "colorMode": "custom",
+ "fill": true,
+ "fillColor": "rgba(0, 255, 0, 0.22)",
+ "fromDayOfWeek": 4,
+ "line": true,
+ "lineColor": "rgba(0, 255, 0, 0.32)",
+ "op": "time",
+ "toDayOfWeek": 4
+ },
+ {
+ "colorMode": "custom",
+ "fill": true,
+ "fillColor": "rgba(0, 0, 255, 0.22)",
+ "fromDayOfWeek": 5,
+ "line": true,
+ "lineColor": "rgba(0, 0, 255, 0.32)",
+ "op": "time",
+ "toDayOfWeek": 5
+ },
+ {
+ "colorMode": "custom",
+ "fill": true,
+ "fillColor": "rgba(75, 0, 130, 0.22)",
+ "fromDayOfWeek": 6,
+ "line": true,
+ "lineColor": "rgba(75, 0, 130, 0.32)",
+ "op": "time",
+ "toDayOfWeek": 6
+ },
+ {
+ "colorMode": "custom",
+ "fill": true,
+ "fillColor": "rgba(148, 0, 211, 0.22)",
+ "fromDayOfWeek": 7,
+ "line": true,
+ "lineColor": "rgba(148, 0, 211, 0.32)",
+ "op": "time",
+ "toDayOfWeek": 7
+ }
+ ],
+ "timeShift": null,
+ "title": "Each day of week",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-testdata",
+ "fill": 2,
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 24
+ },
+ "id": 5,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "",
+ "format": "time_series",
+ "intervalFactor": 1,
+ "refId": "A",
+ "scenarioId": "random_walk",
+ "target": ""
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [
+ {
+ "colorMode": "red",
+ "fill": false,
+ "from": "05:00",
+ "line": true,
+ "op": "time"
+ }
+ ],
+ "timeShift": null,
+ "title": "05:00",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ }
+ ],
+ "refresh": false,
+ "schemaVersion": 16,
+ "style": "dark",
+ "tags": [
+ "gdev",
+ "panel-tests"
+ ],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-30d",
+ "to": "now"
+ },
+ "timepicker": {
+ "refresh_intervals": [
+ "5s",
+ "10s",
+ "30s",
+ "1m",
+ "5m",
+ "15m",
+ "30m",
+ "1h",
+ "2h",
+ "1d"
+ ],
+ "time_options": [
+ "5m",
+ "15m",
+ "1h",
+ "6h",
+ "12h",
+ "24h",
+ "2d",
+ "7d",
+ "30d"
+ ]
+ },
+ "timezone": "browser",
+ "title": "Panel Tests - Graph (Time Regions)",
+ "uid": "XMjIZPmik",
+ "version": 1
+}
\ No newline at end of file
diff --git a/devenv/dev-dashboards/testdata_alerts.json b/devenv/dev-dashboards/testdata_alerts.json
index 8c2edebf155..9f36638c012 100644
--- a/devenv/dev-dashboards/testdata_alerts.json
+++ b/devenv/dev-dashboards/testdata_alerts.json
@@ -1,250 +1,681 @@
{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "gnetId": null,
+ "graphTooltip": 0,
+ "links": [],
+ "panels": [
+ {
+ "alert": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 60
+ ],
+ "type": "gt"
+ },
+ "query": {
+ "params": [
+ "A",
+ "5m",
+ "now"
+ ]
+ },
+ "reducer": {
+ "params": [],
+ "type": "avg"
+ },
+ "type": "query"
+ }
+ ],
+ "enabled": true,
+ "frequency": "60s",
+ "handler": 1,
+ "name": "TestData - Always OK",
+ "noDataState": "no_data",
+ "notifications": []
+ },
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-testdata",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "gridPos": {
+ "h": 7,
+ "w": 12,
+ "x": 0,
+ "y": 0
+ },
+ "id": 3,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "refId": "A",
+ "scenario": "random_walk",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,0",
+ "target": ""
+ }
+ ],
+ "thresholds": [
+ {
+ "colorMode": "critical",
+ "fill": true,
+ "line": true,
+ "op": "gt",
+ "value": 60
+ }
+ ],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Always OK",
+ "tooltip": {
+ "msResolution": false,
+ "shared": true,
+ "sort": 0,
+ "value_type": "cumulative"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": "125",
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "alert": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 177
+ ],
+ "type": "gt"
+ },
+ "query": {
+ "params": [
+ "A",
+ "5m",
+ "now"
+ ]
+ },
+ "reducer": {
+ "params": [],
+ "type": "avg"
+ },
+ "type": "query"
+ }
+ ],
+ "enabled": true,
+ "executionErrorState": "alerting",
+ "for": "0m",
+ "frequency": "60s",
+ "handler": 1,
+ "name": "TestData - Always Alerting",
+ "noDataState": "no_data",
+ "notifications": []
+ },
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-testdata",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "gridPos": {
+ "h": 7,
+ "w": 12,
+ "x": 12,
+ "y": 0
+ },
+ "id": 4,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "refId": "A",
+ "scenario": "random_walk",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "200,445,100,150,200,220,190",
+ "target": ""
+ }
+ ],
+ "thresholds": [
+ {
+ "colorMode": "critical",
+ "fill": true,
+ "line": true,
+ "op": "gt",
+ "value": 177
+ }
+ ],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Always Alerting",
+ "tooltip": {
+ "msResolution": false,
+ "shared": true,
+ "sort": 0,
+ "value_type": "cumulative"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "alert": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 1
+ ],
+ "type": "gt"
+ },
+ "operator": {
+ "type": "and"
+ },
+ "query": {
+ "params": [
+ "A",
+ "15m",
+ "now"
+ ]
+ },
+ "reducer": {
+ "params": [],
+ "type": "avg"
+ },
+ "type": "query"
+ }
+ ],
+ "executionErrorState": "alerting",
+ "for": "5m",
+ "frequency": "1m",
+ "handler": 1,
+ "name": "TestData - No data",
+ "noDataState": "no_data",
+ "notifications": []
+ },
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-testdata",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "gridPos": {
+ "h": 7,
+ "w": 12,
+ "x": 0,
+ "y": 7
+ },
+ "id": 5,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "refId": "A",
+ "scenario": "random_walk",
+ "scenarioId": "no_data_points",
+ "stringInput": "",
+ "target": ""
+ }
+ ],
+ "thresholds": [
+ {
+ "colorMode": "critical",
+ "fill": true,
+ "line": true,
+ "op": "gt",
+ "value": 1
+ }
+ ],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "No data",
+ "tooltip": {
+ "msResolution": false,
+ "shared": true,
+ "sort": 0,
+ "value_type": "cumulative"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "alert": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 177
+ ],
+ "type": "gt"
+ },
+ "operator": {
+ "type": "and"
+ },
+ "query": {
+ "params": [
+ "A",
+ "15m",
+ "now"
+ ]
+ },
+ "reducer": {
+ "params": [],
+ "type": "avg"
+ },
+ "type": "query"
+ }
+ ],
+ "executionErrorState": "alerting",
+ "for": "1m",
+ "frequency": "1m",
+ "handler": 1,
+ "name": "TestData - Always Pending",
+ "noDataState": "no_data",
+ "notifications": []
+ },
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-testdata",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "gridPos": {
+ "h": 7,
+ "w": 12,
+ "x": 12,
+ "y": 7
+ },
+ "id": 6,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "refId": "A",
+ "scenario": "random_walk",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "200,445,100,150,200,220,190",
+ "target": ""
+ }
+ ],
+ "thresholds": [
+ {
+ "colorMode": "critical",
+ "fill": true,
+ "line": true,
+ "op": "gt",
+ "value": 177
+ }
+ ],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Always Alerting with For",
+ "tooltip": {
+ "msResolution": false,
+ "shared": true,
+ "sort": 0,
+ "value_type": "cumulative"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "alert": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 100
+ ],
+ "type": "gt"
+ },
+ "operator": {
+ "type": "and"
+ },
+ "query": {
+ "params": [
+ "A",
+ "5m",
+ "now"
+ ]
+ },
+ "reducer": {
+ "params": [],
+ "type": "avg"
+ },
+ "type": "query"
+ }
+ ],
+ "executionErrorState": "alerting",
+ "for": "900000h",
+ "frequency": "1m",
+ "handler": 1,
+ "name": "Always Pending",
+ "noDataState": "no_data",
+ "notifications": []
+ },
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "gdev-testdata",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "gridPos": {
+ "h": 7,
+ "w": 12,
+ "x": 12,
+ "y": 14
+ },
+ "id": 7,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "refId": "A",
+ "scenario": "random_walk",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "200,445,100,150,200,220,190",
+ "target": ""
+ }
+ ],
+ "thresholds": [
+ {
+ "colorMode": "critical",
+ "fill": true,
+ "line": true,
+ "op": "gt",
+ "value": 100
+ }
+ ],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Always Alerting with For",
+ "tooltip": {
+ "msResolution": false,
+ "shared": true,
+ "sort": 0,
+ "value_type": "cumulative"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ }
+ ],
"revision": 2,
- "title": "Alerting with TestData",
+ "schemaVersion": 16,
+ "style": "dark",
"tags": [
"grafana-test"
],
- "style": "dark",
- "timezone": "browser",
- "editable": true,
- "hideControls": false,
- "sharedCrosshair": false,
- "rows": [
- {
- "collapse": false,
- "editable": true,
- "height": 255.625,
- "panels": [
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 60
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "TestData - Always OK",
- "noDataState": "no_data",
- "notifications": []
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "gdev-testdata",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 3,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "scenario": "random_walk",
- "scenarioId": "csv_metric_values",
- "stringInput": "1,20,90,30,5,0",
- "target": ""
- }
- ],
- "thresholds": [
- {
- "value": 60,
- "op": "gt",
- "fill": true,
- "line": true,
- "colorMode": "critical"
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Always OK",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "mode": "time",
- "name": null,
- "show": true,
- "values": []
- },
- "yaxes": [
- {
- "format": "short",
- "label": "",
- "logBase": 1,
- "max": "125",
- "min": "0",
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "alert": {
- "conditions": [
- {
- "evaluator": {
- "params": [
- 177
- ],
- "type": "gt"
- },
- "query": {
- "params": [
- "A",
- "5m",
- "now"
- ]
- },
- "reducer": {
- "params": [],
- "type": "avg"
- },
- "type": "query"
- }
- ],
- "enabled": true,
- "frequency": "60s",
- "handler": 1,
- "name": "TestData - Always Alerting",
- "noDataState": "no_data",
- "notifications": []
- },
- "aliasColors": {},
- "bars": false,
- "datasource": "gdev-testdata",
- "editable": true,
- "error": false,
- "fill": 1,
- "id": 4,
- "isNew": true,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 2,
- "links": [],
- "nullPointMode": "connected",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "refId": "A",
- "scenario": "random_walk",
- "scenarioId": "csv_metric_values",
- "stringInput": "200,445,100,150,200,220,190",
- "target": ""
- }
- ],
- "thresholds": [
- {
- "colorMode": "critical",
- "fill": true,
- "line": true,
- "op": "gt",
- "value": 177
- }
- ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Always Alerting",
- "tooltip": {
- "msResolution": false,
- "shared": true,
- "sort": 0,
- "value_type": "cumulative"
- },
- "type": "graph",
- "xaxis": {
- "mode": "time",
- "name": null,
- "show": true,
- "values": []
- },
- "yaxes": [
- {
- "format": "short",
- "label": "",
- "logBase": 1,
- "max": null,
- "min": "0",
- "show": true
- },
- {
- "format": "short",
- "label": "",
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "title": "New row"
- }
- ],
+ "templating": {
+ "list": []
+ },
"time": {
"from": "now-6h",
"to": "now"
@@ -274,14 +705,8 @@
"30d"
]
},
- "templating": {
- "list": []
- },
- "annotations": {
- "list": []
- },
- "schemaVersion": 13,
- "version": 4,
- "links": [],
- "gnetId": null
-}
+ "timezone": "browser",
+ "title": "Alerting with TestData",
+ "uid": "7MeksYbmk",
+ "version": 7
+}
\ No newline at end of file
diff --git a/devenv/docker/blocks/redis/docker-compose.yaml b/devenv/docker/blocks/redis/docker-compose.yaml
new file mode 100644
index 00000000000..65071d4966b
--- /dev/null
+++ b/devenv/docker/blocks/redis/docker-compose.yaml
@@ -0,0 +1,5 @@
+ memcached:
+ image: redis:latest
+ ports:
+ - "6379:6379"
+
diff --git a/devenv/docker/ha_test/docker-compose.yaml b/devenv/docker/ha_test/docker-compose.yaml
index ce8630d88a4..1195e2a977c 100644
--- a/devenv/docker/ha_test/docker-compose.yaml
+++ b/devenv/docker/ha_test/docker-compose.yaml
@@ -9,7 +9,7 @@ services:
- /var/run/docker.sock:/tmp/docker.sock:ro
db:
- image: mysql
+ image: mysql:5.6
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: grafana
diff --git a/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet b/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet
index 86ded7e79d6..e9b8abfbb9c 100644
--- a/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet
+++ b/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet
@@ -39,6 +39,7 @@ local alertDashboardTemplate = {
"executionErrorState": "alerting",
"frequency": "10s",
"handler": 1,
+ "for": "1m",
"name": "bulk alerting",
"noDataState": "no_data",
"notifications": [
diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md
index b232ee78f27..54d046749a9 100644
--- a/docs/sources/alerting/notifications.md
+++ b/docs/sources/alerting/notifications.md
@@ -55,6 +55,10 @@ Alert rule evaluation interval | Send reminders every | Reminder sent every (aft
+### Disable resolve message
+
+When checked, this option will disable resolve message [OK] that is sent when alerting state returns to false.
+
## Supported Notification Types
Grafana ships with the following set of notification types:
diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md
index 488619055e2..036ff97056d 100644
--- a/docs/sources/alerting/rules.md
+++ b/docs/sources/alerting/rules.md
@@ -39,7 +39,7 @@ Currently alerting supports a limited form of high availability. Since v4.2.0 of
## Rule Config
-{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}}
+
Currently only the graph panel supports alert rules but this will be added to the **Singlestat** and **Table**
panels as well in a future release.
@@ -48,6 +48,19 @@ panels as well in a future release.
Here you can specify the name of the alert rule and how often the scheduler should evaluate the alert rule.
+### For
+
+> This setting is available in Grafana 5.4 and above.
+
+If an alert rule has a configured `For` and the query violates the configured threshold it will first go from `OK` to `Pending`. Going from `OK` to `Pending` Grafana will not send any notifications. Once the alert rule has been firing for more than `For` duration, it will change to `Alerting` and send alert notifications.
+
+Typically, it's always a good idea to use this setting since it's often worse to get false positive than wait a few minutes before the alert notification triggers. Looking at the `Alert list` or `Alert list panels` you will be able to see alerts in pending state.
+
+Below you can see an example timeline of an alert using the `For` setting. At ~16:04 the alert state changes to `Pending` and after 4 minutes it changes to `Alerting` which is when alert notifications are sent. Once the series falls back to normal the alert rule goes back to `OK`.
+{{< imgbox img="/img/docs/v54/alerting-for-dark-theme.png" caption="Alerting For" >}}
+
+{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}}
+
### Conditions
Currently the only condition type that exists is a `Query` condition that allows you to
@@ -57,11 +70,11 @@ specify a query letter, time range and an aggregation function.
### Query condition example
```sql
-avg() OF query(A, 5m, now) IS BELOW 14
+avg() OF query(A, 15m, now) IS BELOW 14
```
- `avg()` Controls how the values for **each** series should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function.
-- `query(A, 5m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `5m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data.
+- `query(A, 15m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `15m, now` means 15 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data.
- `IS BELOW 14` Defines the type of threshold and the threshold value. You can click on `IS BELOW` to change the type of threshold.
The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md
index 4a884a60d15..4313b563d9e 100644
--- a/docs/sources/auth/ldap.md
+++ b/docs/sources/auth/ldap.md
@@ -3,6 +3,7 @@ title = "LDAP Authentication"
description = "Grafana LDAP Authentication Guide "
keywords = ["grafana", "configuration", "documentation", "ldap", "active directory"]
type = "docs"
+aliases = ["/installation/ldap/"]
[menu.docs]
name = "LDAP"
identifier = "ldap"
diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md
index a372600ac46..0480ee88adc 100644
--- a/docs/sources/auth/overview.md
+++ b/docs/sources/auth/overview.md
@@ -73,7 +73,18 @@ You can hide the Grafana login form using the below configuration settings.
```bash
[auth]
-disable_login_form ⁼ true
+disable_login_form = true
+```
+
+### Automatic OAuth login
+
+Set to true to attempt login with OAuth automatically, skipping the login screen.
+This setting is ignored if multiple OAuth providers are configured.
+Defaults to `false`.
+
+```bash
+[auth]
+oauth_auto_login = true
```
### Hide sign-out menu
@@ -84,3 +95,12 @@ Set to the option detailed below to true to hide sign-out menu link. Useful if y
[auth]
disable_signout_menu = true
```
+
+### URL redirect after signing out
+
+URL to redirect the user to after signing out from Grafana. This can for example be used to enable signout from oauth provider.
+
+```bash
+[auth]
+signout_redirect_url =
+```
diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md
index d19dbe4ea50..2c14d897d8e 100644
--- a/docs/sources/features/datasources/stackdriver.md
+++ b/docs/sources/features/datasources/stackdriver.md
@@ -158,9 +158,9 @@ Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod`
It is also possible to resolve the name of the Monitored Resource Type.
-| Alias Pattern Format | Description | Example Result |
-| ------------------------ | ------------------------------------------------| ---------------- |
-| `{{resource.type}}` | returns the name of the monitored resource type | `gce_instance` |
+| Alias Pattern Format | Description | Example Result |
+| -------------------- | ----------------------------------------------- | -------------- |
+| `{{resource.type}}` | returns the name of the monitored resource type | `gce_instance` |
Example Alias By: `{{resource.type}} - {{metric.type}}`
@@ -177,7 +177,17 @@ types of template variables.
### Query Variable
-Writing variable queries is not supported yet.
+Variable of the type *Query* allows you to query Stackdriver for various types of data. The Stackdriver data source plugin provides the following `Query Types`.
+
+| Name | Description |
+| ------------------- | ------------------------------------------------------------------------------------------------- |
+| *Metric Types* | Returns a list of metric type names that are available for the specified service. |
+| *Labels Keys* | Returns a list of keys for `metric label` and `resource label` in the specified metric. |
+| *Labels Values* | Returns a list of values for the label in the specified metric. |
+| *Resource Types* | Returns a list of resource types for the the specified metric. |
+| *Aggregations* | Returns a list of aggregations (cross series reducers) for the the specified metric. |
+| *Aligners* | Returns a list of aligners (per series aligners) for the the specified metric. |
+| *Alignment periods* | Returns a list of all alignment periods that are available in Stackdriver query editor in Grafana |
### Using variables in queries
diff --git a/docs/sources/features/panels/graph.md b/docs/sources/features/panels/graph.md
index 5a010ceca40..44fa0e7c0db 100644
--- a/docs/sources/features/panels/graph.md
+++ b/docs/sources/features/panels/graph.md
@@ -186,6 +186,14 @@ There is an option under Series overrides to draw lines as dashes. Set Dashes to
Thresholds allow you to add arbitrary lines or sections to the graph to make it easier to see when
the graph crosses a particular threshold.
+### Time Regions
+
+> Only available in Grafana v5.4 and above.
+
+{{< docs-imagebox img="/img/docs/v54/graph_time_regions.png" max-width= "800px" >}}
+
+Time regions allow you to highlight certain time regions of the graph to make it easier to see for example weekends, business hours and/or off work hours.
+
## Time Range
{{< docs-imagebox img="/img/docs/v51/graph-time-range.png" max-width= "900px" >}}
diff --git a/docs/sources/guides/whats-new-in-v5-4.md b/docs/sources/guides/whats-new-in-v5-4.md
new file mode 100644
index 00000000000..18b7b61d621
--- /dev/null
+++ b/docs/sources/guides/whats-new-in-v5-4.md
@@ -0,0 +1,83 @@
++++
+title = "What's New in Grafana v5.4"
+description = "Feature & improvement highlights for Grafana v5.4"
+keywords = ["grafana", "new", "documentation", "5.4"]
+type = "docs"
+[menu.docs]
+name = "Version 5.4"
+identifier = "v5.4"
+parent = "whatsnew"
+weight = -10
++++
+
+# What's New in Grafana v5.4
+
+Grafana v5.4 brings new features, many enhancements and bug fixes. This article will detail the major new features and enhancements.
+
+- [Alerting]({{< relref "#alerting" >}}) Limit false positives with the new `For` setting
+- [Google Stackdriver]({{< relref "#google-stackdriver" >}}) Now with support for templating queries
+- [MySQL]({{< relref "#mysql-query-builder" >}}) gets a new query builder!
+- [Graph Panel]({{< relref "#graph-panel-enhancements" >}}) Highlight time regions and more
+- [Team Preferences]({{< relref "#team-preferences" >}}) Give your teams their own home dashboard
+
+## Alerting
+
+{{< docs-imagebox img="/img/docs/v54/alerting-for-dark-theme.png" max-width="600px" class="docs-image--right" >}}
+
+Grafana v5.4 ships with a new alert rule setting named `For` which is great for removing false positives. If an alert rule has a configured `For` and the query violates the configured threshold it will first go from `OK` to `Pending`. Going from `OK` to `Pending` Grafana will not send any notifications. Once the alert rule has been firing for more than `For` duration, it will change to `Alerting` and send alert notifications. Typically, it's always a good idea to use this setting since it's often worse to get false positive than wait a few minutes before the alert notification triggers.
+
+In the screenshot you can see an example timeline of an alert using the `For` setting. At ~16:04 the alert state changes to `Pending` and after 4 minutes it changes to `Alerting` which is when alert notifications are sent. Once the series falls back to normal the alert rule goes back to `OK`. [Learn more](/alerting/rules/#for).
+
+Additionally, there's now support for disable the sending of `OK` alert notifications. [Learn more](/alerting/notifications/#disable-resolve-message).
+
+
+
+## Google Stackdriver
+
+{{< docs-imagebox img="/img/docs/v54/stackdriver_template_query.png" max-width="600px" class="docs-image--right" >}}
+
+Grafana v5.3 included built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) which enables you to visualize your Stackdriver metrics in Grafana.
+One important feature missing was support for templating queries. This is now included together with a brand new templating query editor for Stackdriver.
+
+The Stackdriver templating query editor lets you choose from a set of different Query Types. This will in turn reveal additional drop downs to help you
+find, filter and select the templating values you're interested in, see screenshot for details. The templating query editor also supports chaining multiple variables
+making it easy to define variables that's dependent on other variables.
+
+Stackdriver is the first datasource which has support for a custom templating query editor. But starting from Grafana v5.4 it's now possible for all datasources, including plugin datasources, to
+create their very own templating query editor.
+
+Additionally, if Grafana is running on a Google Compute Engine (GCE) virtual machine, it is now possible for Grafana to automatically retrieve default credentials from the metadata server.
+This has the advantage of not needing to generate a private key file for the service account and also not having to upload the file to Grafana. [Learn more](/features/datasources/stackdriver/#using-gce-default-service-account).
+
+Please read [Using Google Stackdriver in Grafana](/features/datasources/stackdriver/) for more detailed information on how to get started and use it.
+
+
+
+## MySQL Query Builder
+
+Grafana v5.4 comes with a new graphical query builder for MySQL. This brings MySQL integration more in line with some of the other datasources and makes it easier for both advanced users and beginners to work with timeseries in MySQL. Learn more about it in the [documentation](/features/datasources/mysql/#query-editor).
+
+{{< docs-imagebox img="/img/docs/v54/mysql_query_still.png" animated-gif="/img/docs/v54/mysql_query.gif" >}}
+
+## Graph Panel Enhancements
+
+Grafana v5.4 adds support for highlighting weekdays and/or certain timespans in the graph panel. This should make it easier to compare for example weekends, business hours and/or off work hours.
+
+{{< docs-imagebox img="/img/docs/v54/graph_time_regions.png" max-width= "800px" >}}
+
+Additionally, when rendering series as lines in the graph panel, should there be only one data point available for one series so that a connecting line cannot be established, a point will
+automatically be rendered for that data point. This should make it easier to understand what's going on when only receiving a single data point.
+
+{{< docs-imagebox img="/img/docs/v54/graph_dot_single_point.png" max-width= "800px" >}}
+
+## Team Preferences
+
+Grafana v5.4 adds support for customizing home dashboard, timezone and theme for teams, in addition to the existing customization on Organization and user Profile level.
+
+1. Specifying a preference on User Profile level will override preference on Team and/or Organization level
+2. Specifying a preference on Team level will override preference on Organization level.
+
+## 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.
diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md
index 2d70a6d2017..0684f65f166 100644
--- a/docs/sources/http_api/alerting.md
+++ b/docs/sources/http_api/alerting.md
@@ -160,11 +160,14 @@ Content-Type: application/json
`POST /api/admin/pause-all-alerts`
+Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+
+**Example Request**:
+
```http
POST /api/admin/pause-all-alerts HTTP/1.1
Accept: application/json
Content-Type: application/json
-Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"paused": true
diff --git a/docs/sources/http_api/team.md b/docs/sources/http_api/team.md
index 94ea4108481..5024ec69522 100644
--- a/docs/sources/http_api/team.md
+++ b/docs/sources/http_api/team.md
@@ -30,7 +30,7 @@ Authorization: Basic YWRtaW46YWRtaW4=
### Using the query parameter
-Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`.
+Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`.
The `totalCount` field in the response can be used for pagination of the teams list E.g. if `totalCount` is equal to 100 teams and the `perpage` parameter is set to 10 then there are 10 pages of teams.
@@ -314,3 +314,67 @@ Status Codes:
- **401** - Unauthorized
- **403** - Permission denied
- **404** - Team not found/Team member not found
+
+## Get Team Preferences
+
+`GET /api/teams/:teamId/preferences`
+
+**Example Request**:
+
+```http
+GET /api/teams/2/preferences HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+ "theme": "",
+ "homeDashboardId": 0,
+ "timezone": ""
+}
+```
+
+## Update Team Preferences
+
+`PUT /api/teams/:teamId/preferences`
+
+**Example Request**:
+
+```http
+PUT /api/teams/2/preferences HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+{
+ "theme": "dark",
+ "homeDashboardId": 39,
+ "timezone": "utc"
+}
+```
+
+JSON Body Schema:
+
+- **theme** - One of: ``light``, ``dark``, or an empty string for the default theme
+- **homeDashboardId** - The numerical ``:id`` of a dashboard, default: ``0``
+- **timezone** - One of: ``utc``, ``browser``, or an empty string for the default
+
+Omitting a key will cause the current value to be replaced with the system default value.
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: text/plain; charset=utf-8
+
+{
+ "message":"Preferences updated"
+}
+```
diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md
index b9047187b2d..047d4473603 100644
--- a/docs/sources/http_api/user.md
+++ b/docs/sources/http_api/user.md
@@ -226,6 +226,40 @@ Content-Type: application/json
]
```
+## Get Teams for user
+
+`GET /api/users/:id/teams`
+
+**Example Request**:
+
+```http
+GET /api/users/1/teams HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Basic YWRtaW46YWRtaW4=
+```
+
+Requires basic authentication and that the authenticated user is a Grafana Admin.
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+ {
+ "id":1,
+ "orgId":1,
+ "name":"team1",
+ "email":"",
+ "avatarUrl":"/avatar/3fcfe295eae3bcb67a49349377428a66",
+ "memberCount":1
+ }
+]
+```
+
+
## User
## Actual User
diff --git a/docs/sources/index.md b/docs/sources/index.md
index e9a900d93f1..3e8e014e606 100644
--- a/docs/sources/index.md
+++ b/docs/sources/index.md
@@ -60,9 +60,9 @@ aliases = ["v1.1", "guides/reference/admin"]
Provisioning
A guide to help you automate your Grafana setup & configuration.
- }}" class="nav-cards__item nav-cards__item--guide">
- What's new in v5.3
- Article on all the new cool features and enhancements in v5.3
+ }}" class="nav-cards__item nav-cards__item--guide">
+ What's new in v5.4
+ Article on all the new cool features and enhancements in v5.4
}}" class="nav-cards__item nav-cards__item--guide">
Screencasts
diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index 8d156e739bf..30ef020a3de 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -454,6 +454,12 @@ Ex `filters = sqlstore:debug`
### enabled
Enable metrics reporting. defaults true. Available via HTTP API `/metrics`.
+### basic_auth_username
+If set configures the username to use for basic authentication on the metrics endpoint.
+
+### basic_auth_password
+If set configures the password to use for basic authentication on the metrics endpoint.
+
### interval_seconds
Flush/Write interval when sending metrics to external TSDB. Defaults to 10s.
diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index 403dabba8ae..f20cc0ccfc9 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -25,7 +25,7 @@ the value, using the dropdown at the top of the dashboard, your panel's metric q
Panel titles and metric queries can refer to variables using two different syntaxes:
-- `$` Example: apps.frontend.$server.requests.count
+- `$varname` Example: apps.frontend.$server.requests.count
- `[[varname]]` Example: apps.frontend.[[server]].requests.count
Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of word. Use
diff --git a/latest.json b/latest.json
index 992af0a8336..5e72c11c1f2 100644
--- a/latest.json
+++ b/latest.json
@@ -1,4 +1,4 @@
{
- "stable": "5.3.2",
- "testing": "5.3.2"
+ "stable": "5.3.4",
+ "testing": "5.3.4"
}
diff --git a/packaging/deb/init.d/grafana-server b/packaging/deb/init.d/grafana-server
index 567da94f881..5c1d9c8271a 100755
--- a/packaging/deb/init.d/grafana-server
+++ b/packaging/deb/init.d/grafana-server
@@ -56,7 +56,7 @@ if [ -f "$DEFAULT" ]; then
. "$DEFAULT"
fi
-DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
+DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=deb cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
function checkUser() {
if [ `id -u` -ne 0 ]; then
diff --git a/packaging/deb/systemd/grafana-server.service b/packaging/deb/systemd/grafana-server.service
index acd2a360a93..b1e2e387e4d 100644
--- a/packaging/deb/systemd/grafana-server.service
+++ b/packaging/deb/systemd/grafana-server.service
@@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750
ExecStart=/usr/sbin/grafana-server \
--config=${CONF_FILE} \
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
+ --packaging=deb \
cfg:default.paths.logs=${LOG_DIR} \
cfg:default.paths.data=${DATA_DIR} \
cfg:default.paths.plugins=${PLUGINS_DIR} \
diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile
index dc8972b0ba0..4d4f6539972 100644
--- a/packaging/docker/Dockerfile
+++ b/packaging/docker/Dockerfile
@@ -25,7 +25,8 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
WORKDIR $GF_PATHS_HOME
-RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates curl && \
+RUN apt-get update && apt-get -y upgrade && \
+ apt-get install -qq -y libfontconfig ca-certificates curl && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
diff --git a/packaging/docker/build-enterprise.sh b/packaging/docker/build-enterprise.sh
index 2f59e436d95..e10e0d691e8 100755
--- a/packaging/docker/build-enterprise.sh
+++ b/packaging/docker/build-enterprise.sh
@@ -1,9 +1,17 @@
#!/bin/sh
set -e
-_grafana_tag=$1
+_raw_grafana_tag=$1
_docker_repo=${2:-grafana/grafana-enterprise}
+if echo "$_raw_grafana_tag" | grep -q "^v"; then
+ _grafana_tag=$(echo "${_raw_grafana_tag}" | cut -d "v" -f 2)
+else
+ _grafana_tag="${_raw_grafana_tag}"
+fi
+
+echo "Building and deploying ${_docker_repo}:${_grafana_tag}"
+
docker build \
--tag "${_docker_repo}:${_grafana_tag}"\
--no-cache=true \
diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh
index bc001bdf90a..63f20742c96 100755
--- a/packaging/docker/run.sh
+++ b/packaging/docker/run.sh
@@ -80,6 +80,7 @@ fi
exec grafana-server \
--homepath="$GF_PATHS_HOME" \
--config="$GF_PATHS_CONFIG" \
+ --packaging=docker \
"$@" \
cfg:default.log.mode="console" \
cfg:default.paths.data="$GF_PATHS_DATA" \
diff --git a/packaging/rpm/init.d/grafana-server b/packaging/rpm/init.d/grafana-server
index cefe212116c..b7b41e58e8d 100755
--- a/packaging/rpm/init.d/grafana-server
+++ b/packaging/rpm/init.d/grafana-server
@@ -60,7 +60,7 @@ fi
# overwrite settings from default file
[ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME
-DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
+DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=rpm cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
function isRunning() {
status -p $PID_FILE $NAME > /dev/null 2>&1
diff --git a/packaging/rpm/systemd/grafana-server.service b/packaging/rpm/systemd/grafana-server.service
index f228c8d8b14..ad5006d1d4c 100644
--- a/packaging/rpm/systemd/grafana-server.service
+++ b/packaging/rpm/systemd/grafana-server.service
@@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750
ExecStart=/usr/sbin/grafana-server \
--config=${CONF_FILE} \
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
+ --packaging=rpm \
cfg:default.paths.logs=${LOG_DIR} \
cfg:default.paths.data=${DATA_DIR} \
cfg:default.paths.plugins=${PLUGINS_DIR} \
diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go
index c68cee50948..66b3b504946 100644
--- a/pkg/api/alerting.go
+++ b/pkg/api/alerting.go
@@ -295,7 +295,7 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
return Error(500, "", err)
}
- var response m.AlertStateType = m.AlertStatePending
+ var response m.AlertStateType = m.AlertStateUnknown
pausedState := "un-paused"
if cmd.Paused {
response = m.AlertStatePaused
diff --git a/pkg/api/api.go b/pkg/api/api.go
index f1fe940e416..0526ee80afe 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -140,6 +140,7 @@ func (hs *HTTPServer) registerRoutes() {
usersRoute.Get("/", Wrap(SearchUsers))
usersRoute.Get("/search", Wrap(SearchUsersWithPaging))
usersRoute.Get("/:id", Wrap(GetUserByID))
+ usersRoute.Get("/:id/teams", Wrap(GetUserTeams))
usersRoute.Get("/:id/orgs", Wrap(GetUserOrgList))
// query parameters /users/lookup?loginOrEmail=admin@example.com
usersRoute.Get("/lookup", Wrap(GetUserByLoginOrEmail))
@@ -155,6 +156,8 @@ func (hs *HTTPServer) registerRoutes() {
teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers))
teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember))
teamsRoute.Delete("/:teamId/members/:userId", Wrap(RemoveTeamMember))
+ teamsRoute.Get("/:teamId/preferences", Wrap(GetTeamPreferences))
+ teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateTeamPreferences))
}, reqOrgAdmin)
// team without requirement of user to be org admin
@@ -242,7 +245,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIdByName), reqSignedIn)
- apiRoute.Get("/plugins", Wrap(GetPluginList))
+ apiRoute.Get("/plugins", Wrap(hs.GetPluginList))
apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))
apiRoute.Get("/plugins/:pluginId/markdown/:name", Wrap(GetPluginMarkdown))
diff --git a/pkg/api/basic_auth.go b/pkg/api/basic_auth.go
new file mode 100644
index 00000000000..376cfb24c91
--- /dev/null
+++ b/pkg/api/basic_auth.go
@@ -0,0 +1,19 @@
+package api
+
+import (
+ "crypto/subtle"
+ macaron "gopkg.in/macaron.v1"
+)
+
+// BasicAuthenticatedRequest parses the provided HTTP request for basic authentication credentials
+// and returns true if the provided credentials match the expected username and password.
+// Returns false if the request is unauthenticated.
+// Uses constant-time comparison in order to mitigate timing attacks.
+func BasicAuthenticatedRequest(req macaron.Request, expectedUser, expectedPass string) bool {
+ user, pass, ok := req.BasicAuth()
+ if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(expectedUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass)) != 1 {
+ return false
+ }
+
+ return true
+}
diff --git a/pkg/api/basic_auth_test.go b/pkg/api/basic_auth_test.go
new file mode 100644
index 00000000000..0b5051c3e2a
--- /dev/null
+++ b/pkg/api/basic_auth_test.go
@@ -0,0 +1,45 @@
+package api
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+ "gopkg.in/macaron.v1"
+)
+
+func TestBasicAuthenticatedRequest(t *testing.T) {
+ expectedUser := "prometheus"
+ expectedPass := "password"
+
+ Convey("Given a valid set of basic auth credentials", t, func() {
+ httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
+ So(err, ShouldBeNil)
+ req := macaron.Request{
+ Request: httpReq,
+ }
+ encodedCreds := encodeBasicAuthCredentials(expectedUser, expectedPass)
+ req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds))
+ authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass)
+ So(authenticated, ShouldBeTrue)
+ })
+
+ Convey("Given an invalid set of basic auth credentials", t, func() {
+ httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
+ So(err, ShouldBeNil)
+ req := macaron.Request{
+ Request: httpReq,
+ }
+ encodedCreds := encodeBasicAuthCredentials("invaliduser", "invalidpass")
+ req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds))
+ authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass)
+ So(authenticated, ShouldBeFalse)
+ })
+}
+
+func encodeBasicAuthCredentials(user, pass string) string {
+ creds := fmt.Sprintf("%s:%s", user, pass)
+ return base64.StdEncoding.EncodeToString([]byte(creds))
+}
diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go
index 02248334b9c..2789b0bf51e 100644
--- a/pkg/api/dashboard.go
+++ b/pkg/api/dashboard.go
@@ -277,10 +277,6 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
return Error(500, "Failed to save dashboard", err)
}
- if err == m.ErrDashboardFailedToUpdateAlertData {
- return Error(500, "Invalid alert data. Cannot save dashboard", err)
- }
-
c.TimeRequest(metrics.M_Api_Dashboard_Save)
return JSON(200, util.DynMap{
"status": "success",
@@ -293,7 +289,7 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
}
func GetHomeDashboard(c *m.ReqContext) Response {
- prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
+ prefsQuery := m.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}
if err := bus.Dispatch(&prefsQuery); err != nil {
return Error(500, "Failed to get preferences", err)
}
diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go
index 2726623c242..8ee40920cbc 100644
--- a/pkg/api/dashboard_test.go
+++ b/pkg/api/dashboard_test.go
@@ -727,7 +727,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
{SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400},
{SaveError: alerting.ValidationError{Reason: "Mu"}, ExpectedStatusCode: 422},
- {SaveError: m.ErrDashboardFailedToUpdateAlertData, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderWithSameNameAsDashboard, ExpectedStatusCode: 400},
diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go
index 78a611c5eeb..edc9d96d1ac 100644
--- a/pkg/api/dtos/plugins.go
+++ b/pkg/api/dtos/plugins.go
@@ -19,9 +19,9 @@ type PluginSetting struct {
JsonData map[string]interface{} `json:"jsonData"`
DefaultNavUrl string `json:"defaultNavUrl"`
- LatestVersion string `json:"latestVersion"`
- HasUpdate bool `json:"hasUpdate"`
- State string `json:"state"`
+ LatestVersion string `json:"latestVersion"`
+ HasUpdate bool `json:"hasUpdate"`
+ State plugins.PluginState `json:"state"`
}
type PluginListItem struct {
@@ -34,7 +34,7 @@ type PluginListItem struct {
LatestVersion string `json:"latestVersion"`
HasUpdate bool `json:"hasUpdate"`
DefaultNavUrl string `json:"defaultNavUrl"`
- State string `json:"state"`
+ State plugins.PluginState `json:"state"`
}
type PluginList []PluginListItem
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index 1de65c7b2ce..65affe83e98 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -133,7 +133,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
panels := map[string]interface{}{}
for _, panel := range enabledPlugins.Panels {
- if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels {
+ if panel.State == plugins.PluginStateAlpha && !hs.Cfg.EnableAlphaPanels {
continue
}
diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go
index ce28e4716ee..d4d7b41bec5 100644
--- a/pkg/api/http_server.go
+++ b/pkg/api/http_server.go
@@ -245,6 +245,11 @@ func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
return
}
+ if hs.metricsEndpointBasicAuthEnabled() && !BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) {
+ ctx.Resp.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).
ServeHTTP(ctx.Resp, ctx.Req.Request)
}
@@ -299,3 +304,7 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string,
},
))
}
+
+func (hs *HTTPServer) metricsEndpointBasicAuthEnabled() bool {
+ return hs.Cfg.MetricsEndpointBasicAuthUsername != "" && hs.Cfg.MetricsEndpointBasicAuthPassword != ""
+}
diff --git a/pkg/api/http_server_test.go b/pkg/api/http_server_test.go
new file mode 100644
index 00000000000..0f99ae82db5
--- /dev/null
+++ b/pkg/api/http_server_test.go
@@ -0,0 +1,30 @@
+package api
+
+import (
+ "testing"
+
+ "github.com/grafana/grafana/pkg/setting"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func TestHTTPServer(t *testing.T) {
+ Convey("Given a HTTPServer", t, func() {
+ ts := &HTTPServer{
+ Cfg: setting.NewCfg(),
+ }
+
+ Convey("Given that basic auth on the metrics endpoint is enabled", func() {
+ ts.Cfg.MetricsEndpointBasicAuthUsername = "foo"
+ ts.Cfg.MetricsEndpointBasicAuthPassword = "bar"
+
+ So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeTrue)
+ })
+
+ Convey("Given that basic auth on the metrics endpoint is disabled", func() {
+ ts.Cfg.MetricsEndpointBasicAuthUsername = ""
+ ts.Cfg.MetricsEndpointBasicAuthPassword = ""
+
+ So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeFalse)
+ })
+ })
+}
diff --git a/pkg/api/index.go b/pkg/api/index.go
index fe7c9e79a17..253fa9c17af 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -23,7 +23,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
return nil, err
}
- prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
+ prefsQuery := m.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}
if err := bus.Dispatch(&prefsQuery); err != nil {
return nil, err
}
diff --git a/pkg/api/login.go b/pkg/api/login.go
index 1083f89adfd..05afc40e59a 100644
--- a/pkg/api/login.go
+++ b/pkg/api/login.go
@@ -39,6 +39,10 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
viewData.Settings["loginError"] = loginError
}
+ if tryOAuthAutoLogin(c) {
+ return
+ }
+
if !tryLoginUsingRememberCookie(c) {
c.HTML(200, ViewIndex, viewData)
return
@@ -53,6 +57,24 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
c.Redirect(setting.AppSubUrl + "/")
}
+func tryOAuthAutoLogin(c *m.ReqContext) bool {
+ if !setting.OAuthAutoLogin {
+ return false
+ }
+ oauthInfos := setting.OAuthService.OAuthInfos
+ if len(oauthInfos) != 1 {
+ log.Warn("Skipping OAuth auto login because multiple OAuth providers are configured.")
+ return false
+ }
+ for key := range setting.OAuthService.OAuthInfos {
+ redirectUrl := setting.AppSubUrl + "/login/" + key
+ log.Info("OAuth auto login enabled. Redirecting to " + redirectUrl)
+ c.Redirect(redirectUrl, 307)
+ return true
+ }
+ return false
+}
+
func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
// Check auto-login.
uname := c.GetCookie(setting.CookieUserName)
diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go
index 0c000058e4b..38a2fd187e3 100644
--- a/pkg/api/pluginproxy/ds_proxy.go
+++ b/pkg/api/pluginproxy/ds_proxy.go
@@ -195,6 +195,10 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
req.Header.Del("X-Forwarded-Proto")
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
+ // Clear Origin and Referer to avoir CORS issues
+ req.Header.Del("Origin")
+ req.Header.Del("Referer")
+
// set X-Forwarded-For header
if req.RemoteAddr != "" {
remoteAddr, _, err := net.SplitHostPort(req.RemoteAddr)
diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go
index ab0effb298f..c9be169565f 100644
--- a/pkg/api/pluginproxy/ds_proxy_test.go
+++ b/pkg/api/pluginproxy/ds_proxy_test.go
@@ -371,13 +371,22 @@ func TestDSRouteRule(t *testing.T) {
ctx := &m.ReqContext{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/")
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
+ req.Header.Add("Origin", "grafana.com")
+ req.Header.Add("Referer", "grafana.com")
+ req.Header.Add("X-Canary", "stillthere")
So(err, ShouldBeNil)
proxy.getDirector()(req)
- Convey("Shoudl keep user request (including trailing slash)", func() {
+ Convey("Should keep user request (including trailing slash)", func() {
So(req.URL.String(), ShouldEqual, "http://host/root/path/to/folder/")
})
+
+ Convey("Origin and Referer headers should be dropped", func() {
+ So(req.Header.Get("Origin"), ShouldEqual, "")
+ So(req.Header.Get("Referer"), ShouldEqual, "")
+ So(req.Header.Get("X-Canary"), ShouldEqual, "stillthere")
+ })
})
})
}
diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go
index 4b44009ab8c..455420e4688 100644
--- a/pkg/api/plugins.go
+++ b/pkg/api/plugins.go
@@ -10,7 +10,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
-func GetPluginList(c *m.ReqContext) Response {
+func (hs *HTTPServer) GetPluginList(c *m.ReqContext) Response {
typeFilter := c.Query("type")
enabledFilter := c.Query("enabled")
embeddedFilter := c.Query("embedded")
@@ -39,6 +39,10 @@ func GetPluginList(c *m.ReqContext) Response {
continue
}
+ if pluginDef.State == plugins.PluginStateAlpha && !hs.Cfg.EnableAlphaPanels {
+ continue
+ }
+
listItem := dtos.PluginListItem{
Id: pluginDef.Id,
Name: pluginDef.Name,
diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go
index 26695130975..9b451aa2a6e 100644
--- a/pkg/api/preferences.go
+++ b/pkg/api/preferences.go
@@ -21,11 +21,11 @@ func SetHomeDashboard(c *m.ReqContext, cmd m.SavePreferencesCommand) Response {
// GET /api/user/preferences
func GetUserPreferences(c *m.ReqContext) Response {
- return getPreferencesFor(c.OrgId, c.UserId)
+ return getPreferencesFor(c.OrgId, c.UserId, 0)
}
-func getPreferencesFor(orgID int64, userID int64) Response {
- prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID}
+func getPreferencesFor(orgID, userID, teamID int64) Response {
+ prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID, TeamId: teamID}
if err := bus.Dispatch(&prefsQuery); err != nil {
return Error(500, "Failed to get preferences", err)
@@ -42,13 +42,14 @@ func getPreferencesFor(orgID int64, userID int64) Response {
// PUT /api/user/preferences
func UpdateUserPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
- return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd)
+ return updatePreferencesFor(c.OrgId, c.UserId, 0, &dtoCmd)
}
-func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
+func updatePreferencesFor(orgID, userID, teamId int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
saveCmd := m.SavePreferencesCommand{
UserId: userID,
OrgId: orgID,
+ TeamId: teamId,
Theme: dtoCmd.Theme,
Timezone: dtoCmd.Timezone,
HomeDashboardId: dtoCmd.HomeDashboardID,
@@ -63,10 +64,10 @@ func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd
// GET /api/org/preferences
func GetOrgPreferences(c *m.ReqContext) Response {
- return getPreferencesFor(c.OrgId, 0)
+ return getPreferencesFor(c.OrgId, 0, 0)
}
// PUT /api/org/preferences
func UpdateOrgPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
- return updatePreferencesFor(c.OrgId, 0, &dtoCmd)
+ return updatePreferencesFor(c.OrgId, 0, 0, &dtoCmd)
}
diff --git a/pkg/api/team.go b/pkg/api/team.go
index ebb426c4c82..32265e5d018 100644
--- a/pkg/api/team.go
+++ b/pkg/api/team.go
@@ -96,3 +96,13 @@ func GetTeamByID(c *m.ReqContext) Response {
query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name)
return JSON(200, &query.Result)
}
+
+// GET /api/teams/:teamId/preferences
+func GetTeamPreferences(c *m.ReqContext) Response {
+ return getPreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"))
+}
+
+// PUT /api/teams/:teamId/preferences
+func UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
+ return updatePreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"), &dtoCmd)
+}
diff --git a/pkg/api/user.go b/pkg/api/user.go
index 7116ad83f3f..6db9c6f6baf 100644
--- a/pkg/api/user.go
+++ b/pkg/api/user.go
@@ -113,7 +113,16 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response {
// GET /api/user/teams
func GetSignedInUserTeamList(c *m.ReqContext) Response {
- query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId}
+ return getUserTeamList(c.OrgId, c.UserId)
+}
+
+// GET /api/users/:id/teams
+func GetUserTeams(c *m.ReqContext) Response {
+ return getUserTeamList(c.OrgId, c.ParamsInt64(":id"))
+}
+
+func getUserTeamList(userID int64, orgID int64) Response {
+ query := m.GetTeamsByUserQuery{OrgId: orgID, UserId: userID}
if err := bus.Dispatch(&query); err != nil {
return Error(500, "Failed to get user teams", err)
@@ -122,11 +131,10 @@ func GetSignedInUserTeamList(c *m.ReqContext) Response {
for _, team := range query.Result {
team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
}
-
return JSON(200, query.Result)
}
-// GET /api/user/:id/orgs
+// GET /api/users/:id/orgs
func GetUserOrgList(c *m.ReqContext) Response {
return getUserOrgList(c.ParamsInt64(":id"))
}
diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go
index c7c1ff3aff7..3bdaf0cc80e 100644
--- a/pkg/cmd/grafana-server/main.go
+++ b/pkg/cmd/grafana-server/main.go
@@ -13,7 +13,7 @@ import (
"syscall"
"time"
- extensions "github.com/grafana/grafana/pkg/extensions"
+ "github.com/grafana/grafana/pkg/extensions"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
@@ -39,6 +39,7 @@ var buildstamp string
var configFile = flag.String("config", "", "path to config file")
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
var pidFile = flag.String("pidfile", "", "path to pid file")
+var packaging = flag.String("packaging", "unknown", "describes the way Grafana was installed")
func main() {
v := flag.Bool("v", false, "prints current version and exits")
@@ -53,7 +54,10 @@ func main() {
if *profile {
runtime.SetBlockProfileRate(1)
go func() {
- http.ListenAndServe(fmt.Sprintf("localhost:%d", *profilePort), nil)
+ err := http.ListenAndServe(fmt.Sprintf("localhost:%d", *profilePort), nil)
+ if err != nil {
+ panic(err)
+ }
}()
f, err := os.Create("trace.out")
@@ -79,6 +83,7 @@ func main() {
setting.BuildStamp = buildstampInt64
setting.BuildBranch = buildBranch
setting.IsEnterprise = extensions.IsEnterprise
+ setting.Packaging = validPackaging(*packaging)
metrics.SetBuildInformation(version, commit, buildBranch)
@@ -95,6 +100,16 @@ func main() {
os.Exit(code)
}
+func validPackaging(packaging string) string {
+ validTypes := []string{"dev", "deb", "rpm", "docker", "brew", "hosted", "unknown"}
+ for _, vt := range validTypes {
+ if packaging == vt {
+ return packaging
+ }
+ }
+ return "unknown"
+}
+
func listenToSystemSignals(server *GrafanaServerImpl) {
signalChan := make(chan os.Signal, 1)
sighupChan := make(chan os.Signal, 1)
diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go
index 2c67a06a843..d9fd1e62ce7 100644
--- a/pkg/cmd/grafana-server/server.go
+++ b/pkg/cmd/grafana-server/server.go
@@ -67,6 +67,7 @@ type GrafanaServerImpl struct {
}
func (g *GrafanaServerImpl) Run() error {
+ var err error
g.loadConfiguration()
g.writePIDFile()
@@ -74,20 +75,38 @@ func (g *GrafanaServerImpl) Run() error {
social.NewOAuthService()
serviceGraph := inject.Graph{}
- serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
- serviceGraph.Provide(&inject.Object{Value: g.cfg})
- serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
- serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
+ err = serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
+ if err != nil {
+ return fmt.Errorf("Failed to provide object to the graph: %v", err)
+ }
+ err = serviceGraph.Provide(&inject.Object{Value: g.cfg})
+ if err != nil {
+ return fmt.Errorf("Failed to provide object to the graph: %v", err)
+ }
+ err = serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
+ if err != nil {
+ return fmt.Errorf("Failed to provide object to the graph: %v", err)
+ }
+ err = serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
+ if err != nil {
+ return fmt.Errorf("Failed to provide object to the graph: %v", err)
+ }
// self registered services
services := registry.GetServices()
// Add all services to dependency graph
for _, service := range services {
- serviceGraph.Provide(&inject.Object{Value: service.Instance})
+ err = serviceGraph.Provide(&inject.Object{Value: service.Instance})
+ if err != nil {
+ return fmt.Errorf("Failed to provide object to the graph: %v", err)
+ }
}
- serviceGraph.Provide(&inject.Object{Value: g})
+ err = serviceGraph.Provide(&inject.Object{Value: g})
+ if err != nil {
+ return fmt.Errorf("Failed to provide object to the graph: %v", err)
+ }
// Inject dependencies to services
if err := serviceGraph.Populate(); err != nil {
@@ -144,6 +163,7 @@ func (g *GrafanaServerImpl) Run() error {
}
sendSystemdNotification("READY=1")
+
return g.childRoutines.Wait()
}
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 5709e3e3213..326514a9687 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -313,7 +313,7 @@ func init() {
// SetBuildInformation sets the build information for this binary
func SetBuildInformation(version, revision, branch string) {
- // We export this info twice for backwards compability.
+ // We export this info twice for backwards compatibility.
// Once this have been released for some time we should be able to remote `M_Grafana_Version`
// The reason we added a new one is that its common practice in the prometheus community
// to name this metric `*_build_info` so its easy to do aggregation on all programs.
@@ -397,11 +397,12 @@ func sendUsageStats(oauthProviders map[string]bool) {
metrics := map[string]interface{}{}
report := map[string]interface{}{
- "version": version,
- "metrics": metrics,
- "os": runtime.GOOS,
- "arch": runtime.GOARCH,
- "edition": getEdition(),
+ "version": version,
+ "metrics": metrics,
+ "os": runtime.GOOS,
+ "arch": runtime.GOARCH,
+ "edition": getEdition(),
+ "packaging": setting.Packaging,
}
statsQuery := models.GetSystemStatsQuery{}
@@ -447,6 +448,8 @@ func sendUsageStats(oauthProviders map[string]bool) {
}
metrics["stats.ds.other.count"] = dsOtherCount
+ metrics["stats.packaging."+setting.Packaging+".count"] = 1
+
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
if err := bus.Dispatch(&dsAccessStats); err != nil {
metricsLogger.Error("Failed to get datasource access stats", "error", err)
diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go
index 43739221f1e..c27d6f64b8c 100644
--- a/pkg/metrics/metrics_test.go
+++ b/pkg/metrics/metrics_test.go
@@ -176,6 +176,7 @@ func TestMetrics(t *testing.T) {
setting.BasicAuthEnabled = true
setting.LdapEnabled = true
setting.AuthProxyEnabled = true
+ setting.Packaging = "deb"
wg.Add(1)
sendUsageStats(oauthProviders)
@@ -243,6 +244,8 @@ func TestMetrics(t *testing.T) {
So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1)
So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1)
So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
+
+ So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1)
})
})
diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go
index eef07c8c24a..b8ca85fb45b 100644
--- a/pkg/middleware/recovery.go
+++ b/pkg/middleware/recovery.go
@@ -115,6 +115,7 @@ func Recovery() macaron.Handler {
c.Data["Title"] = "Server Error"
c.Data["AppSubUrl"] = setting.AppSubUrl
+ c.Data["Theme"] = setting.DefaultTheme
if setting.Env == setting.DEV {
if theErr, ok := err.(error); ok {
diff --git a/pkg/models/alert.go b/pkg/models/alert.go
index aaf9c50197a..e8171f8cf0b 100644
--- a/pkg/models/alert.go
+++ b/pkg/models/alert.go
@@ -19,6 +19,7 @@ const (
AlertStateAlerting AlertStateType = "alerting"
AlertStateOK AlertStateType = "ok"
AlertStatePending AlertStateType = "pending"
+ AlertStateUnknown AlertStateType = "unknown"
)
const (
@@ -39,7 +40,12 @@ var (
)
func (s AlertStateType) IsValid() bool {
- return s == AlertStateOK || s == AlertStateNoData || s == AlertStatePaused || s == AlertStatePending
+ return s == AlertStateOK ||
+ s == AlertStateNoData ||
+ s == AlertStatePaused ||
+ s == AlertStatePending ||
+ s == AlertStateAlerting ||
+ s == AlertStateUnknown
}
func (s NoDataOption) IsValid() bool {
@@ -66,12 +72,13 @@ type Alert struct {
PanelId int64
Name string
Message string
- Severity string
+ Severity string //Unused
State AlertStateType
- Handler int64
+ Handler int64 //Unused
Silenced bool
ExecutionError string
Frequency int64
+ For time.Duration
EvalData *simplejson.Json
NewStateDate time.Time
diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go
index e8aebb1d1f4..3a8010e797b 100644
--- a/pkg/models/dashboards.go
+++ b/pkg/models/dashboards.go
@@ -21,7 +21,6 @@ var (
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
- ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go
index 4c77bc96d4d..c73e0be4949 100644
--- a/pkg/models/preferences.go
+++ b/pkg/models/preferences.go
@@ -14,6 +14,7 @@ type Preferences struct {
Id int64
OrgId int64
UserId int64
+ TeamId int64
Version int
HomeDashboardId int64
Timezone string
@@ -29,14 +30,13 @@ type GetPreferencesQuery struct {
Id int64
OrgId int64
UserId int64
+ TeamId int64
Result *Preferences
}
type GetPreferencesWithDefaultsQuery struct {
- Id int64
- OrgId int64
- UserId int64
+ User *SignedInUser
Result *Preferences
}
@@ -46,6 +46,7 @@ type GetPreferencesWithDefaultsQuery struct {
type SavePreferencesCommand struct {
UserId int64
OrgId int64
+ TeamId int64
HomeDashboardId int64 `json:"homeDashboardId"`
Timezone string `json:"timezone"`
diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go
index 9677c21ef04..5ac436205c1 100644
--- a/pkg/plugins/models.go
+++ b/pkg/plugins/models.go
@@ -17,6 +17,13 @@ var (
PluginTypeDashboard = "dashboard"
)
+type PluginState string
+
+var (
+ PluginStateAlpha PluginState = "alpha"
+ PluginStateBeta PluginState = "beta"
+)
+
type PluginNotFoundError struct {
PluginId string
}
@@ -39,7 +46,7 @@ type PluginBase struct {
Module string `json:"module"`
BaseUrl string `json:"baseUrl"`
HideFromList bool `json:"hideFromList,omitempty"`
- State string `json:"state,omitempty"`
+ State PluginState `json:"state,omitempty"`
IncludedInAppId string `json:"-"`
PluginDir string `json:"-"`
diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go
index d0441d379b7..4db942e0a55 100644
--- a/pkg/services/alerting/eval_context.go
+++ b/pkg/services/alerting/eval_context.go
@@ -68,8 +68,13 @@ func (c *EvalContext) GetStateModel() *StateDescription {
Color: "#D63232",
Text: "Alerting",
}
+ case m.AlertStateUnknown:
+ return &StateDescription{
+ Color: "#888888",
+ Text: "Unknown",
+ }
default:
- panic("Unknown rule state " + c.Rule.State)
+ panic("Unknown rule state for alert " + c.Rule.State)
}
}
@@ -113,7 +118,26 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
}
+// GetNewState returns the new state from the alert rule evaluation
func (c *EvalContext) GetNewState() m.AlertStateType {
+ ns := getNewStateInternal(c)
+ if ns != m.AlertStateAlerting || c.Rule.For == 0 {
+ return ns
+ }
+
+ since := time.Since(c.Rule.LastStateChange)
+ if c.PrevAlertState == m.AlertStatePending && since > c.Rule.For {
+ return m.AlertStateAlerting
+ }
+
+ if c.PrevAlertState == m.AlertStateAlerting {
+ return m.AlertStateAlerting
+ }
+
+ return m.AlertStatePending
+}
+
+func getNewStateInternal(c *EvalContext) m.AlertStateType {
if c.Error != nil {
c.log.Error("Alert Rule Result Error",
"ruleId", c.Rule.Id,
@@ -125,11 +149,13 @@ func (c *EvalContext) GetNewState() m.AlertStateType {
return c.PrevAlertState
}
return c.Rule.ExecutionErrorState.ToAlertState()
+ }
- } else if c.Firing {
+ if c.Firing {
return m.AlertStateAlerting
+ }
- } else if c.NoDataFound {
+ if c.NoDataFound {
c.log.Info("Alert Rule returned no data",
"ruleId", c.Rule.Id,
"name", c.Rule.Name,
diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go
index 750fa959683..af7e66b2f07 100644
--- a/pkg/services/alerting/eval_context_test.go
+++ b/pkg/services/alerting/eval_context_test.go
@@ -2,11 +2,11 @@ package alerting
import (
"context"
- "fmt"
+ "errors"
"testing"
+ "time"
"github.com/grafana/grafana/pkg/models"
- . "github.com/smartystreets/goconvey/convey"
)
func TestStateIsUpdatedWhenNeeded(t *testing.T) {
@@ -31,71 +31,176 @@ func TestStateIsUpdatedWhenNeeded(t *testing.T) {
})
}
-func TestAlertingEvalContext(t *testing.T) {
- Convey("Should compute and replace properly new rule state", t, func() {
+func TestGetStateFromEvalContext(t *testing.T) {
+ tcs := []struct {
+ name string
+ expected models.AlertStateType
+ applyFn func(ec *EvalContext)
+ }{
+ {
+ name: "ok -> alerting",
+ expected: models.AlertStateAlerting,
+ applyFn: func(ec *EvalContext) {
+ ec.Firing = true
+ ec.PrevAlertState = models.AlertStateOK
+ },
+ },
+ {
+ name: "ok -> error(alerting)",
+ expected: models.AlertStateAlerting,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStateOK
+ ec.Error = errors.New("test error")
+ ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
+ },
+ },
+ {
+ name: "ok -> pending. since its been firing for less than FOR",
+ expected: models.AlertStatePending,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStateOK
+ ec.Firing = true
+ ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
+ ec.Rule.For = time.Minute * 5
+ },
+ },
+ {
+ name: "ok -> pending. since it has to be pending longer than FOR and prev state is ok",
+ expected: models.AlertStatePending,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStateOK
+ ec.Firing = true
+ ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
+ ec.Rule.For = time.Minute * 2
+ },
+ },
+ {
+ name: "pending -> alerting. since its been firing for more than FOR and prev state is pending",
+ expected: models.AlertStateAlerting,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStatePending
+ ec.Firing = true
+ ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
+ ec.Rule.For = time.Minute * 2
+ },
+ },
+ {
+ name: "alerting -> alerting. should not update regardless of FOR",
+ expected: models.AlertStateAlerting,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStateAlerting
+ ec.Firing = true
+ ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
+ ec.Rule.For = time.Minute * 2
+ },
+ },
+ {
+ name: "ok -> ok. should not update regardless of FOR",
+ expected: models.AlertStateOK,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStateOK
+ ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
+ ec.Rule.For = time.Minute * 2
+ },
+ },
+ {
+ name: "ok -> error(keep_last)",
+ expected: models.AlertStateOK,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStateOK
+ ec.Error = errors.New("test error")
+ ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
+ },
+ },
+ {
+ name: "pending -> error(keep_last)",
+ expected: models.AlertStatePending,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStatePending
+ ec.Error = errors.New("test error")
+ ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
+ },
+ },
+ {
+ name: "ok -> no_data(alerting)",
+ expected: models.AlertStateAlerting,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStateOK
+ ec.Rule.NoDataState = models.NoDataSetAlerting
+ ec.NoDataFound = true
+ },
+ },
+ {
+ name: "ok -> no_data(keep_last)",
+ expected: models.AlertStateOK,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStateOK
+ ec.Rule.NoDataState = models.NoDataKeepState
+ ec.NoDataFound = true
+ },
+ },
+ {
+ name: "pending -> no_data(keep_last)",
+ expected: models.AlertStatePending,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStatePending
+ ec.Rule.NoDataState = models.NoDataKeepState
+ ec.NoDataFound = true
+ },
+ },
+ {
+ name: "pending -> no_data(alerting) with for duration have not passed",
+ expected: models.AlertStatePending,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStatePending
+ ec.Rule.NoDataState = models.NoDataSetAlerting
+ ec.NoDataFound = true
+ ec.Rule.For = time.Minute * 5
+ ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
+ },
+ },
+ {
+ name: "pending -> no_data(alerting) should set alerting since time passed FOR",
+ expected: models.AlertStateAlerting,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStatePending
+ ec.Rule.NoDataState = models.NoDataSetAlerting
+ ec.NoDataFound = true
+ ec.Rule.For = time.Minute * 2
+ ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
+ },
+ },
+ {
+ name: "pending -> error(alerting) with for duration have not passed ",
+ expected: models.AlertStatePending,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStatePending
+ ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
+ ec.Error = errors.New("test error")
+ ec.Rule.For = time.Minute * 5
+ ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
+ },
+ },
+ {
+ name: "pending -> error(alerting) should set alerting since time passed FOR",
+ expected: models.AlertStateAlerting,
+ applyFn: func(ec *EvalContext) {
+ ec.PrevAlertState = models.AlertStatePending
+ ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
+ ec.Error = errors.New("test error")
+ ec.Rule.For = time.Minute * 2
+ ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
+ },
+ },
+ }
+
+ for _, tc := range tcs {
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
- dummieError := fmt.Errorf("dummie error")
- Convey("ok -> alerting", func() {
- ctx.PrevAlertState = models.AlertStateOK
- ctx.Firing = true
-
- ctx.Rule.State = ctx.GetNewState()
- So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
- })
-
- Convey("ok -> error(alerting)", func() {
- ctx.PrevAlertState = models.AlertStateOK
- ctx.Error = dummieError
- ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
-
- ctx.Rule.State = ctx.GetNewState()
- So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
- })
-
- Convey("ok -> error(keep_last)", func() {
- ctx.PrevAlertState = models.AlertStateOK
- ctx.Error = dummieError
- ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
-
- ctx.Rule.State = ctx.GetNewState()
- So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
- })
-
- Convey("pending -> error(keep_last)", func() {
- ctx.PrevAlertState = models.AlertStatePending
- ctx.Error = dummieError
- ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
-
- ctx.Rule.State = ctx.GetNewState()
- So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
- })
-
- Convey("ok -> no_data(alerting)", func() {
- ctx.PrevAlertState = models.AlertStateOK
- ctx.Rule.NoDataState = models.NoDataSetAlerting
- ctx.NoDataFound = true
-
- ctx.Rule.State = ctx.GetNewState()
- So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
- })
-
- Convey("ok -> no_data(keep_last)", func() {
- ctx.PrevAlertState = models.AlertStateOK
- ctx.Rule.NoDataState = models.NoDataKeepState
- ctx.NoDataFound = true
-
- ctx.Rule.State = ctx.GetNewState()
- So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
- })
-
- Convey("pending -> no_data(keep_last)", func() {
- ctx.PrevAlertState = models.AlertStatePending
- ctx.Rule.NoDataState = models.NoDataKeepState
- ctx.NoDataFound = true
-
- ctx.Rule.State = ctx.GetNewState()
- So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
- })
- })
+ tc.applyFn(ctx)
+ have := ctx.GetNewState()
+ if have != tc.expected {
+ t.Errorf("failed: %s \n expected '%s' have '%s'\n", tc.name, tc.expected, string(have))
+ }
+ }
}
diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go
index 0abacc91313..e33e3dc2af3 100644
--- a/pkg/services/alerting/extractor.go
+++ b/pkg/services/alerting/extractor.go
@@ -2,8 +2,8 @@ package alerting
import (
"errors"
-
"fmt"
+ "time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -115,6 +115,15 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
return nil, ValidationError{Reason: "Could not parse frequency"}
}
+ rawFor := jsonAlert.Get("for").MustString()
+ var forValue time.Duration
+ if rawFor != "" {
+ forValue, err = time.ParseDuration(rawFor)
+ if err != nil {
+ return nil, ValidationError{Reason: "Could not parse for"}
+ }
+ }
+
alert := &m.Alert{
DashboardId: e.Dash.Id,
OrgId: e.OrgID,
@@ -124,6 +133,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
Handler: jsonAlert.Get("handler").MustInt64(),
Message: jsonAlert.Get("message").MustString(),
Frequency: frequency,
+ For: forValue,
}
for _, condition := range jsonAlert.Get("conditions").MustArray() {
diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go
index 0890b9e1bd1..9665a657bb7 100644
--- a/pkg/services/alerting/extractor_test.go
+++ b/pkg/services/alerting/extractor_test.go
@@ -3,6 +3,7 @@ package alerting
import (
"io/ioutil"
"testing"
+ "time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -46,7 +47,7 @@ func TestAlertRuleExtraction(t *testing.T) {
return nil
})
- json, err := ioutil.ReadFile("./test-data/graphite-alert.json")
+ json, err := ioutil.ReadFile("./testdata/graphite-alert.json")
So(err, ShouldBeNil)
Convey("Extractor should not modify the original json", func() {
@@ -118,6 +119,11 @@ func TestAlertRuleExtraction(t *testing.T) {
So(alerts[1].PanelId, ShouldEqual, 4)
})
+ Convey("should extract for param", func() {
+ So(alerts[0].For, ShouldEqual, time.Minute*2)
+ So(alerts[1].For, ShouldEqual, time.Duration(0))
+ })
+
Convey("should extract name and desc", func() {
So(alerts[0].Name, ShouldEqual, "name1")
So(alerts[0].Message, ShouldEqual, "desc1")
@@ -140,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Panels missing id should return error", func() {
- panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json")
+ panelWithoutId, err := ioutil.ReadFile("./testdata/panels-missing-id.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson(panelWithoutId)
@@ -156,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Panel with id set to zero should return error", func() {
- panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json")
+ panelWithIdZero, err := ioutil.ReadFile("./testdata/panel-with-id-0.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson(panelWithIdZero)
@@ -172,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Parse alerts from dashboard without rows", func() {
- json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
+ json, err := ioutil.ReadFile("./testdata/v5-dashboard.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson(json)
@@ -192,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Parse and validate dashboard containing influxdb alert", func() {
- json, err := ioutil.ReadFile("./test-data/influxdb-alert.json")
+ json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson(json)
@@ -221,7 +227,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Should be able to extract collapsed panels", func() {
- json, err := ioutil.ReadFile("./test-data/collapsed-panels.json")
+ json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson(json)
@@ -242,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Parse and validate dashboard without id and containing an alert", func() {
- json, err := ioutil.ReadFile("./test-data/dash-without-id.json")
+ json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
So(err, ShouldBeNil)
dashJSON, err := simplejson.NewJson(json)
diff --git a/pkg/services/alerting/notifiers/alertmanager_test.go b/pkg/services/alerting/notifiers/alertmanager_test.go
index 3549b536e48..7510742ed17 100644
--- a/pkg/services/alerting/notifiers/alertmanager_test.go
+++ b/pkg/services/alerting/notifiers/alertmanager_test.go
@@ -1,13 +1,60 @@
package notifiers
import (
+ "context"
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
+func TestWhenAlertManagerShouldNotify(t *testing.T) {
+ tcs := []struct {
+ prevState m.AlertStateType
+ newState m.AlertStateType
+
+ expect bool
+ }{
+ {
+ prevState: m.AlertStatePending,
+ newState: m.AlertStateOK,
+ expect: false,
+ },
+ {
+ prevState: m.AlertStateAlerting,
+ newState: m.AlertStateOK,
+ expect: true,
+ },
+ {
+ prevState: m.AlertStateOK,
+ newState: m.AlertStatePending,
+ expect: false,
+ },
+ {
+ prevState: m.AlertStateUnknown,
+ newState: m.AlertStatePending,
+ expect: false,
+ },
+ }
+
+ for _, tc := range tcs {
+ am := &AlertmanagerNotifier{log: log.New("test.logger")}
+ evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
+ State: tc.prevState,
+ })
+
+ evalContext.Rule.State = tc.newState
+
+ res := am.ShouldNotify(context.TODO(), evalContext, &m.AlertNotificationState{})
+ if res != tc.expect {
+ t.Errorf("got %v expected %v", res, tc.expect)
+ }
+ }
+}
+
func TestAlertmanagerNotifier(t *testing.T) {
Convey("Alertmanager notifier tests", t, func() {
diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go
index d141d6cd257..d4a9975bcba 100644
--- a/pkg/services/alerting/notifiers/base.go
+++ b/pkg/services/alerting/notifiers/base.go
@@ -67,6 +67,16 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC
}
// Do not notify when we become OK for the first time.
+ if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStateOK {
+ return false
+ }
+
+ // Do not notify when we become OK for the first time.
+ if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStatePending {
+ return false
+ }
+
+ // Do not notify when we become OK from pending
if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK {
return false
}
diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go
index 5062828cb4f..3fd4447eefe 100644
--- a/pkg/services/alerting/notifiers/base_test.go
+++ b/pkg/services/alerting/notifiers/base_test.go
@@ -29,7 +29,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStateOK,
prevState: m.AlertStatePending,
sendReminder: false,
- state: &m.AlertNotificationState{},
expect: false,
},
@@ -38,7 +37,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStateAlerting,
prevState: m.AlertStateOK,
sendReminder: false,
- state: &m.AlertNotificationState{},
expect: true,
},
@@ -47,7 +45,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStatePending,
prevState: m.AlertStateOK,
sendReminder: false,
- state: &m.AlertNotificationState{},
expect: false,
},
@@ -56,7 +53,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStateOK,
prevState: m.AlertStateOK,
sendReminder: false,
- state: &m.AlertNotificationState{},
expect: false,
},
@@ -65,7 +61,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStateOK,
prevState: m.AlertStateOK,
sendReminder: true,
- state: &m.AlertNotificationState{},
expect: false,
},
@@ -74,7 +69,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStateOK,
prevState: m.AlertStateAlerting,
sendReminder: false,
- state: &m.AlertNotificationState{},
expect: true,
},
@@ -94,7 +88,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
prevState: m.AlertStateAlerting,
frequency: time.Minute * 10,
sendReminder: true,
- state: &m.AlertNotificationState{},
expect: true,
},
@@ -132,6 +125,27 @@ func TestShouldSendAlertNotification(t *testing.T) {
prevState: m.AlertStateOK,
state: &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-2 * time.Minute).Unix()},
+ expect: true,
+ },
+ {
+ name: "unknown -> ok",
+ prevState: m.AlertStateUnknown,
+ newState: m.AlertStateOK,
+
+ expect: false,
+ },
+ {
+ name: "unknown -> pending",
+ prevState: m.AlertStateUnknown,
+ newState: m.AlertStatePending,
+
+ expect: false,
+ },
+ {
+ name: "unknown -> alerting",
+ prevState: m.AlertStateUnknown,
+ newState: m.AlertStateAlerting,
+
expect: true,
},
}
@@ -141,6 +155,10 @@ func TestShouldSendAlertNotification(t *testing.T) {
State: tc.prevState,
})
+ if tc.state == nil {
+ tc.state = &m.AlertNotificationState{}
+ }
+
evalContext.Rule.State = tc.newState
nb := &NotifierBase{SendReminder: tc.sendReminder, Frequency: tc.frequency}
diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go
index 420ffeb9a55..ce12a8a6b96 100644
--- a/pkg/services/alerting/result_handler.go
+++ b/pkg/services/alerting/result_handler.go
@@ -73,6 +73,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
// when two servers are raising. This makes sure that the server
// with the last state change always sends a notification.
evalContext.Rule.StateChanges = cmd.Result.StateChanges
+
+ // Update the last state change of the alert rule in memory
+ evalContext.Rule.LastStateChange = time.Now()
}
// save annotation
diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go
index 999611f15c4..d2a505145ac 100644
--- a/pkg/services/alerting/rule.go
+++ b/pkg/services/alerting/rule.go
@@ -4,6 +4,7 @@ import (
"fmt"
"regexp"
"strconv"
+ "time"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -18,6 +19,8 @@ type Rule struct {
Frequency int64
Name string
Message string
+ LastStateChange time.Time
+ For time.Duration
NoDataState m.NoDataOption
ExecutionErrorState m.ExecutionErrorOption
State m.AlertStateType
@@ -100,6 +103,8 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
model.Message = ruleDef.Message
model.Frequency = ruleDef.Frequency
model.State = ruleDef.State
+ model.LastStateChange = ruleDef.NewStateDate
+ model.For = ruleDef.For
model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data"))
model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
model.StateChanges = ruleDef.StateChanges
diff --git a/pkg/services/alerting/test-data/collapsed-panels.json b/pkg/services/alerting/testdata/collapsed-panels.json
similarity index 100%
rename from pkg/services/alerting/test-data/collapsed-panels.json
rename to pkg/services/alerting/testdata/collapsed-panels.json
diff --git a/pkg/services/alerting/test-data/dash-without-id.json b/pkg/services/alerting/testdata/dash-without-id.json
similarity index 100%
rename from pkg/services/alerting/test-data/dash-without-id.json
rename to pkg/services/alerting/testdata/dash-without-id.json
diff --git a/pkg/services/alerting/test-data/graphite-alert.json b/pkg/services/alerting/testdata/graphite-alert.json
similarity index 98%
rename from pkg/services/alerting/test-data/graphite-alert.json
rename to pkg/services/alerting/testdata/graphite-alert.json
index 5f23e224f9a..3cb4ae1dd22 100644
--- a/pkg/services/alerting/test-data/graphite-alert.json
+++ b/pkg/services/alerting/testdata/graphite-alert.json
@@ -23,6 +23,7 @@
"message": "desc1",
"handler": 1,
"frequency": "60s",
+ "for": "2m",
"conditions": [
{
"type": "query",
diff --git a/pkg/services/alerting/test-data/influxdb-alert.json b/pkg/services/alerting/testdata/influxdb-alert.json
similarity index 100%
rename from pkg/services/alerting/test-data/influxdb-alert.json
rename to pkg/services/alerting/testdata/influxdb-alert.json
diff --git a/pkg/services/alerting/test-data/panel-with-id-0.json b/pkg/services/alerting/testdata/panel-with-id-0.json
similarity index 100%
rename from pkg/services/alerting/test-data/panel-with-id-0.json
rename to pkg/services/alerting/testdata/panel-with-id-0.json
diff --git a/pkg/services/alerting/test-data/panels-missing-id.json b/pkg/services/alerting/testdata/panels-missing-id.json
similarity index 100%
rename from pkg/services/alerting/test-data/panels-missing-id.json
rename to pkg/services/alerting/testdata/panels-missing-id.json
diff --git a/pkg/services/alerting/test-data/v5-dashboard.json b/pkg/services/alerting/testdata/v5-dashboard.json
similarity index 100%
rename from pkg/services/alerting/test-data/v5-dashboard.json
rename to pkg/services/alerting/testdata/v5-dashboard.json
diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go
index b52d1845a0b..7e334ff656f 100644
--- a/pkg/services/dashboards/dashboard_service.go
+++ b/pkg/services/dashboards/dashboard_service.go
@@ -165,7 +165,7 @@ func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand,
}
if err := bus.Dispatch(&alertCmd); err != nil {
- return models.ErrDashboardFailedToUpdateAlertData
+ return err
}
return nil
diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go
index 2f17402b80c..62ab348664f 100644
--- a/pkg/services/sqlstore/alert.go
+++ b/pkg/services/sqlstore/alert.go
@@ -193,7 +193,8 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
if alertToUpdate.ContainsUpdates(alert) {
alert.Updated = timeNow()
alert.State = alertToUpdate.State
- sess.MustCols("message")
+ sess.MustCols("message", "for")
+
_, err := sess.ID(alert.Id).Update(alert)
if err != nil {
return err
@@ -204,7 +205,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
} else {
alert.Updated = timeNow()
alert.Created = timeNow()
- alert.State = m.AlertStatePending
+ alert.State = m.AlertStateUnknown
alert.NewStateDate = timeNow()
_, err := sess.Insert(alert)
@@ -299,7 +300,7 @@ func PauseAlert(cmd *m.PauseAlertCommand) error {
params = append(params, string(m.AlertStatePaused))
params = append(params, timeNow())
} else {
- params = append(params, string(m.AlertStatePending))
+ params = append(params, string(m.AlertStateUnknown))
params = append(params, timeNow())
}
@@ -323,7 +324,7 @@ func PauseAllAlerts(cmd *m.PauseAllAlertCommand) error {
if cmd.Paused {
newState = string(m.AlertStatePaused)
} else {
- newState = string(m.AlertStatePending)
+ newState = string(m.AlertStateUnknown)
}
res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow())
diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go
index d97deb45f0e..40867e96b4d 100644
--- a/pkg/services/sqlstore/alert_test.go
+++ b/pkg/services/sqlstore/alert_test.go
@@ -109,7 +109,7 @@ func TestAlertingDataAccess(t *testing.T) {
So(alert.DashboardId, ShouldEqual, testDash.Id)
So(alert.PanelId, ShouldEqual, 1)
So(alert.Name, ShouldEqual, "Alerting title")
- So(alert.State, ShouldEqual, "pending")
+ So(alert.State, ShouldEqual, m.AlertStateUnknown)
So(alert.NewStateDate, ShouldNotBeNil)
So(alert.EvalData, ShouldNotBeNil)
So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
@@ -154,7 +154,7 @@ func TestAlertingDataAccess(t *testing.T) {
So(query.Result[0].Name, ShouldEqual, "Name")
Convey("Alert state should not be updated", func() {
- So(query.Result[0].State, ShouldEqual, "pending")
+ So(query.Result[0].State, ShouldEqual, m.AlertStateUnknown)
})
})
diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go
index 198a47b50ff..b5aeb26483c 100644
--- a/pkg/services/sqlstore/migrations/alert_mig.go
+++ b/pkg/services/sqlstore/migrations/alert_mig.go
@@ -133,4 +133,8 @@ func addAlertMigrations(mg *Migrator) {
mg.AddMigration("create alert_notification_state table v1", NewAddTableMigration(alert_notification_state))
mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id",
NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0]))
+
+ mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{
+ Name: "for", Type: DB_BigInt, Nullable: true,
+ }))
}
diff --git a/pkg/services/sqlstore/migrations/preferences_mig.go b/pkg/services/sqlstore/migrations/preferences_mig.go
index b3822fe5239..d8134f11f79 100644
--- a/pkg/services/sqlstore/migrations/preferences_mig.go
+++ b/pkg/services/sqlstore/migrations/preferences_mig.go
@@ -34,4 +34,13 @@ func addPreferencesMigrations(mg *Migrator) {
{Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: false},
{Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: false},
}))
+
+ mg.AddMigration("Add column team_id in preferences", NewAddColumnMigration(preferencesV2, &Column{
+ Name: "team_id", Type: DB_BigInt, Nullable: true,
+ }))
+
+ mg.AddMigration("Update team_id column values in preferences", NewRawSqlMigration("").
+ Sqlite("UPDATE preferences SET team_id=0 WHERE team_id IS NULL;").
+ Postgres("UPDATE preferences SET team_id=0 WHERE team_id IS NULL;").
+ Mysql("UPDATE preferences SET team_id=0 WHERE team_id IS NULL;"))
}
diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go
index 04e787971d9..858a2c77075 100644
--- a/pkg/services/sqlstore/preferences.go
+++ b/pkg/services/sqlstore/preferences.go
@@ -1,6 +1,7 @@
package sqlstore
import (
+ "strings"
"time"
"github.com/grafana/grafana/pkg/bus"
@@ -16,11 +17,22 @@ func init() {
}
func GetPreferencesWithDefaults(query *m.GetPreferencesWithDefaultsQuery) error {
-
+ params := make([]interface{}, 0)
+ filter := ""
+ if len(query.User.Teams) > 0 {
+ filter = "(org_id=? AND team_id IN (?" + strings.Repeat(",?", len(query.User.Teams)-1) + ")) OR "
+ params = append(params, query.User.OrgId)
+ for _, v := range query.User.Teams {
+ params = append(params, v)
+ }
+ }
+ filter += "(org_id=? AND user_id=? AND team_id=0) OR (org_id=? AND team_id=0 AND user_id=0)"
+ params = append(params, query.User.OrgId)
+ params = append(params, query.User.UserId)
+ params = append(params, query.User.OrgId)
prefs := make([]*m.Preferences, 0)
- filter := "(org_id=? AND user_id=?) OR (org_id=? AND user_id=0)"
- err := x.Where(filter, query.OrgId, query.UserId, query.OrgId).
- OrderBy("user_id ASC").
+ err := x.Where(filter, params...).
+ OrderBy("user_id ASC, team_id ASC").
Find(&prefs)
if err != nil {
@@ -50,9 +62,8 @@ func GetPreferencesWithDefaults(query *m.GetPreferencesWithDefaultsQuery) error
}
func GetPreferences(query *m.GetPreferencesQuery) error {
-
var prefs m.Preferences
- exists, err := x.Where("org_id=? AND user_id=?", query.OrgId, query.UserId).Get(&prefs)
+ exists, err := x.Where("org_id=? AND user_id=? AND team_id=?", query.OrgId, query.UserId, query.TeamId).Get(&prefs)
if err != nil {
return err
@@ -71,7 +82,7 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error {
return inTransaction(func(sess *DBSession) error {
var prefs m.Preferences
- exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&prefs)
+ exists, err := sess.Where("org_id=? AND user_id=? AND team_id=?", cmd.OrgId, cmd.UserId, cmd.TeamId).Get(&prefs)
if err != nil {
return err
}
@@ -80,6 +91,7 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error {
prefs = m.Preferences{
UserId: cmd.UserId,
OrgId: cmd.OrgId,
+ TeamId: cmd.TeamId,
HomeDashboardId: cmd.HomeDashboardId,
Timezone: cmd.Timezone,
Theme: cmd.Theme,
diff --git a/pkg/services/sqlstore/preferences_test.go b/pkg/services/sqlstore/preferences_test.go
new file mode 100644
index 00000000000..f9a839bf5a7
--- /dev/null
+++ b/pkg/services/sqlstore/preferences_test.go
@@ -0,0 +1,91 @@
+package sqlstore
+
+import (
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+
+ "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/setting"
+)
+
+func TestPreferencesDataAccess(t *testing.T) {
+ Convey("Testing preferences data access", t, func() {
+ InitTestDB(t)
+
+ Convey("GetPreferencesWithDefaults with no saved preferences should return defaults", func() {
+ query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{}}
+ err := GetPreferencesWithDefaults(query)
+ So(err, ShouldBeNil)
+ So(query.Result.Theme, ShouldEqual, setting.DefaultTheme)
+ So(query.Result.Timezone, ShouldEqual, "browser")
+ So(query.Result.HomeDashboardId, ShouldEqual, 0)
+ })
+
+ Convey("GetPreferencesWithDefaults with saved org and user home dashboard should return user home dashboard", func() {
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4})
+
+ query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 1}}
+ err := GetPreferencesWithDefaults(query)
+ So(err, ShouldBeNil)
+ So(query.Result.HomeDashboardId, ShouldEqual, 4)
+ })
+
+ Convey("GetPreferencesWithDefaults with saved org and other user home dashboard should return org home dashboard", func() {
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4})
+
+ query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 2}}
+ err := GetPreferencesWithDefaults(query)
+ So(err, ShouldBeNil)
+ So(query.Result.HomeDashboardId, ShouldEqual, 1)
+ })
+
+ Convey("GetPreferencesWithDefaults with saved org and teams home dashboard should return last team home dashboard", func() {
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3})
+
+ query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, Teams: []int64{2, 3}}}
+ err := GetPreferencesWithDefaults(query)
+ So(err, ShouldBeNil)
+ So(query.Result.HomeDashboardId, ShouldEqual, 3)
+ })
+
+ Convey("GetPreferencesWithDefaults with saved org and other teams home dashboard should return org home dashboard", func() {
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3})
+
+ query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1}}
+ err := GetPreferencesWithDefaults(query)
+ So(err, ShouldBeNil)
+ So(query.Result.HomeDashboardId, ShouldEqual, 1)
+ })
+
+ Convey("GetPreferencesWithDefaults with saved org, teams and user home dashboard should return user home dashboard", func() {
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4})
+
+ query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 1, Teams: []int64{2, 3}}}
+ err := GetPreferencesWithDefaults(query)
+ So(err, ShouldBeNil)
+ So(query.Result.HomeDashboardId, ShouldEqual, 4)
+ })
+
+ Convey("GetPreferencesWithDefaults with saved org, other teams and user home dashboard should return org home dashboard", func() {
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3})
+ SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4})
+
+ query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 2}}
+ err := GetPreferencesWithDefaults(query)
+ So(err, ShouldBeNil)
+ So(query.Result.HomeDashboardId, ShouldEqual, 1)
+ })
+ })
+}
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index afae642f5b3..1417392fdf8 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -57,6 +57,9 @@ var (
IsEnterprise bool
ApplicationName string
+ // packaging
+ Packaging = "unknown"
+
// Paths
HomePath string
PluginsPath string
@@ -112,6 +115,7 @@ var (
ExternalUserMngLinkUrl string
ExternalUserMngLinkName string
ExternalUserMngInfo string
+ OAuthAutoLogin bool
ViewersCanEdit bool
// Http auth
@@ -215,6 +219,8 @@ type Cfg struct {
DisableBruteForceLoginProtection bool
TempDataLifetime time.Duration
MetricsEndpointEnabled bool
+ MetricsEndpointBasicAuthUsername string
+ MetricsEndpointBasicAuthPassword string
EnableAlphaPanels bool
EnterpriseLicensePath string
}
@@ -626,6 +632,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
auth := iniFile.Section("auth")
DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)
+ OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false)
SignoutRedirectUrl = auth.Key("signout_redirect_url").String()
// anonymous access
@@ -676,6 +683,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true)
+ cfg.MetricsEndpointBasicAuthUsername = iniFile.Section("metrics").Key("basic_auth_username").String()
+ cfg.MetricsEndpointBasicAuthPassword = iniFile.Section("metrics").Key("basic_auth_password").String()
analytics := iniFile.Section("analytics")
ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)
diff --git a/pkg/social/google_oauth.go b/pkg/social/google_oauth.go
index e9ab08305f6..05ae2a481f2 100644
--- a/pkg/social/google_oauth.go
+++ b/pkg/social/google_oauth.go
@@ -32,6 +32,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool {
func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
var data struct {
+ Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
@@ -47,6 +48,7 @@ func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
}
return &BasicUserInfo{
+ Id: data.Id,
Name: data.Name,
Email: data.Email,
Login: data.Email,
diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go
index 437457df52a..8bb1ab6c928 100644
--- a/pkg/tsdb/cloudwatch/cloudwatch.go
+++ b/pkg/tsdb/cloudwatch/cloudwatch.go
@@ -126,6 +126,18 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
}
eg.Go(func() error {
+ defer func() {
+ if err := recover(); err != nil {
+ plog.Error("Execute Query Panic", "error", err, "stack", log.Stack(1))
+ if theErr, ok := err.(error); ok {
+ resultChan <- &tsdb.QueryResult{
+ RefId: query.RefId,
+ Error: theErr,
+ }
+ }
+ }
+ }()
+
queryRes, err := e.executeQuery(ectx, query, queryContext)
if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
return err
@@ -146,6 +158,17 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
for region, getMetricDataQuery := range getMetricDataQueries {
q := getMetricDataQuery
eg.Go(func() error {
+ defer func() {
+ if err := recover(); err != nil {
+ plog.Error("Execute Get Metric Data Query Panic", "error", err, "stack", log.Stack(1))
+ if theErr, ok := err.(error); ok {
+ resultChan <- &tsdb.QueryResult{
+ Error: theErr,
+ }
+ }
+ }
+ }()
+
queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext)
if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
return err
@@ -188,8 +211,8 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatch
return nil, err
}
- if endTime.Before(startTime) {
- return nil, fmt.Errorf("Invalid time range: End time can't be before start time")
+ if !startTime.Before(endTime) {
+ return nil, fmt.Errorf("Invalid time range: Start time must be before end time")
}
params := &cloudwatch.GetMetricStatisticsInput{
diff --git a/pkg/tsdb/cloudwatch/cloudwatch_test.go b/pkg/tsdb/cloudwatch/cloudwatch_test.go
index 32b8c910f2b..3b0b073eb06 100644
--- a/pkg/tsdb/cloudwatch/cloudwatch_test.go
+++ b/pkg/tsdb/cloudwatch/cloudwatch_test.go
@@ -1,9 +1,13 @@
package cloudwatch
import (
+ "context"
"testing"
"time"
+ "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/tsdb"
+
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/components/null"
@@ -14,6 +18,24 @@ import (
func TestCloudWatch(t *testing.T) {
Convey("CloudWatch", t, func() {
+ Convey("executeQuery", func() {
+ e := &CloudWatchExecutor{
+ DataSource: &models.DataSource{
+ JsonData: simplejson.New(),
+ },
+ }
+
+ Convey("End time before start time should result in error", func() {
+ _, err := e.executeQuery(context.Background(), &CloudWatchQuery{}, &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("now-1h", "now-2h")})
+ So(err.Error(), ShouldEqual, "Invalid time range: Start time must be before end time")
+ })
+
+ Convey("End time equals start time should result in error", func() {
+ _, err := e.executeQuery(context.Background(), &CloudWatchQuery{}, &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("now-1h", "now-1h")})
+ So(err.Error(), ShouldEqual, "Invalid time range: Start time must be before end time")
+ })
+ })
+
Convey("can parse cloudwatch json model", func() {
json := `
{
diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go
index 1a860519f2b..76eadf1c8dc 100644
--- a/pkg/tsdb/cloudwatch/metric_find_query.go
+++ b/pkg/tsdb/cloudwatch/metric_find_query.go
@@ -46,6 +46,7 @@ func init() {
"AWS/Billing": {"EstimatedCharges"},
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
+ "AWS/CloudHSM": {"HsmUnhealthy", "HsmTemperature", "HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSslCtxsOccupied", "HsmSessionCount", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"},
"AWS/Connect": {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
"AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
"AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
@@ -121,6 +122,7 @@ func init() {
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
"AWS/CloudFront": {"DistributionId", "Region"},
"AWS/CloudSearch": {},
+ "AWS/CloudHSM": {"Region", "ClusterId", "HsmId"},
"AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
"AWS/DX": {"ConnectionId"},
diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go
index 4ebe0db8f89..f5a200f60ce 100644
--- a/pkg/tsdb/elasticsearch/client/client.go
+++ b/pkg/tsdb/elasticsearch/client/client.go
@@ -65,7 +65,7 @@ var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange *tsdb
clientLog.Debug("Creating new client", "version", version, "timeField", timeField, "indices", strings.Join(indices, ", "))
switch version {
- case 2, 5, 56:
+ case 2, 5, 56, 60:
return &baseClientImpl{
ctx: ctx,
ds: ds,
diff --git a/pkg/tsdb/elasticsearch/client/client_test.go b/pkg/tsdb/elasticsearch/client/client_test.go
index 540a999688a..7002aa5db4f 100644
--- a/pkg/tsdb/elasticsearch/client/client_test.go
+++ b/pkg/tsdb/elasticsearch/client/client_test.go
@@ -90,6 +90,19 @@ func TestClient(t *testing.T) {
So(err, ShouldBeNil)
So(c.GetVersion(), ShouldEqual, 56)
})
+
+ Convey("When version 60 should return v6.0 client", func() {
+ ds := &models.DataSource{
+ JsonData: simplejson.NewFromAny(map[string]interface{}{
+ "esVersion": 60,
+ "timeField": "@timestamp",
+ }),
+ }
+
+ c, err := NewClient(context.Background(), ds, nil)
+ So(err, ShouldBeNil)
+ So(c.GetVersion(), ShouldEqual, 60)
+ })
})
Convey("Given a fake http client", func() {
@@ -153,8 +166,6 @@ func TestClient(t *testing.T) {
jBody, err := simplejson.NewJson(bodyBytes)
So(err, ShouldBeNil)
- fmt.Println("body", string(headerBytes))
-
So(jHeader.Get("index").MustString(), ShouldEqual, "metrics-2018.05.15")
So(jHeader.Get("ignore_unavailable").MustBool(false), ShouldEqual, true)
So(jHeader.Get("search_type").MustString(), ShouldEqual, "count")
@@ -209,8 +220,6 @@ func TestClient(t *testing.T) {
jBody, err := simplejson.NewJson(bodyBytes)
So(err, ShouldBeNil)
- fmt.Println("body", string(headerBytes))
-
So(jHeader.Get("index").MustString(), ShouldEqual, "metrics-2018.05.15")
So(jHeader.Get("ignore_unavailable").MustBool(false), ShouldEqual, true)
So(jHeader.Get("search_type").MustString(), ShouldEqual, "query_then_fetch")
@@ -265,8 +274,6 @@ func TestClient(t *testing.T) {
jBody, err := simplejson.NewJson(bodyBytes)
So(err, ShouldBeNil)
- fmt.Println("body", string(headerBytes))
-
So(jHeader.Get("index").MustString(), ShouldEqual, "metrics-2018.05.15")
So(jHeader.Get("ignore_unavailable").MustBool(false), ShouldEqual, true)
So(jHeader.Get("search_type").MustString(), ShouldEqual, "query_then_fetch")
diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go
index 6100d3b401e..ec1e9ff01bd 100644
--- a/pkg/tsdb/influxdb/influxdb.go
+++ b/pkg/tsdb/influxdb/influxdb.go
@@ -96,16 +96,15 @@ func (e *InfluxDBExecutor) Query(ctx context.Context, dsInfo *models.DataSource,
}
func (e *InfluxDBExecutor) getQuery(dsInfo *models.DataSource, queries []*tsdb.Query, context *tsdb.TsdbQuery) (*Query, error) {
- for _, v := range queries {
-
- query, err := e.QueryParser.Parse(v.Model, dsInfo)
+ // The model supports multiple queries, but right now this is only used from
+ // alerting so we only needed to support batch executing 1 query at a time.
+ if len(queries) > 0 {
+ query, err := e.QueryParser.Parse(queries[0].Model, dsInfo)
if err != nil {
return nil, err
}
-
return query, nil
}
-
return nil, fmt.Errorf("query request contains no queries")
}
diff --git a/public/app/core/actions/user.ts b/public/app/core/actions/user.ts
deleted file mode 100644
index dba0588c058..00000000000
--- a/public/app/core/actions/user.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { ThunkAction } from 'redux-thunk';
-import { getBackendSrv } from '../services/backend_srv';
-import { DashboardAcl, DashboardSearchHit, StoreState } from '../../types';
-
-type ThunkResult = ThunkAction;
-
-export type Action = LoadStarredDashboardsAction;
-
-export enum ActionTypes {
- LoadStarredDashboards = 'LOAD_STARRED_DASHBOARDS',
-}
-
-interface LoadStarredDashboardsAction {
- type: ActionTypes.LoadStarredDashboards;
- payload: DashboardSearchHit[];
-}
-
-const starredDashboardsLoaded = (dashboards: DashboardAcl[]) => ({
- type: ActionTypes.LoadStarredDashboards,
- payload: dashboards,
-});
-
-export function loadStarredDashboards(): ThunkResult {
- return async dispatch => {
- const starredDashboards = await getBackendSrv().search({ starred: true });
- dispatch(starredDashboardsLoaded(starredDashboards));
- };
-}
diff --git a/public/app/core/components/Picker/SimplePicker.tsx b/public/app/core/components/Picker/SimplePicker.tsx
index 6c9e8aca199..817cd14150f 100644
--- a/public/app/core/components/Picker/SimplePicker.tsx
+++ b/public/app/core/components/Picker/SimplePicker.tsx
@@ -5,13 +5,14 @@ import ResetStyles from './ResetStyles';
interface Props {
className?: string;
- defaultValue: any;
+ defaultValue?: any;
getOptionLabel: (item: any) => string;
getOptionValue: (item: any) => string;
onSelected: (item: any) => {} | void;
options: any[];
placeholder?: string;
width: number;
+ value: any;
}
const SimplePicker: SFC = ({
@@ -23,6 +24,7 @@ const SimplePicker: SFC = ({
options,
placeholder,
width,
+ value,
}) => {
return (
= ({
Option: DescriptionOption,
}}
defaultValue={defaultValue}
+ value={value}
getOptionLabel={getOptionLabel}
getOptionValue={getOptionValue}
isSearchable={false}
diff --git a/public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap b/public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap
index 748fcbee4aa..b376ab24934 100644
--- a/public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap
+++ b/public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap
@@ -14,3 +14,4 @@ exports[`PickerOption renders correctly 1`] = `
`;
+
\ No newline at end of file
diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx
new file mode 100644
index 00000000000..098cbe16ab8
--- /dev/null
+++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx
@@ -0,0 +1,141 @@
+import React, { PureComponent } from 'react';
+
+import { Label } from 'app/core/components/Label/Label';
+import SimplePicker from 'app/core/components/Picker/SimplePicker';
+import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
+
+import { DashboardSearchHit } from 'app/types';
+
+export interface Props {
+ resourceUri: string;
+}
+
+export interface State {
+ homeDashboardId: number;
+ theme: string;
+ timezone: string;
+ dashboards: DashboardSearchHit[];
+}
+
+const themes = [{ value: '', text: 'Default' }, { value: 'dark', text: 'Dark' }, { value: 'light', text: 'Light' }];
+
+const timezones = [
+ { value: '', text: 'Default' },
+ { value: 'browser', text: 'Local browser time' },
+ { value: 'utc', text: 'UTC' },
+];
+
+export class SharedPreferences extends PureComponent {
+ backendSrv: BackendSrv = getBackendSrv();
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ homeDashboardId: 0,
+ theme: '',
+ timezone: '',
+ dashboards: [],
+ };
+ }
+
+ async componentDidMount() {
+ const prefs = await this.backendSrv.get(`/api/${this.props.resourceUri}/preferences`);
+ const dashboards = await this.backendSrv.search({ starred: true });
+
+ if (prefs.homeDashboardId > 0 && !dashboards.find(d => d.id === prefs.homeDashboardId)) {
+ const missing = await this.backendSrv.search({ dashboardIds: [prefs.homeDashboardId] });
+ if (missing && missing.length > 0) {
+ dashboards.push(missing[0]);
+ }
+ }
+
+ this.setState({
+ homeDashboardId: prefs.homeDashboardId,
+ theme: prefs.theme,
+ timezone: prefs.timezone,
+ dashboards: [{ id: 0, title: 'Default', tags: [], type: '', uid: '', uri: '', url: '' }, ...dashboards],
+ });
+ }
+
+ onSubmitForm = async event => {
+ event.preventDefault();
+
+ const { homeDashboardId, theme, timezone } = this.state;
+
+ await this.backendSrv.put(`/api/${this.props.resourceUri}/preferences`, {
+ homeDashboardId,
+ theme,
+ timezone,
+ });
+ window.location.reload();
+ };
+
+ onThemeChanged = (theme: string) => {
+ this.setState({ theme });
+ };
+
+ onTimeZoneChanged = (timezone: string) => {
+ this.setState({ timezone });
+ };
+
+ onHomeDashboardChanged = (dashboardId: number) => {
+ this.setState({ homeDashboardId: dashboardId });
+ };
+
+ render() {
+ const { theme, timezone, homeDashboardId, dashboards } = this.state;
+
+ return (
+
+ );
+ }
+}
+
+export default SharedPreferences;
diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts
index ab0a3f26a88..2485dbda40a 100644
--- a/public/app/core/logs_model.ts
+++ b/public/app/core/logs_model.ts
@@ -3,25 +3,26 @@ import { TimeSeries } from 'app/core/core';
import colors from 'app/core/utils/colors';
export enum LogLevel {
- crit = 'crit',
- warn = 'warn',
+ crit = 'critical',
+ critical = 'critical',
+ warn = 'warning',
+ warning = 'warning',
err = 'error',
error = 'error',
info = 'info',
debug = 'debug',
trace = 'trace',
- none = 'none',
+ unkown = 'unkown',
}
export const LogLevelColor = {
- [LogLevel.crit]: colors[7],
- [LogLevel.warn]: colors[1],
- [LogLevel.err]: colors[4],
+ [LogLevel.critical]: colors[7],
+ [LogLevel.warning]: colors[1],
[LogLevel.error]: colors[4],
[LogLevel.info]: colors[0],
- [LogLevel.debug]: colors[3],
- [LogLevel.trace]: colors[3],
- [LogLevel.none]: '#eee',
+ [LogLevel.debug]: colors[5],
+ [LogLevel.trace]: colors[2],
+ [LogLevel.unkown]: '#ddd',
};
export interface LogSearchMatch {
@@ -31,6 +32,7 @@ export interface LogSearchMatch {
}
export interface LogRow {
+ duplicates?: number;
entry: string;
key: string; // timestamp + labels
labels: string;
@@ -71,6 +73,71 @@ export interface LogsStreamLabels {
[key: string]: string;
}
+export enum LogsDedupStrategy {
+ none = 'none',
+ exact = 'exact',
+ numbers = 'numbers',
+ signature = 'signature',
+}
+
+const isoDateRegexp = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d[,\.]\d+([+-][0-2]\d:[0-5]\d|Z)/g;
+function isDuplicateRow(row: LogRow, other: LogRow, strategy: LogsDedupStrategy): boolean {
+ switch (strategy) {
+ case LogsDedupStrategy.exact:
+ // Exact still strips dates
+ return row.entry.replace(isoDateRegexp, '') === other.entry.replace(isoDateRegexp, '');
+
+ case LogsDedupStrategy.numbers:
+ return row.entry.replace(/\d/g, '') === other.entry.replace(/\d/g, '');
+
+ case LogsDedupStrategy.signature:
+ return row.entry.replace(/\w/g, '') === other.entry.replace(/\w/g, '');
+
+ default:
+ return false;
+ }
+}
+
+export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): LogsModel {
+ if (strategy === LogsDedupStrategy.none) {
+ return logs;
+ }
+
+ const dedupedRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => {
+ const previous = result[result.length - 1];
+ if (index > 0 && isDuplicateRow(row, previous, strategy)) {
+ previous.duplicates++;
+ } else {
+ row.duplicates = 0;
+ result.push(row);
+ }
+ return result;
+ }, []);
+
+ return {
+ ...logs,
+ rows: dedupedRows,
+ };
+}
+
+export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set): LogsModel {
+ if (hiddenLogLevels.size === 0) {
+ return logs;
+ }
+
+ const filteredRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => {
+ if (!hiddenLogLevels.has(row.logLevel)) {
+ result.push(row);
+ }
+ return result;
+ }, []);
+
+ return {
+ ...logs,
+ rows: filteredRows,
+ };
+}
+
export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] {
// Graph time series by log level
const seriesByLevel = {};
diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts
index 6455849ab46..1c8670ed0d6 100644
--- a/public/app/core/reducers/index.ts
+++ b/public/app/core/reducers/index.ts
@@ -1,11 +1,9 @@
import { navIndexReducer as navIndex } from './navModel';
import { locationReducer as location } from './location';
import { appNotificationsReducer as appNotifications } from './appNotification';
-import { userReducer as user } from './user';
export default {
navIndex,
location,
appNotifications,
- user,
};
diff --git a/public/app/core/reducers/user.ts b/public/app/core/reducers/user.ts
deleted file mode 100644
index d49395060ce..00000000000
--- a/public/app/core/reducers/user.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { DashboardSearchHit, UserState } from '../../types';
-import { Action, ActionTypes } from '../actions/user';
-
-const initialState: UserState = {
- starredDashboards: [] as DashboardSearchHit[],
-};
-
-export const userReducer = (state: UserState = initialState, action: Action): UserState => {
- switch (action.type) {
- case ActionTypes.LoadStarredDashboards:
- return { ...state, starredDashboards: action.payload };
- }
-
- return state;
-};
diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts
index 9b7ede59853..c27842ab54f 100644
--- a/public/app/core/services/dynamic_directive_srv.ts
+++ b/public/app/core/services/dynamic_directive_srv.ts
@@ -21,7 +21,6 @@ class DynamicDirectiveSrv {
}
if (!directiveInfo.fn.registered) {
- console.log('register panel tab');
coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
directiveInfo.fn.registered = true;
}
diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index 6fe57dfa77a..c02f6850e8b 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -32,8 +32,8 @@ export class KeybindingSrv {
this.setupGlobal();
appEvents.on('show-modal', () => (this.modalOpen = true));
- $rootScope.onAppEvent('timepickerOpen', () => (this.timepickerOpen = true));
- $rootScope.onAppEvent('timepickerClosed', () => (this.timepickerOpen = false));
+ appEvents.on('timepickerOpen', () => (this.timepickerOpen = true));
+ appEvents.on('timepickerClosed', () => (this.timepickerOpen = false));
}
setupGlobal() {
diff --git a/public/app/core/specs/logs_model.test.ts b/public/app/core/specs/logs_model.test.ts
new file mode 100644
index 00000000000..5e427468339
--- /dev/null
+++ b/public/app/core/specs/logs_model.test.ts
@@ -0,0 +1,108 @@
+import { dedupLogRows, LogsDedupStrategy, LogsModel } from '../logs_model';
+
+describe('dedupLogRows()', () => {
+ test('should return rows as is when dedup is set to none', () => {
+ const logs = {
+ rows: [
+ {
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ {
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ ],
+ };
+ expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toMatchObject(logs.rows);
+ });
+
+ test('should dedup on exact matches', () => {
+ const logs = {
+ rows: [
+ {
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ {
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ {
+ entry: 'INFO test 2.44 on [xxx]',
+ },
+ {
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ ],
+ };
+ expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([
+ {
+ duplicates: 1,
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ {
+ duplicates: 0,
+ entry: 'INFO test 2.44 on [xxx]',
+ },
+ {
+ duplicates: 0,
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ ]);
+ });
+
+ test('should dedup on number matches', () => {
+ const logs = {
+ rows: [
+ {
+ entry: 'WARN test 1.2323423 on [xxx]',
+ },
+ {
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ {
+ entry: 'INFO test 2.44 on [xxx]',
+ },
+ {
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ ],
+ };
+ expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.numbers).rows).toEqual([
+ {
+ duplicates: 1,
+ entry: 'WARN test 1.2323423 on [xxx]',
+ },
+ {
+ duplicates: 0,
+ entry: 'INFO test 2.44 on [xxx]',
+ },
+ {
+ duplicates: 0,
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ ]);
+ });
+
+ test('should dedup on signature matches', () => {
+ const logs = {
+ rows: [
+ {
+ entry: 'WARN test 1.2323423 on [xxx]',
+ },
+ {
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ {
+ entry: 'INFO test 2.44 on [xxx]',
+ },
+ {
+ entry: 'WARN test 1.23 on [xxx]',
+ },
+ ],
+ };
+ expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.signature).rows).toEqual([
+ {
+ duplicates: 3,
+ entry: 'WARN test 1.2323423 on [xxx]',
+ },
+ ]);
+ });
+});
diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts
index 16214679996..13e02b76e30 100644
--- a/public/app/core/utils/colors.ts
+++ b/public/app/core/utils/colors.ts
@@ -7,6 +7,7 @@ export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)';
export const OK_COLOR = 'rgba(11, 237, 50, 1)';
export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)';
export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)';
+export const PENDING_COLOR = 'rgba(247, 149, 32, 1)';
export const REGION_FILL_ALPHA = 0.09;
const colors = [
diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts
index 4252730338d..37d3d3bfac9 100644
--- a/public/app/core/utils/explore.test.ts
+++ b/public/app/core/utils/explore.test.ts
@@ -1,5 +1,13 @@
-import { DEFAULT_RANGE, serializeStateToUrlParam, parseUrlState } from './explore';
+import {
+ DEFAULT_RANGE,
+ serializeStateToUrlParam,
+ parseUrlState,
+ updateHistory,
+ clearHistory,
+ hasNonEmptyQuery,
+} from './explore';
import { ExploreState } from 'app/types/explore';
+import store from 'app/core/store';
const DEFAULT_EXPLORE_STATE: ExploreState = {
datasource: null,
@@ -10,7 +18,7 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
exploreDatasources: [],
graphRange: DEFAULT_RANGE,
history: [],
- queries: [],
+ initialQueries: [],
queryTransactions: [],
range: DEFAULT_RANGE,
showingGraph: true,
@@ -33,10 +41,10 @@ describe('state functions', () => {
it('returns a valid Explore state from URL parameter', () => {
const paramValue =
- '%7B"datasource":"Local","queries":%5B%7B"query":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D';
+ '%7B"datasource":"Local","queries":%5B%7B"expr":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D';
expect(parseUrlState(paramValue)).toMatchObject({
datasource: 'Local',
- queries: [{ query: 'metric' }],
+ queries: [{ expr: 'metric' }],
range: {
from: 'now-1h',
to: 'now',
@@ -45,10 +53,10 @@ describe('state functions', () => {
});
it('returns a valid Explore state from a compact URL parameter', () => {
- const paramValue = '%5B"now-1h","now","Local","metric"%5D';
+ const paramValue = '%5B"now-1h","now","Local",%7B"expr":"metric"%7D%5D';
expect(parseUrlState(paramValue)).toMatchObject({
datasource: 'Local',
- queries: [{ query: 'metric' }],
+ queries: [{ expr: 'metric' }],
range: {
from: 'now-1h',
to: 'now',
@@ -66,18 +74,20 @@ describe('state functions', () => {
from: 'now-5h',
to: 'now',
},
- queries: [
+ initialQueries: [
{
- query: 'metric{test="a/b"}',
+ refId: '1',
+ expr: 'metric{test="a/b"}',
},
{
- query: 'super{foo="x/z"}',
+ refId: '2',
+ expr: 'super{foo="x/z"}',
},
],
};
expect(serializeStateToUrlParam(state)).toBe(
- '{"datasource":"foo","queries":[{"query":"metric{test=\\"a/b\\"}"},' +
- '{"query":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}'
+ '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
+ '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}'
);
});
@@ -89,17 +99,19 @@ describe('state functions', () => {
from: 'now-5h',
to: 'now',
},
- queries: [
+ initialQueries: [
{
- query: 'metric{test="a/b"}',
+ refId: '1',
+ expr: 'metric{test="a/b"}',
},
{
- query: 'super{foo="x/z"}',
+ refId: '2',
+ expr: 'super{foo="x/z"}',
},
],
};
expect(serializeStateToUrlParam(state, true)).toBe(
- '["now-5h","now","foo","metric{test=\\"a/b\\"}","super{foo=\\"x/z\\"}"]'
+ '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
);
});
});
@@ -113,12 +125,14 @@ describe('state functions', () => {
from: 'now - 5h',
to: 'now',
},
- queries: [
+ initialQueries: [
{
- query: 'metric{test="a/b"}',
+ refId: '1',
+ expr: 'metric{test="a/b"}',
},
{
- query: 'super{foo="x/z"}',
+ refId: '2',
+ expr: 'super{foo="x/z"}',
},
],
};
@@ -126,14 +140,50 @@ describe('state functions', () => {
const parsed = parseUrlState(serialized);
// Account for datasource vs datasourceName
- const { datasource, ...rest } = parsed;
- const sameState = {
+ const { datasource, queries, ...rest } = parsed;
+ const resultState = {
...rest,
datasource: DEFAULT_EXPLORE_STATE.datasource,
datasourceName: datasource,
+ initialQueries: queries,
};
- expect(state).toMatchObject(sameState);
+ expect(state).toMatchObject(resultState);
});
});
});
+
+describe('updateHistory()', () => {
+ const datasourceId = 'myDatasource';
+ const key = `grafana.explore.history.${datasourceId}`;
+
+ beforeEach(() => {
+ clearHistory(datasourceId);
+ expect(store.exists(key)).toBeFalsy();
+ });
+
+ test('should save history item to localStorage', () => {
+ const expected = [
+ {
+ query: { refId: '1', expr: 'metric' },
+ },
+ ];
+ expect(updateHistory([], datasourceId, [{ refId: '1', expr: 'metric' }])).toMatchObject(expected);
+ expect(store.exists(key)).toBeTruthy();
+ expect(store.getObject(key)).toMatchObject(expected);
+ });
+});
+
+describe('hasNonEmptyQuery', () => {
+ test('should return true if one query is non-empty', () => {
+ expect(hasNonEmptyQuery([{ refId: '1', key: '2', expr: 'foo' }])).toBeTruthy();
+ });
+
+ test('should return false if query is empty', () => {
+ expect(hasNonEmptyQuery([{ refId: '1', key: '2' }])).toBeFalsy();
+ });
+
+ test('should return false if no queries exist', () => {
+ expect(hasNonEmptyQuery([])).toBeFalsy();
+ });
+});
diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts
index ecd11a495ad..9ecc36a192f 100644
--- a/public/app/core/utils/explore.ts
+++ b/public/app/core/utils/explore.ts
@@ -1,11 +1,20 @@
import { renderUrl } from 'app/core/utils/url';
-import { ExploreState, ExploreUrlState } from 'app/types/explore';
+import { ExploreState, ExploreUrlState, HistoryItem } from 'app/types/explore';
+import { DataQuery, RawTimeRange } from 'app/types/series';
+
+import kbn from 'app/core/utils/kbn';
+import colors from 'app/core/utils/colors';
+import TimeSeries from 'app/core/time_series2';
+import { parse as parseDate } from 'app/core/utils/datemath';
+import store from 'app/core/store';
export const DEFAULT_RANGE = {
from: 'now-6h',
to: 'now',
};
+const MAX_HISTORY_ITEMS = 100;
+
/**
* Returns an Explore-URL that contains a panel's queries and the dashboard time range.
*
@@ -23,7 +32,7 @@ export async function getExploreUrl(
timeSrv: any
) {
let exploreDatasource = panelDatasource;
- let exploreTargets = panelTargets;
+ let exploreTargets: DataQuery[] = panelTargets;
let url;
// Mixed datasources need to choose only one datasource
@@ -57,6 +66,8 @@ export async function getExploreUrl(
return url;
}
+const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
+
export function parseUrlState(initial: string | undefined): ExploreUrlState {
if (initial) {
try {
@@ -70,7 +81,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
to: parsed[1],
};
const datasource = parsed[2];
- const queries = parsed.slice(3).map(query => ({ query }));
+ const queries = parsed.slice(3);
return { datasource, queries, range };
}
return parsed;
@@ -84,16 +95,97 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
const urlState: ExploreUrlState = {
datasource: state.datasourceName,
- queries: state.queries.map(q => ({ query: q.query })),
+ queries: state.initialQueries.map(clearQueryKeys),
range: state.range,
};
if (compact) {
- return JSON.stringify([
- urlState.range.from,
- urlState.range.to,
- urlState.datasource,
- ...urlState.queries.map(q => q.query),
- ]);
+ return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
}
return JSON.stringify(urlState);
}
+
+export function generateKey(index = 0): string {
+ return `Q-${Date.now()}-${Math.random()}-${index}`;
+}
+
+export function generateRefId(index = 0): string {
+ return `${index + 1}`;
+}
+
+export function generateQueryKeys(index = 0): { refId: string; key: string } {
+ return { refId: generateRefId(index), key: generateKey(index) };
+}
+
+/**
+ * Ensure at least one target exists and that targets have the necessary keys
+ */
+export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
+ if (queries && typeof queries === 'object' && queries.length > 0) {
+ return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) }));
+ }
+ return [{ ...generateQueryKeys() }];
+}
+
+/**
+ * A target is non-empty when it has keys other than refId and key.
+ */
+export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
+ return queries.some(query => Object.keys(query).length > 2);
+}
+
+export function getIntervals(
+ range: RawTimeRange,
+ datasource,
+ resolution: number
+): { interval: string; intervalMs: number } {
+ if (!datasource || !resolution) {
+ return { interval: '1s', intervalMs: 1000 };
+ }
+ const absoluteRange: RawTimeRange = {
+ from: parseDate(range.from, false),
+ to: parseDate(range.to, true),
+ };
+ return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
+}
+
+export function makeTimeSeriesList(dataList) {
+ return dataList.map((seriesData, index) => {
+ const datapoints = seriesData.datapoints || [];
+ const alias = seriesData.target;
+ const colorIndex = index % colors.length;
+ const color = colors[colorIndex];
+
+ const series = new TimeSeries({
+ datapoints,
+ alias,
+ color,
+ unit: seriesData.unit,
+ });
+
+ return series;
+ });
+}
+
+/**
+ * Update the query history. Side-effect: store history in local storage
+ */
+export function updateHistory(history: HistoryItem[], datasourceId: string, queries: DataQuery[]): HistoryItem[] {
+ const ts = Date.now();
+ queries.forEach(query => {
+ history = [{ query, ts }, ...history];
+ });
+
+ if (history.length > MAX_HISTORY_ITEMS) {
+ history = history.slice(0, MAX_HISTORY_ITEMS);
+ }
+
+ // Combine all queries of a datasource type into one history
+ const historyKey = `grafana.explore.history.${datasourceId}`;
+ store.setObject(historyKey, history);
+ return history;
+}
+
+export function clearHistory(datasourceId: string) {
+ const historyKey = `grafana.explore.history.${datasourceId}`;
+ store.delete(historyKey);
+}
diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts
index 398ad9bb3e7..5d417d24169 100644
--- a/public/app/core/utils/kbn.ts
+++ b/public/app/core/utils/kbn.ts
@@ -584,8 +584,8 @@ kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms');
kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs');
kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm');
kbn.valueFormats.litreh = kbn.formatBuilders.fixedUnit('l/h');
-kbn.valueFormats.flowlpm = kbn.formatBuilders.decimalSIPrefix('L');
-kbn.valueFormats.flowmlpm = kbn.formatBuilders.decimalSIPrefix('L', -1);
+kbn.valueFormats.flowlpm = kbn.formatBuilders.decimalSIPrefix('l/min');
+kbn.valueFormats.flowmlpm = kbn.formatBuilders.decimalSIPrefix('mL/min', -1);
// Angle
kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°');
diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx
index d25fc659af5..f94134f3ee1 100644
--- a/public/app/features/alerting/AlertRuleList.tsx
+++ b/public/app/features/alerting/AlertRuleList.tsx
@@ -29,6 +29,7 @@ export class AlertRuleList extends PureComponent {
{ text: 'Alerting', value: 'alerting' },
{ text: 'No Data', value: 'no_data' },
{ text: 'Paused', value: 'paused' },
+ { text: 'Pending', value: 'pending' },
];
componentDidMount() {
diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts
index 146b7026353..ef68ddcf4a5 100644
--- a/public/app/features/alerting/AlertTabCtrl.ts
+++ b/public/app/features/alerting/AlertTabCtrl.ts
@@ -169,6 +169,7 @@ export class AlertTabCtrl {
alert.frequency = alert.frequency || '1m';
alert.handler = alert.handler || 1;
alert.notifications = alert.notifications || [];
+ alert.for = alert.for || '0m';
const defaultName = this.panel.title + ' alert';
alert.name = alert.name || defaultName;
@@ -217,7 +218,7 @@ export class AlertTabCtrl {
buildDefaultCondition() {
return {
type: 'query',
- query: { params: ['A', '15m', 'now'] },
+ query: { params: ['A', '5m', 'now'] },
reducer: { type: 'avg', params: [] },
evaluator: { type: 'gt', params: [null] },
operator: { type: 'and' },
@@ -354,6 +355,7 @@ export class AlertTabCtrl {
enable() {
this.panel.alert = {};
this.initModel();
+ this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes
}
evaluatorParamsChanged() {
diff --git a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap
index 4ae27213e1e..b753a852e92 100644
--- a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap
+++ b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap
@@ -81,6 +81,12 @@ exports[`Render should render alert rules 1`] = `
>
Paused
+
+ Pending
+
@@ -230,6 +236,12 @@ exports[`Render should render component 1`] = `
>
Paused
+
+ Pending
+
diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html
index cb101672aa4..2ebe2b53a76 100644
--- a/public/app/features/alerting/partials/alert_tab.html
+++ b/public/app/features/alerting/partials/alert_tab.html
@@ -1,147 +1,159 @@