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 ( @@ -230,6 +236,12 @@ exports[`Render should render component 1`] = ` > Paused + 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 @@
- -
-
-
- {{ctrl.error}} -
+
+
+
+ {{ctrl.error}} +
-
-
Alert Config
-
- Name - - Evaluate every - -
-
+
+
Alert Config
+
+ Name + +
+
+
+ Evaluate every + +
+
+ + + + 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. + +
+
+
-
-
Conditions
-
-
- - WHEN -
-
- - - OF -
-
- - -
-
- - - - -
-
- -
-
+
+
Conditions
+
+
+ + WHEN +
+
+ + + OF +
+
+ + +
+
+ + + + +
+
+ +
+
-
- -
-
+
+ +
+
-
-
- If no data or all values are null - SET STATE TO -
- -
-
+
+
+ If no data or all values are null + SET STATE TO +
+ +
+
-
- If execution error or timeout - SET STATE TO -
- -
-
+
+ If execution error or timeout + SET STATE TO +
+ +
+
-
- -
-
+
+ +
+
-
- Evaluating rule -
+
+ Evaluating rule +
-
- -
-
+
+ +
+
-
-
Notifications
-
-
- Send to - -  {{nc.name}}  - - - -
-
-
- Message - -
-
+
+
Notifications
+
+
+ Send to + +  {{nc.name}}  + + + +
+
+
+ Message + +
+
-
- -
- State history (last 50 state changes) -
+
+ +
+ State history (last 50 state changes) +
-
-
- No state changes recorded -
+
+
+ No state changes recorded +
  1. diff --git a/public/app/features/alerting/state/alertDef.ts b/public/app/features/alerting/state/alertDef.ts index 11d2aafaa7f..8e50697d33c 100644 --- a/public/app/features/alerting/state/alertDef.ts +++ b/public/app/features/alerting/state/alertDef.ts @@ -8,9 +8,9 @@ const alertQueryDef = new QueryPartDef({ { name: 'from', type: 'string', - options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h', '24h', '48h'], + options: ['10s', '1m', '5m', '10m', '15m', '1h', '24h', '48h'], }, - { name: 'to', type: 'string', options: ['now'] }, + { name: 'to', type: 'string', options: ['now', 'now-1m', 'now-5m', 'now-10m', 'now-1h'] }, ], defaultParams: ['#A', '15m', 'now', 'avg'], }); @@ -99,6 +99,13 @@ function getStateDisplayModel(state) { stateClass: 'alert-state-warning', }; } + case 'unknown': { + return { + text: 'UNKNOWN', + iconClass: 'fa fa-question', + stateClass: 'alert-state-paused', + }; + } } throw { message: 'Unknown alert state' }; diff --git a/public/app/features/all.ts b/public/app/features/all.ts index ccdc7d7a820..b1283fdfd3e 100644 --- a/public/app/features/all.ts +++ b/public/app/features/all.ts @@ -10,5 +10,4 @@ import './alerting/NotificationsEditCtrl'; import './alerting/NotificationsListCtrl'; import './manage-dashboards'; import './teams/CreateTeamCtrl'; -import './profile/ProfileCtrl'; -import './profile/ChangePasswordCtrl'; +import './profile/all'; diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index 16c18005204..fbe85856f31 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -32,7 +32,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, if (event.alertId) { const stateModel = alertDef.getStateDisplayModel(event.newState); titleStateClass = stateModel.stateClass; - title = ` ${stateModel.text}`; + title = ` ${stateModel.text}`; text = alertDef.getAlertAnnotationInfo(event); if (event.text) { text = text + '
    ' + event.text; diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index ef74ca193d4..db748e639a1 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -7,6 +7,7 @@ import { OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, + PENDING_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA, } from 'app/core/utils/colors'; @@ -71,6 +72,11 @@ export class EventManager { position: 'BOTTOM', markerSize: 5, }, + $__pending: { + color: PENDING_COLOR, + position: 'BOTTOM', + markerSize: 5, + }, $__editing: { color: DEFAULT_ANNOTATION_COLOR, position: 'BOTTOM', diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts index b1419df7376..d5695a577c5 100644 --- a/public/app/features/dashboard/dashboard_srv.ts +++ b/public/app/features/dashboard/dashboard_srv.ts @@ -77,6 +77,10 @@ export class DashboardSrv { postSave(clone, data) { this.dash.version = data.version; + // important that these happens before location redirect below + this.$rootScope.appEvent('dashboard-saved', this.dash); + this.$rootScope.appEvent('alert-success', ['Dashboard saved']); + const newUrl = locationUtil.stripBaseFromUrl(data.url); const currentPath = this.$location.path(); @@ -84,9 +88,6 @@ export class DashboardSrv { this.$location.url(newUrl).replace(); } - this.$rootScope.appEvent('dashboard-saved', this.dash); - this.$rootScope.appEvent('alert-success', ['Dashboard saved']); - return this.dash; } diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index ed032a118fe..dc8a509f2eb 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -95,7 +95,7 @@ export class PanelModel { setViewMode(fullscreen: boolean, isEditing: boolean) { this.fullscreen = fullscreen; this.isEditing = isEditing; - this.events.emit('panel-size-changed'); + this.events.emit('view-mode-changed'); } updateGridPos(newPos: GridPos) { diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 8805050831e..ff12d26233d 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -126,8 +126,7 @@ export class DashboardViewState { if (!panel.fullscreen) { this.enterFullscreen(panel); - } else { - // already in fullscreen view just update the view mode + } else if (this.dashboard.meta.isEditing !== this.state.edit) { this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit); } } else if (this.fullscreenPanel) { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 753f158fd9f..d4e9b689495 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -3,81 +3,44 @@ import { hot } from 'react-hot-loader'; import Select from 'react-select'; import _ from 'lodash'; -import { ExploreState, ExploreUrlState, HistoryItem, Query, QueryTransaction, ResultType } from 'app/types/explore'; -import { RawTimeRange } from 'app/types/series'; -import kbn from 'app/core/utils/kbn'; -import colors from 'app/core/utils/colors'; +import { DataSource } from 'app/types/datasources'; +import { + ExploreState, + ExploreUrlState, + QueryTransaction, + ResultType, + QueryHintGetter, + QueryHint, +} from 'app/types/explore'; +import { RawTimeRange, DataQuery } from 'app/types/series'; import store from 'app/core/store'; -import TimeSeries from 'app/core/time_series2'; -import { parse as parseDate } from 'app/core/utils/datemath'; -import { DEFAULT_RANGE } from 'app/core/utils/explore'; +import { + DEFAULT_RANGE, + ensureQueries, + getIntervals, + generateKey, + generateQueryKeys, + hasNonEmptyQuery, + makeTimeSeriesList, + updateHistory, +} from 'app/core/utils/explore'; import ResetStyles from 'app/core/components/Picker/ResetStyles'; import PickerOption from 'app/core/components/Picker/PickerOption'; import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer'; import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; +import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import Panel from './Panel'; import QueryRows from './QueryRows'; import Graph from './Graph'; import Logs from './Logs'; import Table from './Table'; import ErrorBoundary from './ErrorBoundary'; import TimePicker from './TimePicker'; -import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; -import { DataSource } from 'app/types/datasources'; - -const MAX_HISTORY_ITEMS = 100; - -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); -} - -function makeTimeSeriesList(dataList, options) { - 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 - */ -function updateHistory(history: HistoryItem[], datasourceId: string, queries: string[]): 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; -} interface ExploreProps { - datasourceSrv: any; + datasourceSrv: DatasourceSrv; onChangeSplit: (split: boolean, state?: ExploreState) => void; onSaveState: (key: string, state: ExploreState) => void; position: string; @@ -87,22 +50,62 @@ interface ExploreProps { urlState: ExploreUrlState; } +/** + * Explore provides an area for quick query iteration for a given datasource. + * Once a datasource is selected it populates the query section at the top. + * When queries are run, their results are being displayed in the main section. + * The datasource determines what kind of query editor it brings, and what kind + * of results viewers it supports. + * + * QUERY HANDLING + * + * TLDR: to not re-render Explore during edits, query editing is not "controlled" + * in a React sense: values need to be pushed down via `initialQueries`, while + * edits travel up via `this.modifiedQueries`. + * + * By default the query rows start without prior state: `initialQueries` will + * contain one empty DataQuery. While the user modifies the DataQuery, the + * modifications are being tracked in `this.modifiedQueries`, which need to be + * used whenever a query is sent to the datasource to reflect what the user sees + * on the screen. Query rows can be initialized or reset using `initialQueries`, + * by giving the respective row a new key. This wipes the old row and its state. + * This property is also used to govern how many query rows there are (minimum 1). + * + * This flow makes sure that a query row can be arbitrarily complex without the + * fear of being wiped or re-initialized via props. The query row is free to keep + * its own state while the user edits or builds a query. Valid queries can be sent + * up to Explore via the `onChangeQuery` prop. + * + * DATASOURCE REQUESTS + * + * A click on Run Query creates transactions for all DataQueries for all expanded + * result viewers. New runs are discarding previous runs. Upon completion a transaction + * saves the result. The result viewers construct their data from the currently existing + * transactions. + * + * The result viewers determine some of the query options sent to the datasource, e.g., + * `format`, to indicate eventual transformations by the datasources' result transformers. + */ export class Explore extends React.PureComponent { el: any; /** * Current query expressions of the rows including their modifications, used for running queries. * Not kept in component state to prevent edit-render roundtrips. */ - queryExpressions: string[]; + modifiedQueries: DataQuery[]; + /** + * Local ID cache to compare requested vs selected datasource + */ + requestedDatasourceId: string; constructor(props) { super(props); const splitState: ExploreState = props.splitState; - let initialQueries: Query[]; + let initialQueries: DataQuery[]; if (splitState) { // Split state overrides everything this.state = splitState; - initialQueries = splitState.queries; + initialQueries = splitState.initialQueries; } else { const { datasource, queries, range } = props.urlState as ExploreUrlState; initialQueries = ensureQueries(queries); @@ -115,19 +118,20 @@ export class Explore extends React.PureComponent { datasourceName: datasource, exploreDatasources: [], graphRange: initialRange, + initialQueries, history: [], - queries: initialQueries, queryTransactions: [], range: initialRange, showingGraph: true, showingLogs: true, + showingStartPage: false, showingTable: true, supportsGraph: null, supportsLogs: null, supportsTable: null, }; } - this.queryExpressions = initialQueries.map(q => q.query); + this.modifiedQueries = initialQueries.slice(); } async componentDidMount() { @@ -160,13 +164,16 @@ export class Explore extends React.PureComponent { } } - async setDatasource(datasource: DataSource) { + async setDatasource(datasource: any, origin?: DataSource) { const supportsGraph = datasource.meta.metrics; const supportsLogs = datasource.meta.logs; const supportsTable = datasource.meta.metrics; const datasourceId = datasource.meta.id; let datasourceError = null; + // Keep ID to track selection + this.requestedDatasourceId = datasourceId; + try { const testResult = await datasource.testDatasource(); datasourceError = testResult.status === 'success' ? null : testResult.message; @@ -174,6 +181,11 @@ export class Explore extends React.PureComponent { datasourceError = (error && error.statusText) || 'Network error'; } + if (datasourceId !== this.requestedDatasourceId) { + // User already changed datasource again, discard results + return; + } + const historyKey = `grafana.explore.history.${datasourceId}`; const history = store.getObject(historyKey, []); @@ -181,12 +193,27 @@ export class Explore extends React.PureComponent { datasource.init(); } - // Keep queries but reset edit state - const nextQueries = this.state.queries.map((q, i) => ({ - ...q, - key: generateQueryKey(i), - query: this.queryExpressions[i], + // Check if queries can be imported from previously selected datasource + let modifiedQueries = this.modifiedQueries; + if (origin) { + if (origin.meta.id === datasource.meta.id) { + // Keep same queries if same type of datasource + modifiedQueries = [...this.modifiedQueries]; + } else if (datasource.importQueries) { + // Datasource-specific importers + modifiedQueries = await datasource.importQueries(this.modifiedQueries, origin.meta); + } else { + // Default is blank queries + modifiedQueries = ensureQueries(); + } + } + + // Reset edit state with new queries + const nextQueries = this.state.initialQueries.map((q, i) => ({ + ...modifiedQueries[i], + ...generateQueryKeys(i), })); + this.modifiedQueries = modifiedQueries; // Custom components const StartPage = datasource.pluginExports.ExploreStartPage; @@ -202,7 +229,8 @@ export class Explore extends React.PureComponent { supportsTable, datasourceLoading: false, datasourceName: datasource.name, - queries: nextQueries, + initialQueries: nextQueries, + showingStartPage: Boolean(StartPage), }, () => { if (datasourceError === null) { @@ -218,16 +246,15 @@ export class Explore extends React.PureComponent { onAddQueryRow = index => { // Local cache - this.queryExpressions[index + 1] = ''; + this.modifiedQueries[index + 1] = { ...generateQueryKeys(index + 1) }; this.setState(state => { - const { queries, queryTransactions } = state; + const { initialQueries, queryTransactions } = state; - // Add row by generating new react key const nextQueries = [ - ...queries.slice(0, index + 1), - { query: '', key: generateQueryKey() }, - ...queries.slice(index + 1), + ...initialQueries.slice(0, index + 1), + { ...this.modifiedQueries[index + 1] }, + ...initialQueries.slice(index + 1), ]; // Ongoing transactions need to update their row indices @@ -241,11 +268,12 @@ export class Explore extends React.PureComponent { return qt; }); - return { queries: nextQueries, queryTransactions: nextQueryTransactions }; + return { initialQueries: nextQueries, queryTransactions: nextQueryTransactions }; }); }; onChangeDatasource = async option => { + const origin = this.state.datasource; this.setState({ datasource: null, datasourceError: null, @@ -254,29 +282,35 @@ export class Explore extends React.PureComponent { }); const datasourceName = option.value; const datasource = await this.props.datasourceSrv.get(datasourceName); - this.setDatasource(datasource); + this.setDatasource(datasource as any, origin); }; - onChangeQuery = (value: string, index: number, override?: boolean) => { + onChangeQuery = (value: DataQuery, index: number, override?: boolean) => { + // Null value means reset + if (value === null) { + value = { ...generateQueryKeys(index) }; + } + // Keep current value in local cache - this.queryExpressions[index] = value; + this.modifiedQueries[index] = value; if (override) { this.setState(state => { - // Replace query row - const { queries, queryTransactions } = state; - const nextQuery: Query = { - key: generateQueryKey(index), - query: value, + // Replace query row by injecting new key + const { initialQueries, queryTransactions } = state; + const query: DataQuery = { + ...value, + ...generateQueryKeys(index), }; - const nextQueries = [...queries]; - nextQueries[index] = nextQuery; + const nextQueries = [...initialQueries]; + nextQueries[index] = query; + this.modifiedQueries = [...nextQueries]; // Discard ongoing transaction related to row query const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); return { - queries: nextQueries, + initialQueries: nextQueries, queryTransactions: nextQueryTransactions, }; }, this.onSubmit); @@ -291,12 +325,13 @@ export class Explore extends React.PureComponent { }; onClickClear = () => { - this.queryExpressions = ['']; + this.modifiedQueries = ensureQueries(); this.setState( - { - queries: ensureQueries(), + prevState => ({ + initialQueries: [...this.modifiedQueries], queryTransactions: [], - }, + showingStartPage: Boolean(prevState.StartPage), + }), this.saveState ); }; @@ -347,10 +382,10 @@ export class Explore extends React.PureComponent { }; // Use this in help pages to set page to a single query - onClickQuery = query => { - const nextQueries = [{ query, key: generateQueryKey() }]; - this.queryExpressions = nextQueries.map(q => q.query); - this.setState({ queries: nextQueries }, this.onSubmit); + onClickExample = (query: DataQuery) => { + const nextQueries = [{ ...query, ...generateQueryKeys() }]; + this.modifiedQueries = [...nextQueries]; + this.setState({ initialQueries: nextQueries }, this.onSubmit); }; onClickSplit = () => { @@ -390,28 +425,28 @@ export class Explore extends React.PureComponent { const preventSubmit = action.preventSubmit; this.setState( state => { - const { queries, queryTransactions } = state; - let nextQueries; + const { initialQueries, queryTransactions } = state; + let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { // Modify all queries - nextQueries = queries.map((q, i) => ({ - key: generateQueryKey(i), - query: datasource.modifyQuery(this.queryExpressions[i], action), + nextQueries = initialQueries.map((query, i) => ({ + ...datasource.modifyQuery(this.modifiedQueries[i], action), + ...generateQueryKeys(i), })); // Discard all ongoing transactions nextQueryTransactions = []; } else { // Modify query only at index - nextQueries = queries.map((q, i) => { + nextQueries = initialQueries.map((query, i) => { // Synchronise all queries with local query cache to ensure consistency - q.query = this.queryExpressions[i]; + // TODO still needed? return i === index ? { - key: generateQueryKey(index), - query: datasource.modifyQuery(q.query, action), + ...datasource.modifyQuery(this.modifiedQueries[i], action), + ...generateQueryKeys(i), } - : q; + : query; }); nextQueryTransactions = queryTransactions // Consume the hint corresponding to the action @@ -424,9 +459,9 @@ export class Explore extends React.PureComponent { // Preserve previous row query transaction to keep results visible if next query is incomplete .filter(qt => preventSubmit || qt.rowIndex !== index); } - this.queryExpressions = nextQueries.map(q => q.query); + this.modifiedQueries = [...nextQueries]; return { - queries: nextQueries, + initialQueries: nextQueries, queryTransactions: nextQueryTransactions, }; }, @@ -438,22 +473,22 @@ export class Explore extends React.PureComponent { onRemoveQueryRow = index => { // Remove from local cache - this.queryExpressions = [...this.queryExpressions.slice(0, index), ...this.queryExpressions.slice(index + 1)]; + this.modifiedQueries = [...this.modifiedQueries.slice(0, index), ...this.modifiedQueries.slice(index + 1)]; this.setState( state => { - const { queries, queryTransactions } = state; - if (queries.length <= 1) { + const { initialQueries, queryTransactions } = state; + if (initialQueries.length <= 1) { return null; } // Remove row from react state - const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; + const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; // Discard transactions related to row query const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); return { - queries: nextQueries, + initialQueries: nextQueries, queryTransactions: nextQueryTransactions, }; }, @@ -463,52 +498,68 @@ export class Explore extends React.PureComponent { onSubmit = () => { const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state; + // Keep table queries first since they need to return quickly if (showingTable && supportsTable) { - this.runTableQuery(); + this.runQueries( + 'Table', + { + format: 'table', + instant: true, + valueWithRefId: true, + }, + data => data[0] + ); } if (showingGraph && supportsGraph) { - this.runGraphQueries(); + this.runQueries( + 'Graph', + { + format: 'time_series', + instant: false, + }, + makeTimeSeriesList + ); } if (showingLogs && supportsLogs) { - this.runLogsQuery(); + this.runQueries('Logs', { format: 'logs' }); } this.saveState(); }; - buildQueryOptions( - query: string, - rowIndex: number, - targetOptions: { format: string; hinting?: boolean; instant?: boolean } - ) { + buildQueryOptions(query: DataQuery, queryOptions: { format: string; hinting?: boolean; instant?: boolean }) { const { datasource, range } = this.state; const { interval, intervalMs } = getIntervals(range, datasource, this.el.offsetWidth); - const targets = [ + + const configuredQueries = [ { - ...targetOptions, - // Target identifier is needed for table transformations - refId: rowIndex + 1, - expr: query, + ...queryOptions, + ...query, }, ]; // Clone range for query request const queryRange: RawTimeRange = { ...range }; + // Datasource is using `panelId + query.refId` for cancellation logic. + // Using `format` here because it relates to the view panel that the request is for. + const panelId = queryOptions.format; + return { interval, intervalMs, - targets, + panelId, + targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key. range: queryRange, }; } - startQueryTransaction(query: string, rowIndex: number, resultType: ResultType, options: any): QueryTransaction { - const queryOptions = this.buildQueryOptions(query, rowIndex, options); + startQueryTransaction(query: DataQuery, rowIndex: number, resultType: ResultType, options: any): QueryTransaction { + const queryOptions = this.buildQueryOptions(query, options); const transaction: QueryTransaction = { query, resultType, rowIndex, - id: generateQueryKey(), + id: generateKey(), // reusing for unique ID done: false, latency: 0, options: queryOptions, @@ -527,6 +578,7 @@ export class Explore extends React.PureComponent { return { queryTransactions: nextQueryTransactions, + showingStartPage: false, }; }); @@ -537,7 +589,7 @@ export class Explore extends React.PureComponent { transactionId: string, result: any, latency: number, - queries: string[], + queries: DataQuery[], datasourceId: string ) { const { datasource } = this.state; @@ -556,8 +608,8 @@ export class Explore extends React.PureComponent { } // Get query hints - let hints; - if (datasource.getQueryHints) { + let hints: QueryHint[]; + if (datasource.getQueryHints as QueryHintGetter) { hints = datasource.getQueryHints(transaction.query, result); } @@ -593,7 +645,7 @@ export class Explore extends React.PureComponent { failQueryTransaction(transactionId: string, response: any, datasourceId: string) { const { datasource } = this.state; - if (datasource.meta.id !== datasourceId) { + if (datasource.meta.id !== datasourceId || response.cancelled) { // Navigated away, queries did not matter return; } @@ -637,88 +689,25 @@ export class Explore extends React.PureComponent { }); } - async runGraphQueries() { - const queries = [...this.queryExpressions]; - if (!hasQuery(queries)) { + async runQueries(resultType: ResultType, queryOptions: any, resultGetter?: any) { + const queries = [...this.modifiedQueries]; + if (!hasNonEmptyQuery(queries)) { return; } const { datasource } = this.state; const datasourceId = datasource.meta.id; // Run all queries concurrently queries.forEach(async (query, rowIndex) => { - if (query) { - const transaction = this.startQueryTransaction(query, rowIndex, 'Graph', { - format: 'time_series', - instant: false, - }); - try { - const now = Date.now(); - const res = await datasource.query(transaction.options); - const latency = Date.now() - now; - const results = makeTimeSeriesList(res.data, transaction.options); - this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); - this.setState({ graphRange: transaction.options.range }); - } catch (response) { - this.failQueryTransaction(transaction.id, response, datasourceId); - } - } else { - this.discardTransactions(rowIndex); - } - }); - } - - async runTableQuery() { - const queries = [...this.queryExpressions]; - if (!hasQuery(queries)) { - return; - } - const { datasource } = this.state; - const datasourceId = datasource.meta.id; - // Run all queries concurrently - queries.forEach(async (query, rowIndex) => { - if (query) { - const transaction = this.startQueryTransaction(query, rowIndex, 'Table', { - format: 'table', - instant: true, - valueWithRefId: true, - }); - try { - const now = Date.now(); - const res = await datasource.query(transaction.options); - const latency = Date.now() - now; - const results = res.data[0]; - this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); - } catch (response) { - this.failQueryTransaction(transaction.id, response, datasourceId); - } - } else { - this.discardTransactions(rowIndex); - } - }); - } - - async runLogsQuery() { - const queries = [...this.queryExpressions]; - if (!hasQuery(queries)) { - return; - } - const { datasource } = this.state; - const datasourceId = datasource.meta.id; - // Run all queries concurrently - queries.forEach(async (query, rowIndex) => { - if (query) { - const transaction = this.startQueryTransaction(query, rowIndex, 'Logs', { format: 'logs' }); - try { - const now = Date.now(); - const res = await datasource.query(transaction.options); - const latency = Date.now() - now; - const results = res.data; - this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); - } catch (response) { - this.failQueryTransaction(transaction.id, response, datasourceId); - } - } else { - this.discardTransactions(rowIndex); + const transaction = this.startQueryTransaction(query, rowIndex, resultType, queryOptions); + try { + const now = Date.now(); + const res = await datasource.query(transaction.options); + const latency = Date.now() - now; + const results = resultGetter ? resultGetter(res.data) : res.data; + this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); + this.setState({ graphRange: transaction.options.range }); + } catch (response) { + this.failQueryTransaction(transaction.id, response, datasourceId); } }); } @@ -728,7 +717,7 @@ export class Explore extends React.PureComponent { return { ...this.state, queryTransactions: [], - queries: ensureQueries(this.queryExpressions.map(query => ({ query }))), + initialQueries: [...this.modifiedQueries], }; } @@ -748,21 +737,18 @@ export class Explore extends React.PureComponent { exploreDatasources, graphRange, history, - queries, + initialQueries, queryTransactions, range, showingGraph, showingLogs, + showingStartPage, showingTable, supportsGraph, supportsLogs, supportsTable, } = this.state; - const showingBoth = showingGraph && showingTable; - const graphHeight = showingBoth ? '200px' : '400px'; - const graphButtonActive = showingBoth || showingGraph ? 'active' : ''; - const logsButtonActive = showingLogs ? 'active' : ''; - const tableButtonActive = showingBoth || showingTable ? 'active' : ''; + const graphHeight = showingGraph && showingTable ? '200px' : '400px'; const exploreClass = split ? 'explore explore-split' : 'explore'; const selectedDatasource = datasource ? exploreDatasources.find(d => d.label === datasource.name) : undefined; const graphRangeIntervals = getIntervals(graphRange, datasource, this.el ? this.el.offsetWidth : 0); @@ -787,8 +773,6 @@ export class Explore extends React.PureComponent { ) : undefined; const loading = queryTransactions.some(qt => !qt.done); - const showStartPages = StartPage && queryTransactions.length === 0; - const viewModeCount = [supportsGraph, supportsLogs, supportsTable].filter(m => m).length; return (
    @@ -867,7 +851,7 @@ export class Explore extends React.PureComponent { { />
    - {showStartPages && } - {!showStartPages && ( + {showingStartPage && } + {!showingStartPage && ( <> - {viewModeCount > 1 && ( -
    - {supportsGraph ? ( - - ) : null} - {supportsTable ? ( - - ) : null} - {supportsLogs ? ( - - ) : null} -
    - )} - - {supportsGraph && - showingGraph && ( + {supportsGraph && ( + - )} - {supportsTable && showingTable ? ( -
    + + )} + {supportsTable && ( + - - ) : null} - {supportsLogs && showingLogs ? ( - - ) : null} + + )} + {supportsLogs && ( + + + + )} )} diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index 9e4fea0d3de..10f3faa1267 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -13,6 +13,7 @@ import * as dateMath from 'app/core/utils/datemath'; import TimeSeries from 'app/core/time_series2'; import Legend from './Legend'; +import { equal, intersect } from './utils/set'; const MAX_NUMBER_OF_TIME_SERIES = 20; @@ -77,22 +78,29 @@ interface GraphProps { data: any[]; height?: string; // e.g., '200px' id?: string; - loading?: boolean; range: RawTimeRange; split?: boolean; size?: { width: number; height: number }; userOptions?: any; onChangeTime?: (range: RawTimeRange) => void; + onToggleSeries?: (alias: string, hiddenSeries: Set) => void; } interface GraphState { + /** + * Type parameter refers to the `alias` property of a `TimeSeries`. + * Consequently, all series sharing the same alias will share visibility state. + */ + hiddenSeries: Set; showAllTimeSeries: boolean; } export class Graph extends PureComponent { $el: any; + dynamicOptions = null; state = { + hiddenSeries: new Set(), showAllTimeSeries: false, }; @@ -108,13 +116,14 @@ export class Graph extends PureComponent { this.$el.bind('plotselected', this.onPlotSelected); } - componentDidUpdate(prevProps: GraphProps) { + componentDidUpdate(prevProps: GraphProps, prevState: GraphState) { if ( prevProps.data !== this.props.data || prevProps.range !== this.props.range || prevProps.split !== this.props.split || prevProps.height !== this.props.height || - (prevProps.size && prevProps.size.width !== this.props.size.width) + (prevProps.size && prevProps.size.width !== this.props.size.width) || + !equal(prevState.hiddenSeries, this.state.hiddenSeries) ) { this.draw(); } @@ -134,30 +143,8 @@ export class Graph extends PureComponent { } }; - onShowAllTimeSeries = () => { - this.setState( - { - showAllTimeSeries: true, - }, - this.draw - ); - }; - - draw() { - const { range, size, userOptions = {} } = this.props; - const data = this.getGraphData(); - - const $el = $(`#${this.props.id}`); - let series = [{ data: [[0, 0]] }]; - - if (data && data.length > 0) { - series = data.map((ts: TimeSeries) => ({ - color: ts.color, - label: ts.label, - data: ts.getFlotPairs('null'), - })); - } - + getDynamicOptions() { + const { range, size } = this.props; const ticks = (size.width || 0) / 100; let { from, to } = range; if (!moment.isMoment(from)) { @@ -168,7 +155,7 @@ export class Graph extends PureComponent { } const min = from.valueOf(); const max = to.valueOf(); - const dynamicOptions = { + return { xaxis: { mode: 'time', min: min, @@ -179,21 +166,83 @@ export class Graph extends PureComponent { timeformat: time_format(ticks, min, max), }, }; + } + + onShowAllTimeSeries = () => { + this.setState( + { + showAllTimeSeries: true, + }, + this.draw + ); + }; + + onToggleSeries = (series: TimeSeries, exclusive: boolean) => { + this.setState((state, props) => { + const { data, onToggleSeries } = props; + const { hiddenSeries } = state; + + // Deduplicate series as visibility tracks the alias property + const oneSeriesVisible = hiddenSeries.size === new Set(data.map(d => d.alias)).size - 1; + + let nextHiddenSeries = new Set(); + if (exclusive) { + if (hiddenSeries.has(series.alias) || !oneSeriesVisible) { + nextHiddenSeries = new Set(data.filter(d => d.alias !== series.alias).map(d => d.alias)); + } + } else { + // Prune hidden series no longer part of those available from the most recent query + const availableSeries = new Set(data.map(d => d.alias)); + nextHiddenSeries = intersect(new Set(hiddenSeries), availableSeries); + if (nextHiddenSeries.has(series.alias)) { + nextHiddenSeries.delete(series.alias); + } else { + nextHiddenSeries.add(series.alias); + } + } + if (onToggleSeries) { + onToggleSeries(series.alias, nextHiddenSeries); + } + return { + hiddenSeries: nextHiddenSeries, + }; + }, this.draw); + }; + + draw() { + const { userOptions = {} } = this.props; + const { hiddenSeries } = this.state; + const data = this.getGraphData(); + + const $el = $(`#${this.props.id}`); + let series = [{ data: [[0, 0]] }]; + + if (data && data.length > 0) { + series = data.filter((ts: TimeSeries) => !hiddenSeries.has(ts.alias)).map((ts: TimeSeries) => ({ + color: ts.color, + label: ts.label, + data: ts.getFlotPairs('null'), + })); + } + + this.dynamicOptions = this.getDynamicOptions(); + const options = { ...FLOT_OPTIONS, - ...dynamicOptions, + ...this.dynamicOptions, ...userOptions, }; + $.plot($el, series, options); } render() { - const { height = '100px', id = 'graph', loading = false } = this.props; + const { height = '100px', id = 'graph' } = this.props; + const { hiddenSeries } = this.state; const data = this.getGraphData(); return ( -
    - {loading &&
    } + <> {this.props.data && this.props.data.length > MAX_NUMBER_OF_TIME_SERIES && !this.state.showAllTimeSeries && ( @@ -206,8 +255,8 @@ export class Graph extends PureComponent {
    )}
    - -
    + + ); } } diff --git a/public/app/features/explore/Legend.tsx b/public/app/features/explore/Legend.tsx index 439b6c3e54f..3b67aa74d91 100644 --- a/public/app/features/explore/Legend.tsx +++ b/public/app/features/explore/Legend.tsx @@ -1,23 +1,65 @@ -import React, { PureComponent } from 'react'; +import React, { MouseEvent, PureComponent } from 'react'; +import classNames from 'classnames'; +import { TimeSeries } from 'app/core/core'; -const LegendItem = ({ series }) => ( - -); +interface LegendProps { + data: TimeSeries[]; + hiddenSeries: Set; + onToggleSeries?: (series: TimeSeries, exclusive: boolean) => void; +} + +interface LegendItemProps { + hidden: boolean; + onClickLabel?: (series: TimeSeries, event: MouseEvent) => void; + series: TimeSeries; +} + +class LegendItem extends PureComponent { + onClickLabel = e => this.props.onClickLabel(this.props.series, e); -export default class Legend extends PureComponent { render() { - const { className = '', data } = this.props; - const items = data || []; + const { hidden, series } = this.props; + const seriesClasses = classNames({ + 'graph-legend-series-hidden': hidden, + }); return ( -
    - {items.map(series => )} + + ); + } +} + +export default class Legend extends PureComponent { + static defaultProps = { + onToggleSeries: () => {}, + }; + + onClickLabel = (series: TimeSeries, event: MouseEvent) => { + const { onToggleSeries } = this.props; + const exclusive = event.ctrlKey || event.metaKey || event.shiftKey; + onToggleSeries(series, !exclusive); + }; + + render() { + const { data, hiddenSeries } = this.props; + const items = data || []; + return ( +
    + {items.map((series, i) => ( +
    ); } diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index edde5acba92..9d875458024 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -2,7 +2,7 @@ import React, { Fragment, PureComponent } from 'react'; import Highlighter from 'react-highlight-words'; import { RawTimeRange } from 'app/types/series'; -import { LogsModel } from 'app/core/logs_model'; +import { LogsDedupStrategy, LogsModel, dedupLogRows, filterLogLevels, LogLevel } from 'app/core/logs_model'; import { findHighlightChunksInText } from 'app/core/utils/text'; import { Switch } from 'app/core/components/Switch/Switch'; @@ -32,6 +32,8 @@ interface LogsProps { } interface LogsState { + dedup: LogsDedupStrategy; + hiddenLogLevels: Set; showLabels: boolean; showLocalTime: boolean; showUtc: boolean; @@ -39,11 +41,22 @@ interface LogsState { export default class Logs extends PureComponent { state = { + dedup: LogsDedupStrategy.none, + hiddenLogLevels: new Set(), showLabels: true, showLocalTime: true, showUtc: false, }; + onChangeDedup = (dedup: LogsDedupStrategy) => { + this.setState(prevState => { + if (prevState.dedup === dedup) { + return { dedup: LogsDedupStrategy.none }; + } + return { dedup }; + }); + }; + onChangeLabels = (event: React.SyntheticEvent) => { const target = event.target as HTMLInputElement; this.setState({ @@ -65,11 +78,26 @@ export default class Logs extends PureComponent { }); }; + onToggleLogLevel = (rawLevel: string, hiddenRawLevels: Set) => { + const hiddenLogLevels: Set = new Set(Array.from(hiddenRawLevels).map(level => LogLevel[level])); + this.setState({ hiddenLogLevels }); + }; + render() { const { className = '', data, loading = false, position, range } = this.props; - const { showLabels, showLocalTime, showUtc } = this.state; + const { dedup, hiddenLogLevels, showLabels, showLocalTime, showUtc } = this.state; const hasData = data && data.rows && data.rows.length > 0; - const cssColumnSizes = ['4px']; + const filteredData = filterLogLevels(data, hiddenLogLevels); + const dedupedData = dedupLogRows(filteredData, dedup); + const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0); + const meta = [...data.meta]; + if (dedup !== LogsDedupStrategy.none) { + meta.push({ + label: 'Dedup count', + value: String(dedupCount), + }); + } + const cssColumnSizes = ['3px']; // Log-level indicator line if (showUtc) { cssColumnSizes.push('minmax(100px, max-content)'); } @@ -93,19 +121,44 @@ export default class Logs extends PureComponent { range={range} id={`explore-logs-graph-${position}`} onChangeTime={this.props.onChangeTime} + onToggleSeries={this.onToggleLogLevel} userOptions={graphOptions} />
    -
    +
    + this.onChangeDedup(LogsDedupStrategy.none)} + small + /> + this.onChangeDedup(LogsDedupStrategy.exact)} + small + /> + this.onChangeDedup(LogsDedupStrategy.numbers)} + small + /> + this.onChangeDedup(LogsDedupStrategy.signature)} + small + /> {hasData && - data.meta && ( + meta && (
    - {data.meta.map(item => ( + {meta.map(item => (
    {item.label}: {item.value} @@ -116,33 +169,38 @@ export default class Logs extends PureComponent {
    -
    - {loading &&
    } -
    - {hasData && - data.rows.map(row => ( - -
    - {showUtc &&
    {row.timestamp}
    } - {showLocalTime &&
    {row.timeLocal}
    } - {showLabels && ( -
    - {row.labels} +
    + {hasData && + dedupedData.rows.map(row => ( + +
    + {row.duplicates > 0 && ( +
    + {Array.apply(null, { length: row.duplicates }).map((bogus, index) => ( +
    + ))}
    )} -
    - +
    + {showUtc &&
    {row.timestamp}
    } + {showLocalTime &&
    {row.timeLocal}
    } + {showLabels && ( +
    + {row.labels}
    - - ))} -
    - {!loading && !hasData && 'No data was returned.'} + )} +
    + +
    + + ))}
    + {!loading && !hasData && 'No data was returned.'}
    ); } diff --git a/public/app/features/explore/Panel.tsx b/public/app/features/explore/Panel.tsx new file mode 100644 index 00000000000..dc75cb0ecca --- /dev/null +++ b/public/app/features/explore/Panel.tsx @@ -0,0 +1,34 @@ +import React, { PureComponent } from 'react'; + +interface Props { + isOpen: boolean; + label: string; + loading?: boolean; + onToggle: (isOpen: boolean) => void; +} + +export default class Panel extends PureComponent { + onClickToggle = () => this.props.onToggle(!this.props.isOpen); + + render() { + const { isOpen, loading } = this.props; + const iconClass = isOpen ? 'fa fa-caret-up' : 'fa fa-caret-down'; + const loaderClass = loading ? 'explore-panel__loader explore-panel__loader--active' : 'explore-panel__loader'; + return ( +
    +
    +
    + +
    +
    {this.props.label}
    +
    + {isOpen && ( +
    +
    + {this.props.children} +
    + )} +
    + ); + } +} diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 224d34574b8..d5cba981951 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -27,14 +27,14 @@ function hasSuggestions(suggestions: CompletionItemGroup[]): boolean { return suggestions && suggestions.length > 0; } -interface QueryFieldProps { +export interface QueryFieldProps { additionalPlugins?: any[]; cleanText?: (text: string) => string; - initialValue: string | null; + initialQuery: string | null; onBlur?: () => void; onFocus?: () => void; onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput; - onValueChanged?: (value: Value) => void; + onValueChanged?: (value: string) => void; onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string; placeholder?: string; portalOrigin?: string; @@ -60,16 +60,22 @@ export interface TypeaheadInput { wrapperNode: Element; } +/** + * Renders an editor field. + * Pass initial value as initialQuery and listen to changes in props.onValueChanged. + * This component can only process strings. Internally it uses Slate Value. + * Implement props.onTypeahead to use suggestions, see PromQueryField.tsx as an example. + */ export class QueryField extends React.PureComponent { menuEl: HTMLElement | null; placeholdersBuffer: PlaceholdersBuffer; plugins: any[]; resetTimer: any; - constructor(props, context) { + constructor(props: QueryFieldProps, context) { super(props, context); - this.placeholdersBuffer = new PlaceholdersBuffer(props.initialValue || ''); + this.placeholdersBuffer = new PlaceholdersBuffer(props.initialQuery || ''); // Base plugins this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins].filter(p => p); @@ -92,7 +98,7 @@ export class QueryField extends React.PureComponent qt.hints && qt.hints.length > 0); @@ -16,7 +16,7 @@ function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHi interface QueryRowEventHandlers { onAddQueryRow: (index: number) => void; - onChangeQuery: (value: string, index: number, override?: boolean) => void; + onChangeQuery: (value: DataQuery, index: number, override?: boolean) => void; onClickHintFix: (action: object, index?: number) => void; onExecuteQuery: () => void; onRemoveQueryRow: (index: number) => void; @@ -32,11 +32,11 @@ interface QueryRowCommonProps { type QueryRowProps = QueryRowCommonProps & QueryRowEventHandlers & { index: number; - query: string; + initialQuery: DataQuery; }; class QueryRow extends PureComponent { - onChangeQuery = (value, override?: boolean) => { + onChangeQuery = (value: DataQuery, override?: boolean) => { const { index, onChangeQuery } = this.props; if (onChangeQuery) { onChangeQuery(value, index, override); @@ -51,7 +51,7 @@ class QueryRow extends PureComponent { }; onClickClearButton = () => { - this.onChangeQuery('', true); + this.onChangeQuery(null, true); }; onClickHintFix = action => { @@ -76,7 +76,7 @@ class QueryRow extends PureComponent { }; render() { - const { datasource, history, query, transactions } = this.props; + const { datasource, history, initialQuery, transactions } = this.props; const transactionWithError = transactions.find(t => t.error !== undefined); const hint = getFirstHintFromTransactions(transactions); const queryError = transactionWithError ? transactionWithError.error : null; @@ -91,7 +91,7 @@ class QueryRow extends PureComponent { datasource={datasource} error={queryError} hint={hint} - initialQuery={query} + initialQuery={initialQuery} history={history} onClickHintFix={this.onClickHintFix} onPressEnter={this.onPressEnter} @@ -116,19 +116,19 @@ class QueryRow extends PureComponent { type QueryRowsProps = QueryRowCommonProps & QueryRowEventHandlers & { - queries: Query[]; + initialQueries: DataQuery[]; }; export default class QueryRows extends PureComponent { render() { - const { className = '', queries, transactions, ...handlers } = this.props; + const { className = '', initialQueries, transactions, ...handlers } = this.props; return (
    - {queries.map((q, index) => ( + {initialQueries.map((query, index) => ( t.rowIndex === index)} {...handlers} /> diff --git a/public/app/features/explore/QueryTransactionStatus.tsx b/public/app/features/explore/QueryTransactionStatus.tsx index 77a50b7d2ca..6f47f147645 100644 --- a/public/app/features/explore/QueryTransactionStatus.tsx +++ b/public/app/features/explore/QueryTransactionStatus.tsx @@ -35,7 +35,9 @@ export default class QueryTransactionStatus extends PureComponent - {transactions.map((t, i) => )} + {transactions.map((t, i) => ( + + ))}
    ); } diff --git a/public/app/features/explore/Typeahead.tsx b/public/app/features/explore/Typeahead.tsx index 13882e030f6..721527fbebe 100644 --- a/public/app/features/explore/Typeahead.tsx +++ b/public/app/features/explore/Typeahead.tsx @@ -42,7 +42,7 @@ class TypeaheadItem extends React.PureComponent { render() { const { isSelected, item, prefix } = this.props; const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item'; - const { label } = item; + const label = item.label || ''; return (
  2. diff --git a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap index fd2010a76d3..a7ec6deb22c 100644 --- a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render component 1`] = ` -
    +
    -
    +
    `; exports[`Render should render component with disclaimer 1`] = ` -
    +
    @@ -951,14 +949,14 @@ exports[`Render should render component with disclaimer 1`] = ` }, ] } + hiddenSeries={Set {}} + onToggleSeries={[Function]} /> -
    +
    `; exports[`Render should show query return no time series 1`] = ` -
    +
    -
    +
    `; diff --git a/public/app/features/explore/utils/query.ts b/public/app/features/explore/utils/query.ts deleted file mode 100644 index 193ee2dbc52..00000000000 --- a/public/app/features/explore/utils/query.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Query } from 'app/types/explore'; - -export function generateQueryKey(index = 0): string { - return `Q-${Date.now()}-${Math.random()}-${index}`; -} - -export function ensureQueries(queries?: Query[]): Query[] { - if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0].query === 'string') { - return queries.map(({ query }, i) => ({ key: generateQueryKey(i), query })); - } - return [{ key: generateQueryKey(), query: '' }]; -} - -export function hasQuery(queries: string[]): boolean { - return queries.some(q => Boolean(q)); -} diff --git a/public/app/features/explore/utils/set.test.ts b/public/app/features/explore/utils/set.test.ts new file mode 100644 index 00000000000..4f586814fc7 --- /dev/null +++ b/public/app/features/explore/utils/set.test.ts @@ -0,0 +1,52 @@ +import { equal, intersect } from './set'; + +describe('equal', () => { + it('returns false for two sets of differing sizes', () => { + const s1 = new Set([1, 2, 3]); + const s2 = new Set([4, 5, 6, 7]); + expect(equal(s1, s2)).toBe(false); + }); + it('returns false for two sets where one is a subset of the other', () => { + const s1 = new Set([1, 2, 3]); + const s2 = new Set([1, 2, 3, 4]); + expect(equal(s1, s2)).toBe(false); + }); + it('returns false for two sets with uncommon elements', () => { + const s1 = new Set([1, 2, 3, 4]); + const s2 = new Set([1, 2, 5, 6]); + expect(equal(s1, s2)).toBe(false); + }); + it('returns false for two deeply equivalent sets', () => { + const s1 = new Set([{ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }]); + const s2 = new Set([{ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }]); + expect(equal(s1, s2)).toBe(false); + }); + it('returns true for two sets with the same elements', () => { + const s1 = new Set([1, 2, 3, 4]); + const s2 = new Set([4, 3, 2, 1]); + expect(equal(s1, s2)).toBe(true); + }); +}); + +describe('intersect', () => { + it('returns an empty set for two sets without any common elements', () => { + const s1 = new Set([1, 2, 3, 4]); + const s2 = new Set([5, 6, 7, 8]); + expect(intersect(s1, s2)).toEqual(new Set()); + }); + it('returns an empty set for two deeply equivalent sets', () => { + const s1 = new Set([{ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }]); + const s2 = new Set([{ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }]); + expect(intersect(s1, s2)).toEqual(new Set()); + }); + it('returns a set containing common elements between two sets of the same size', () => { + const s1 = new Set([1, 2, 3, 4]); + const s2 = new Set([5, 2, 7, 4]); + expect(intersect(s1, s2)).toEqual(new Set([2, 4])); + }); + it('returns a set containing common elements between two sets of differing sizes', () => { + const s1 = new Set([1, 2, 3, 4]); + const s2 = new Set([5, 4, 3, 2, 1]); + expect(intersect(s1, s2)).toEqual(new Set([1, 2, 3, 4])); + }); +}); diff --git a/public/app/features/explore/utils/set.ts b/public/app/features/explore/utils/set.ts new file mode 100644 index 00000000000..12430384fe4 --- /dev/null +++ b/public/app/features/explore/utils/set.ts @@ -0,0 +1,35 @@ +/** + * Performs a shallow comparison of two sets with the same item type. + */ +export function equal(a: Set, b: Set): boolean { + if (a.size !== b.size) { + return false; + } + const it = a.values(); + while (true) { + const { value, done } = it.next(); + if (done) { + return true; + } + if (!b.has(value)) { + return false; + } + } +} + +/** + * Returns a new set with items in both sets using shallow comparison. + */ +export function intersect(a: Set, b: Set): Set { + const result = new Set(); + const it = b.values(); + while (true) { + const { value, done } = it.next(); + if (done) { + return result; + } + if (a.has(value)) { + result.add(value); + } + } +} diff --git a/public/app/features/org/OrgDetailsPage.test.tsx b/public/app/features/org/OrgDetailsPage.test.tsx index 2eb45fa368a..ea0e5c09388 100644 --- a/public/app/features/org/OrgDetailsPage.test.tsx +++ b/public/app/features/org/OrgDetailsPage.test.tsx @@ -1,16 +1,13 @@ import React from 'react'; import { shallow } from 'enzyme'; import { OrgDetailsPage, Props } from './OrgDetailsPage'; -import { NavModel, Organization, OrganizationPreferences } from '../../types'; +import { NavModel, Organization } from '../../types'; const setup = (propOverrides?: object) => { const props: Props = { - preferences: {} as OrganizationPreferences, organization: {} as Organization, navModel: {} as NavModel, loadOrganization: jest.fn(), - loadOrganizationPreferences: jest.fn(), - loadStarredDashboards: jest.fn(), setOrganizationName: jest.fn(), updateOrganization: jest.fn(), }; diff --git a/public/app/features/org/OrgDetailsPage.tsx b/public/app/features/org/OrgDetailsPage.tsx index 86a3fab3268..b011901aa71 100644 --- a/public/app/features/org/OrgDetailsPage.tsx +++ b/public/app/features/org/OrgDetailsPage.tsx @@ -4,33 +4,22 @@ import { connect } from 'react-redux'; import PageHeader from '../../core/components/PageHeader/PageHeader'; import PageLoader from '../../core/components/PageLoader/PageLoader'; import OrgProfile from './OrgProfile'; -import OrgPreferences from './OrgPreferences'; -import { - loadOrganization, - loadOrganizationPreferences, - setOrganizationName, - updateOrganization, -} from './state/actions'; -import { loadStarredDashboards } from '../../core/actions/user'; -import { NavModel, Organization, OrganizationPreferences, StoreState } from 'app/types'; +import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences'; +import { loadOrganization, setOrganizationName, updateOrganization } from './state/actions'; +import { NavModel, Organization, StoreState } from 'app/types'; import { getNavModel } from '../../core/selectors/navModel'; export interface Props { navModel: NavModel; organization: Organization; - preferences: OrganizationPreferences; loadOrganization: typeof loadOrganization; - loadOrganizationPreferences: typeof loadOrganizationPreferences; - loadStarredDashboards: typeof loadStarredDashboards; setOrganizationName: typeof setOrganizationName; updateOrganization: typeof updateOrganization; } export class OrgDetailsPage extends PureComponent { async componentDidMount() { - await this.props.loadStarredDashboards(); await this.props.loadOrganization(); - await this.props.loadOrganizationPreferences(); } onOrgNameChange = name => { @@ -42,22 +31,22 @@ export class OrgDetailsPage extends PureComponent { }; render() { - const { navModel, organization, preferences } = this.props; + const { navModel, organization } = this.props; + const isLoading = Object.keys(organization).length === 0; return (
    - {Object.keys(organization).length === 0 || Object.keys(preferences).length === 0 ? ( - - ) : ( + {isLoading && } + {!isLoading && (
    this.onOrgNameChange(name)} onSubmit={this.onUpdateOrganization} orgName={organization.name} /> - +
    )}
    @@ -70,14 +59,11 @@ function mapStateToProps(state: StoreState) { return { navModel: getNavModel(state.navIndex, 'org-settings'), organization: state.organization.organization, - preferences: state.organization.preferences, }; } const mapDispatchToProps = { loadOrganization, - loadOrganizationPreferences, - loadStarredDashboards, setOrganizationName, updateOrganization, }; diff --git a/public/app/features/org/OrgPreferences.test.tsx b/public/app/features/org/OrgPreferences.test.tsx deleted file mode 100644 index e79e43d04f2..00000000000 --- a/public/app/features/org/OrgPreferences.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { OrgPreferences, Props } from './OrgPreferences'; - -const setup = () => { - const props: Props = { - preferences: { - homeDashboardId: 1, - timezone: 'UTC', - theme: 'Default', - }, - starredDashboards: [{ id: 1, title: 'Standard dashboard', url: '', uri: '', uid: '', type: '', tags: [] }], - setOrganizationTimezone: jest.fn(), - setOrganizationTheme: jest.fn(), - setOrganizationHomeDashboard: jest.fn(), - updateOrganizationPreferences: jest.fn(), - }; - - return shallow(); -}; - -describe('Render', () => { - it('should render component', () => { - const wrapper = setup(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/public/app/features/org/OrgPreferences.tsx b/public/app/features/org/OrgPreferences.tsx deleted file mode 100644 index a8133ba4a1c..00000000000 --- a/public/app/features/org/OrgPreferences.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; -import { Label } from '../../core/components/Label/Label'; -import SimplePicker from '../../core/components/Picker/SimplePicker'; -import { DashboardSearchHit, OrganizationPreferences } from 'app/types'; -import { - setOrganizationHomeDashboard, - setOrganizationTheme, - setOrganizationTimezone, - updateOrganizationPreferences, -} from './state/actions'; - -export interface Props { - preferences: OrganizationPreferences; - starredDashboards: DashboardSearchHit[]; - setOrganizationHomeDashboard: typeof setOrganizationHomeDashboard; - setOrganizationTheme: typeof setOrganizationTheme; - setOrganizationTimezone: typeof setOrganizationTimezone; - updateOrganizationPreferences: typeof updateOrganizationPreferences; -} - -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 OrgPreferences extends PureComponent { - onSubmitForm = event => { - event.preventDefault(); - this.props.updateOrganizationPreferences(); - }; - - render() { - const { - preferences, - starredDashboards, - setOrganizationHomeDashboard, - setOrganizationTimezone, - setOrganizationTheme, - } = this.props; - - starredDashboards.unshift({ id: 0, title: 'Default', tags: [], type: '', uid: '', uri: '', url: '' }); - - return ( -
    -

    Preferences

    -
    - UI Theme - theme.value === preferences.theme)} - options={themes} - getOptionValue={i => i.value} - getOptionLabel={i => i.text} - onSelected={theme => setOrganizationTheme(theme.value)} - width={20} - /> -
    -
    - - dashboard.id === preferences.homeDashboardId)} - getOptionValue={i => i.id} - getOptionLabel={i => i.title} - onSelected={(dashboard: DashboardSearchHit) => setOrganizationHomeDashboard(dashboard.id)} - options={starredDashboards} - placeholder="Chose default dashboard" - width={20} - /> -
    -
    - - timezone.value === preferences.timezone)} - getOptionValue={i => i.value} - getOptionLabel={i => i.text} - onSelected={timezone => setOrganizationTimezone(timezone.value)} - options={timezones} - width={20} - /> -
    -
    - -
    - - ); - } -} - -function mapStateToProps(state) { - return { - preferences: state.organization.preferences, - starredDashboards: state.user.starredDashboards, - }; -} - -const mapDispatchToProps = { - setOrganizationHomeDashboard, - setOrganizationTimezone, - setOrganizationTheme, - updateOrganizationPreferences, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(OrgPreferences); diff --git a/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap b/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap index 28806d2bf1d..582d626d315 100644 --- a/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap +++ b/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap @@ -29,7 +29,9 @@ exports[`Render should render organization and preferences 1`] = ` onSubmit={[Function]} orgName="Cool org" /> - +
    diff --git a/public/app/features/org/__snapshots__/OrgPreferences.test.tsx.snap b/public/app/features/org/__snapshots__/OrgPreferences.test.tsx.snap deleted file mode 100644 index 06bf464a4a0..00000000000 --- a/public/app/features/org/__snapshots__/OrgPreferences.test.tsx.snap +++ /dev/null @@ -1,136 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render should render component 1`] = ` -
    -

    - Preferences -

    -
    - - UI Theme - - -
    -
    - - Home Dashboard - - -
    -
    - - -
    -
    - -
    - -`; diff --git a/public/app/features/org/state/actions.ts b/public/app/features/org/state/actions.ts index 4df9083c323..aeec8297ea6 100644 --- a/public/app/features/org/state/actions.ts +++ b/public/app/features/org/state/actions.ts @@ -1,16 +1,12 @@ import { ThunkAction } from 'redux-thunk'; -import { Organization, OrganizationPreferences, StoreState } from 'app/types'; -import { getBackendSrv } from '../../../core/services/backend_srv'; +import { Organization, StoreState } from 'app/types'; +import { getBackendSrv } from 'app/core/services/backend_srv'; type ThunkResult = ThunkAction; export enum ActionTypes { LoadOrganization = 'LOAD_ORGANISATION', - LoadPreferences = 'LOAD_PREFERENCES', SetOrganizationName = 'SET_ORGANIZATION_NAME', - SetOrganizationTheme = 'SET_ORGANIZATION_THEME', - SetOrganizationHomeDashboard = 'SET_ORGANIZATION_HOME_DASHBOARD', - SetOrganizationTimezone = 'SET_ORGANIZATION_TIMEZONE', } interface LoadOrganizationAction { @@ -18,68 +14,22 @@ interface LoadOrganizationAction { payload: Organization; } -interface LoadPreferencesAction { - type: ActionTypes.LoadPreferences; - payload: OrganizationPreferences; -} - interface SetOrganizationNameAction { type: ActionTypes.SetOrganizationName; payload: string; } -interface SetOrganizationThemeAction { - type: ActionTypes.SetOrganizationTheme; - payload: string; -} - -interface SetOrganizationHomeDashboardAction { - type: ActionTypes.SetOrganizationHomeDashboard; - payload: number; -} - -interface SetOrganizationTimezoneAction { - type: ActionTypes.SetOrganizationTimezone; - payload: string; -} - const organisationLoaded = (organisation: Organization) => ({ type: ActionTypes.LoadOrganization, payload: organisation, }); -const preferencesLoaded = (preferences: OrganizationPreferences) => ({ - type: ActionTypes.LoadPreferences, - payload: preferences, -}); - export const setOrganizationName = (orgName: string) => ({ type: ActionTypes.SetOrganizationName, payload: orgName, }); -export const setOrganizationTheme = (theme: string) => ({ - type: ActionTypes.SetOrganizationTheme, - payload: theme, -}); - -export const setOrganizationHomeDashboard = (id: number) => ({ - type: ActionTypes.SetOrganizationHomeDashboard, - payload: id, -}); - -export const setOrganizationTimezone = (timezone: string) => ({ - type: ActionTypes.SetOrganizationTimezone, - payload: timezone, -}); - -export type Action = - | LoadOrganizationAction - | LoadPreferencesAction - | SetOrganizationNameAction - | SetOrganizationThemeAction - | SetOrganizationHomeDashboardAction - | SetOrganizationTimezoneAction; +export type Action = LoadOrganizationAction | SetOrganizationNameAction; export function loadOrganization(): ThunkResult { return async dispatch => { @@ -90,13 +40,6 @@ export function loadOrganization(): ThunkResult { }; } -export function loadOrganizationPreferences(): ThunkResult { - return async dispatch => { - const preferencesResponse = await getBackendSrv().get('/api/org/preferences'); - dispatch(preferencesLoaded(preferencesResponse)); - }; -} - export function updateOrganization() { return async (dispatch, getStore) => { const organization = getStore().organization.organization; @@ -106,13 +49,3 @@ export function updateOrganization() { dispatch(loadOrganization()); }; } - -export function updateOrganizationPreferences() { - return async (dispatch, getStore) => { - const preferences = getStore().organization.preferences; - - await getBackendSrv().put('/api/org/preferences', preferences); - - window.location.reload(); - }; -} diff --git a/public/app/features/org/state/reducers.ts b/public/app/features/org/state/reducers.ts index b79f915a731..acde6d17e5f 100644 --- a/public/app/features/org/state/reducers.ts +++ b/public/app/features/org/state/reducers.ts @@ -1,9 +1,8 @@ -import { Organization, OrganizationPreferences, OrganizationState } from 'app/types'; +import { Organization, OrganizationState } from 'app/types'; import { Action, ActionTypes } from './actions'; const initialState: OrganizationState = { organization: {} as Organization, - preferences: {} as OrganizationPreferences, }; const organizationReducer = (state = initialState, action: Action): OrganizationState => { @@ -11,20 +10,8 @@ const organizationReducer = (state = initialState, action: Action): Organization case ActionTypes.LoadOrganization: return { ...state, organization: action.payload }; - case ActionTypes.LoadPreferences: - return { ...state, preferences: action.payload }; - case ActionTypes.SetOrganizationName: return { ...state, organization: { ...state.organization, name: action.payload } }; - - case ActionTypes.SetOrganizationTheme: - return { ...state, preferences: { ...state.preferences, theme: action.payload } }; - - case ActionTypes.SetOrganizationHomeDashboard: - return { ...state, preferences: { ...state.preferences, homeDashboardId: action.payload } }; - - case ActionTypes.SetOrganizationTimezone: - return { ...state, preferences: { ...state.preferences, timezone: action.payload } }; } return state; diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index f520b5eefc0..508e7ac1ab4 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -76,7 +76,6 @@ export class MetricsTabCtrl { return; } - this.datasourceInstance = option.datasource; this.setDatasource(option.datasource); this.updateDatasourceOptions(); } @@ -96,6 +95,7 @@ export class MetricsTabCtrl { }); } + this.datasourceInstance = datasource; this.panel.datasource = datasource.value; this.panel.refresh(); } diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 77ebf754b3a..61c2be2adea 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -140,6 +140,19 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { }); }); + ctrl.events.on('view-mode-changed', () => { + // first wait one pass for dashboard fullscreen view mode to take effect (classses being applied) + setTimeout(() => { + // then recalc style + ctrl.calculatePanelHeight(); + // then wait another cycle (this might not be needed) + $timeout(() => { + ctrl.render(); + resizeScrollableContent(); + }); + }); + }); + // set initial height ctrl.calculatePanelHeight(); @@ -161,7 +174,11 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { panelContainer.removeClass('panel-alert-state--' + lastAlertState); } - if (ctrl.alertState.state === 'ok' || ctrl.alertState.state === 'alerting') { + if ( + ctrl.alertState.state === 'ok' || + ctrl.alertState.state === 'alerting' || + ctrl.alertState.state === 'pending' + ) { panelContainer.addClass('panel-alert-state--' + ctrl.alertState.state); } diff --git a/public/app/features/playlist/partials/playlists.html b/public/app/features/playlist/partials/playlists.html index ce6a5498ca7..cf0a8d19ed7 100644 --- a/public/app/features/playlist/partials/playlists.html +++ b/public/app/features/playlist/partials/playlists.html @@ -59,7 +59,7 @@ buttonIcon: 'fa fa-plus', buttonLink: 'playlists/create', buttonTitle: ' Create Playlist', - proTip: 'You can use playlists to remove control TVs', + proTip: 'You can use playlists to cycle dashboards on TVs without user control', proTipLink: 'http://docs.grafana.org/reference/playlist/', proTipLinkTitle: 'Learn more', proTipTarget: '_blank' diff --git a/public/app/features/plugins/VariableQueryComponentLoader.tsx b/public/app/features/plugins/VariableQueryComponentLoader.tsx new file mode 100644 index 00000000000..631161b4e9b --- /dev/null +++ b/public/app/features/plugins/VariableQueryComponentLoader.tsx @@ -0,0 +1,36 @@ +import coreModule from 'app/core/core_module'; +import { importPluginModule } from './plugin_loader'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import DefaultVariableQueryEditor from '../templating/DefaultVariableQueryEditor'; + +async function loadComponent(module) { + const component = await importPluginModule(module); + if (component && component.VariableQueryEditor) { + return component.VariableQueryEditor; + } else { + return DefaultVariableQueryEditor; + } +} + +/** @ngInject */ +function variableQueryEditorLoader(templateSrv) { + return { + restrict: 'E', + link: async (scope, elem) => { + const Component = await loadComponent(scope.currentDatasource.meta.module); + const props = { + datasource: scope.currentDatasource, + query: scope.current.query, + onChange: scope.onQueryChange, + templateSrv, + }; + ReactDOM.render(, elem[0]); + scope.$on('$destroy', () => { + ReactDOM.unmountComponentAtNode(elem[0]); + }); + }, + }; +} + +coreModule.directive('variableQueryEditorLoader', variableQueryEditorLoader); diff --git a/public/app/features/plugins/all.ts b/public/app/features/plugins/all.ts index c9fb250266c..757a9a2f491 100644 --- a/public/app/features/plugins/all.ts +++ b/public/app/features/plugins/all.ts @@ -4,3 +4,4 @@ import './import_list/import_list'; import './ds_edit_ctrl'; import './datasource_srv'; import './plugin_component'; +import './VariableQueryComponentLoader'; diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index b9779190a8b..a0cedb3ebfd 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -24,6 +24,7 @@ import * as heatmapPanel from 'app/plugins/panel/heatmap/module'; import * as tablePanel from 'app/plugins/panel/table/module'; import * as singlestatPanel from 'app/plugins/panel/singlestat/module'; import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module'; +import * as gaugePanel from 'app/plugins/panel/gauge/module'; const builtInPlugins = { 'app/plugins/datasource/graphite/module': graphitePlugin, @@ -52,6 +53,7 @@ const builtInPlugins = { 'app/plugins/panel/table/module': tablePanel, 'app/plugins/panel/singlestat/module': singlestatPanel, 'app/plugins/panel/gettingstarted/module': gettingStartedPanel, + 'app/plugins/panel/gauge/module': gaugePanel, }; export default builtInPlugins; diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index fed455472c9..f7bd6b4d1ee 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -22,7 +22,7 @@ export class DatasourceSrv { this.datasources = {}; } - get(name?): Promise { + get(name?: string): Promise { if (!name) { return this.get(config.defaultDatasource); } @@ -40,7 +40,7 @@ export class DatasourceSrv { return this.loadDatasource(name); } - loadDatasource(name) { + loadDatasource(name: string): Promise { const dsConfig = config.datasources[name]; if (!dsConfig) { return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' }); diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index c223f444ef3..1dec41f05e8 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -118,7 +118,7 @@ export class DataSourceEditCtrl { } testDatasource() { - this.datasourceSrv.get(this.current.name).then(datasource => { + return this.datasourceSrv.get(this.current.name).then(datasource => { if (!datasource.testDatasource) { return; } @@ -126,7 +126,7 @@ export class DataSourceEditCtrl { this.testing = { done: false, status: 'error' }; // make test call in no backend cache context - this.backendSrv + return this.backendSrv .withNoBackendCache(() => { return datasource .testDatasource() @@ -161,8 +161,8 @@ export class DataSourceEditCtrl { return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(result => { this.current = result.datasource; this.updateNav(); - this.updateFrontendSettings().then(() => { - this.testDatasource(); + return this.updateFrontendSettings().then(() => { + return this.testDatasource(); }); }); } else { diff --git a/public/app/features/profile/PrefControlCtrl.ts b/public/app/features/profile/PrefControlCtrl.ts new file mode 100644 index 00000000000..39a01bd0e2d --- /dev/null +++ b/public/app/features/profile/PrefControlCtrl.ts @@ -0,0 +1,4 @@ +import { react2AngularDirective } from 'app/core/utils/react2angular'; +import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; + +react2AngularDirective('prefsControl', SharedPreferences, ['resourceUri']); diff --git a/public/app/features/profile/all.ts b/public/app/features/profile/all.ts new file mode 100644 index 00000000000..6d462c08e6b --- /dev/null +++ b/public/app/features/profile/all.ts @@ -0,0 +1,3 @@ +import './ProfileCtrl'; +import './ChangePasswordCtrl'; +import './PrefControlCtrl'; diff --git a/public/app/features/profile/partials/profile.html b/public/app/features/profile/partials/profile.html index 7858e00c683..543b0e2e39a 100644 --- a/public/app/features/profile/partials/profile.html +++ b/public/app/features/profile/partials/profile.html @@ -24,7 +24,7 @@
    - +

    Teams

    diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index 939dfcc8e31..3c3561c605e 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -124,7 +124,11 @@ export class TeamGroupSync extends PureComponent {
    {headerTooltip} - + Learn more
    diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index da57bfbdfd3..f43dc44808f 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -74,7 +74,7 @@ export class TeamMembers extends PureComponent {
  3. - {syncEnabled ? this.renderLabels(member.labels) : null} + {syncEnabled && this.renderLabels(member.labels)} @@ -132,7 +132,7 @@ export class TeamMembers extends PureComponent { - {syncEnabled ? diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx index 65084d0dc47..5d751f46989 100644 --- a/public/app/features/teams/TeamPages.test.tsx +++ b/public/app/features/teams/TeamPages.test.tsx @@ -43,10 +43,15 @@ describe('Render', () => { expect(wrapper).toMatchSnapshot(); }); - it('should render settings page', () => { + it('should render settings and preferences page', () => { const { wrapper } = setup({ team: getMockTeam(), pageName: 'settings', + preferences: { + homeDashboardId: 1, + theme: 'Default', + timezone: 'Default', + }, }); expect(wrapper).toMatchSnapshot(); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 3dc5a9f6f15..38ba23262ba 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -41,14 +41,14 @@ export class TeamPages extends PureComponent { }; } - componentDidMount() { - this.fetchTeam(); + async componentDidMount() { + await this.fetchTeam(); } async fetchTeam() { const { loadTeam, teamId } = this.props; - await loadTeam(teamId); + return await loadTeam(teamId); } getCurrentPage() { @@ -67,7 +67,6 @@ export class TeamPages extends PureComponent { case PageTypes.Settings: return ; - case PageTypes.GroupSync: return isSyncEnabled && ; } diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 45977de95bf..35c306d664e 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,10 +1,12 @@ import React from 'react'; import { connect } from 'react-redux'; + import { Label } from 'app/core/components/Label/Label'; -import { Team } from '../../types'; +import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { updateTeam } from './state/actions'; -import { getRouteParamsId } from '../../core/selectors/location'; +import { getRouteParamsId } from 'app/core/selectors/location'; import { getTeam } from './state/selectors'; +import { Team } from 'app/types'; export interface Props { team: Team; @@ -41,6 +43,7 @@ export class TeamSettings extends React.Component { }; render() { + const { team } = this.props; const { name, email } = this.state; return ( @@ -76,6 +79,7 @@ export class TeamSettings extends React.Component { + ); } diff --git a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap index 5a143f19038..d486657d352 100644 --- a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap @@ -96,7 +96,7 @@ exports[`Render should render component 1`] = ` Sync LDAP or OAuth groups with your Grafana teams. Learn more diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index f32b8211d2c..f966c52983d 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -36,7 +36,7 @@ exports[`Render should render member page if team not empty 1`] = ` `; -exports[`Render should render settings page 1`] = ` +exports[`Render should render settings and preferences page 1`] = `
    + `; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index 3764a9355c6..e88fbdfd4b1 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -10,7 +10,6 @@ describe('Teams selectors', () => { const mockState: TeamsState = { teams: mockTeams, searchQuery: '', hasFetched: false }; const teams = getTeams(mockState); - expect(teams).toEqual(mockTeams); }); @@ -18,7 +17,6 @@ describe('Teams selectors', () => { const mockState: TeamsState = { teams: mockTeams, searchQuery: '5', hasFetched: false }; const teams = getTeams(mockState); - expect(teams.length).toEqual(1); }); }); @@ -29,10 +27,14 @@ describe('Team selectors', () => { const mockTeam = getMockTeam(); it('should return team if matching with location team', () => { - const mockState: TeamState = { team: mockTeam, searchMemberQuery: '', members: [], groups: [] }; + const mockState: TeamState = { + team: mockTeam, + searchMemberQuery: '', + members: [], + groups: [], + }; const team = getTeam(mockState, '1'); - expect(team).toEqual(mockTeam); }); }); @@ -49,7 +51,6 @@ describe('Team selectors', () => { }; const members = getTeamMembers(mockState); - expect(members).toEqual(mockTeamMembers); }); }); diff --git a/public/app/features/templating/DefaultVariableQueryEditor.tsx b/public/app/features/templating/DefaultVariableQueryEditor.tsx new file mode 100644 index 00000000000..ea5f2acaade --- /dev/null +++ b/public/app/features/templating/DefaultVariableQueryEditor.tsx @@ -0,0 +1,34 @@ +import React, { PureComponent } from 'react'; +import { VariableQueryProps } from 'app/types/plugins'; + +export default class DefaultVariableQueryEditor extends PureComponent { + constructor(props) { + super(props); + this.state = { value: props.query }; + } + + handleChange(event) { + this.setState({ value: event.target.value }); + } + + handleBlur(event) { + this.props.onChange(event.target.value, event.target.value); + } + + render() { + return ( +
    + Query + this.handleChange(e)} + onBlur={e => this.handleBlur(e)} + placeholder="metric name or tags query" + required + /> +
    + ); + } +} diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index cef7c9cc912..fdab5587b42 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -72,6 +72,7 @@ export class VariableEditorCtrl { if ( $scope.current.type === 'query' && + _.isString($scope.current.query) && $scope.current.query.match(new RegExp('\\$' + $scope.current.name + '(/| |$)')) ) { appEvents.emit('alert-warning', [ @@ -106,11 +107,20 @@ export class VariableEditorCtrl { }); }; + $scope.onQueryChange = (query, definition) => { + $scope.current.query = query; + $scope.current.definition = definition; + $scope.runQuery(); + }; + $scope.edit = variable => { $scope.current = variable; $scope.currentIsNew = false; $scope.mode = 'edit'; $scope.validate(); + datasourceSrv.get($scope.current.datasource).then(ds => { + $scope.currentDatasource = ds; + }); }; $scope.duplicate = variable => { @@ -171,6 +181,13 @@ export class VariableEditorCtrl { $scope.showMoreOptions = () => { $scope.optionsLimit += 20; }; + + $scope.datasourceChanged = async () => { + datasourceSrv.get($scope.current.datasource).then(ds => { + $scope.current.query = ''; + $scope.currentDatasource = ds; + }); + }; } } diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index c4463972177..15984eba7d6 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -17,14 +17,16 @@
    What do variables do?
    -

    Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names - in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of - the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard. +

    Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor + names + in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the + top of + the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard. - Check out the - - Templating documentation - for more information. + Check out the + + Templating documentation + for more information.

    @@ -32,7 +34,7 @@
    {member.login} {member.email} this.onRemoveMember(member)} /> Name Email : ''} + {syncEnabled && }
    @@ -51,7 +53,7 @@ @@ -77,7 +79,8 @@
    Name - +
    @@ -87,13 +90,15 @@
    - +
    - Template names cannot begin with '__', that's reserved for Grafana's global variables + Template names cannot begin with '__', that's reserved for + Grafana's global variables
    @@ -115,7 +120,8 @@
    Values - +
    @@ -127,14 +133,16 @@ Step count How many times should the current time range be divided to calculate the value
    - +
    Min interval The calculated value will not go below this threshold - +
    @@ -143,7 +151,8 @@
    Custom Options
    Values separated by comma - +
    @@ -168,15 +177,17 @@
    - Data source + Data source
    -
    +
    - + Refresh When to update the values of this variable. @@ -187,28 +198,32 @@
    + + + + + +
    - Query - -
    -
    - + Regex Optional, if you want to extract part of a series name or metric node segment. - +
    - + Sort How to sort the values of this variable.
    - +
    @@ -219,7 +234,8 @@
    - +
    @@ -234,7 +250,8 @@ - + @@ -243,7 +260,8 @@
    Data source
    -
    @@ -253,18 +271,11 @@
    Selection Options
    - + - +
    @@ -279,11 +290,13 @@
    Tags query - +
  4. Tag values query
  5. - +
    @@ -291,11 +304,11 @@
    Preview of values
    - {{option.text}} -
    -
    - Show more -
    + {{option.text}} +
    +
    + Show more +
    @@ -309,5 +322,4 @@ - - + \ No newline at end of file diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index d3f39023cfb..0aec1d8f412 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -23,6 +23,7 @@ export class QueryVariable implements Variable { tagValuesQuery: string; tags: any[]; skipUrlSync: boolean; + definition: string; defaults = { type: 'query', @@ -44,6 +45,7 @@ export class QueryVariable implements Variable { tagsQuery: '', tagValuesQuery: '', skipUrlSync: false, + definition: '', }; /** @ngInject */ diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 0db7b8e77e0..485d244e9ae 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -28,7 +28,7 @@ export class TemplateSrv { const existsOrEmpty = value => value || value === ''; this.index = this.variables.reduce((acc, currentValue) => { - if (currentValue.current && !currentValue.current.isNone && existsOrEmpty(currentValue.current.value)) { + if (currentValue.current && (currentValue.current.isNone || existsOrEmpty(currentValue.current.value))) { acc[currentValue.name] = currentValue; } return acc; diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts index 1994e86eff0..930d2c49228 100644 --- a/public/app/features/templating/variable.ts +++ b/public/app/features/templating/variable.ts @@ -1,3 +1,4 @@ +import _ from 'lodash'; import { assignModelProperties } from 'app/core/utils/model_utils'; /* @@ -28,6 +29,7 @@ export { assignModelProperties }; export function containsVariable(...args: any[]) { const variableName = args[args.length - 1]; + args[0] = _.isString(args[0]) ? args[0] : Object['values'](args[0]).join(' '); const variableString = args.slice(0, -1).join(' '); const matches = variableString.match(variableRegex); const isMatchingVariable = diff --git a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts index 154ff9bcb91..fad3a6895ae 100644 --- a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts +++ b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts @@ -28,9 +28,13 @@ export class ElasticConfigCtrl { ]; indexPatternTypeChanged() { - const def = _.find(this.indexPatternTypes, { - value: this.current.jsonData.interval, - }); - this.current.database = def.example || 'es-index-name'; + if (!this.current.database || + this.current.database.length === 0 || + this.current.database.startsWith('[logstash-]')) { + const def = _.find(this.indexPatternTypes, { + value: this.current.jsonData.interval, + }); + this.current.database = def.example || 'es-index-name'; + } } } diff --git a/public/app/plugins/datasource/elasticsearch/metric_agg.ts b/public/app/plugins/datasource/elasticsearch/metric_agg.ts index 1dd0d892360..56f874d90b9 100644 --- a/public/app/plugins/datasource/elasticsearch/metric_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/metric_agg.ts @@ -160,6 +160,12 @@ export class ElasticMetricAggCtrl { $scope.agg.settings = {}; $scope.agg.meta = {}; $scope.showOptions = false; + + // reset back to metric/group by query + if ($scope.target.bucketAggs.length === 0 && $scope.agg.type !== 'raw_document') { + $scope.target.bucketAggs = [queryDef.defaultBucketAgg()]; + } + $scope.updatePipelineAggOptions(); $scope.onChange(); }; diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.ts b/public/app/plugins/datasource/elasticsearch/query_builder.ts index 21af0ba9b80..efdeee68370 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.ts +++ b/public/app/plugins/datasource/elasticsearch/query_builder.ts @@ -181,8 +181,8 @@ export class ElasticQueryBuilder { build(target, adhocFilters?, queryString?) { // make sure query has defaults; - target.metrics = target.metrics || [{ type: 'count', id: '1' }]; - target.bucketAggs = target.bucketAggs || [{ type: 'date_histogram', id: '2', settings: { interval: 'auto' } }]; + target.metrics = target.metrics || [queryDef.defaultMetricAgg()]; + target.bucketAggs = target.bucketAggs || [queryDef.defaultBucketAgg()]; target.timeField = this.timeField; let i, nestedAggs, metric; diff --git a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts index 422d35dd277..81dbd5b8f8a 100644 --- a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts +++ b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts @@ -17,6 +17,19 @@ export class ElasticQueryCtrl extends QueryCtrl { super($scope, $injector); this.esVersion = this.datasource.esVersion; + + this.target = this.target || {}; + this.target.metrics = this.target.metrics || [queryDef.defaultMetricAgg()]; + this.target.bucketAggs = this.target.bucketAggs || [queryDef.defaultBucketAgg()]; + + if (this.target.bucketAggs.length === 0) { + const metric = this.target.metrics[0]; + if (!metric || metric.type !== 'raw_document') { + this.target.bucketAggs = [queryDef.defaultBucketAgg()]; + } + this.refresh(); + } + this.queryUpdated(); } diff --git a/public/app/plugins/datasource/elasticsearch/query_def.ts b/public/app/plugins/datasource/elasticsearch/query_def.ts index dd65a8b373e..3ea5945f54d 100644 --- a/public/app/plugins/datasource/elasticsearch/query_def.ts +++ b/public/app/plugins/datasource/elasticsearch/query_def.ts @@ -228,3 +228,11 @@ export function describeOrderBy(orderBy, target) { return 'metric not found'; } } + +export function defaultMetricAgg() { + return { type: 'count', id: '1' }; +} + +export function defaultBucketAgg() { + return { type: 'date_histogram', id: '2', settings: { interval: 'auto' } }; +} diff --git a/public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx b/public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx index a7af48b6eda..651c28783d9 100644 --- a/public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx +++ b/public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx @@ -19,7 +19,10 @@ export default (props: any) => ( {CHEAT_SHEET_ITEMS.map(item => (
    {item.title}
    -
    props.onClickQuery(item.expression)}> +
    props.onClickExample({ refId: '1', expr: item.expression })} + > {item.expression}
    {item.label}
    diff --git a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx b/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx index ce79d38f9a8..5667bd9a20d 100644 --- a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx +++ b/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx @@ -10,7 +10,8 @@ import { TypeaheadOutput } from 'app/types/explore'; import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom'; import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; -import TypeaheadField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; +import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; +import { DataQuery } from 'app/types'; const PRISM_SYNTAX = 'promql'; @@ -53,10 +54,10 @@ interface LoggingQueryFieldProps { error?: string | JSX.Element; hint?: any; history?: any[]; - initialQuery?: string | null; + initialQuery?: DataQuery; onClickHintFix?: (action: any) => void; onPressEnter?: () => void; - onQueryChange?: (value: string, override?: boolean) => void; + onQueryChange?: (value: DataQuery, override?: boolean) => void; } interface LoggingQueryFieldState { @@ -134,9 +135,13 @@ class LoggingQueryField extends React.PureComponent { // Send text change to parent - const { onQueryChange } = this.props; + const { initialQuery, onQueryChange } = this.props; if (onQueryChange) { - onQueryChange(value, override); + const query = { + ...initialQuery, + expr: value, + }; + onQueryChange(query, override); } }; @@ -196,15 +201,15 @@ class LoggingQueryField extends React.PureComponent
    - {error ?
    {error}
    : null} diff --git a/public/app/plugins/datasource/logging/components/LoggingStartPage.tsx b/public/app/plugins/datasource/logging/components/LoggingStartPage.tsx index 89262999637..2c25a248fa9 100644 --- a/public/app/plugins/datasource/logging/components/LoggingStartPage.tsx +++ b/public/app/plugins/datasource/logging/components/LoggingStartPage.tsx @@ -52,7 +52,7 @@ export default class LoggingStartPage extends PureComponent
    - {active === 'start' && } + {active === 'start' && }
    ); diff --git a/public/app/plugins/datasource/logging/datasource.ts b/public/app/plugins/datasource/logging/datasource.ts index fcf3028c025..494dcd78d6c 100644 --- a/public/app/plugins/datasource/logging/datasource.ts +++ b/public/app/plugins/datasource/logging/datasource.ts @@ -1,10 +1,11 @@ import _ from 'lodash'; import * as dateMath from 'app/core/utils/datemath'; +import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model'; +import { PluginMeta, DataQuery } from 'app/types'; import LanguageProvider from './language_provider'; import { mergeStreamsToLogs } from './result_transformer'; -import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model'; export const DEFAULT_LIMIT = 1000; @@ -111,6 +112,10 @@ export default class LoggingDatasource { }); } + async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise { + return this.languageProvider.importQueries(queries, originMeta.id); + } + metadataRequest(url) { // HACK to get label values for {job=|}, will be replaced when implementing LoggingQueryField const apiUrl = url.replace('v1', 'prom'); diff --git a/public/app/plugins/datasource/logging/language_provider.test.ts b/public/app/plugins/datasource/logging/language_provider.test.ts new file mode 100644 index 00000000000..f4fc7efa7cb --- /dev/null +++ b/public/app/plugins/datasource/logging/language_provider.test.ts @@ -0,0 +1,108 @@ +import Plain from 'slate-plain-serializer'; + +import LanguageProvider from './language_provider'; + +describe('Language completion provider', () => { + const datasource = { + metadataRequest: () => ({ data: { data: [] } }), + }; + + describe('empty query suggestions', () => { + it('returns default suggestions on emtpty context', () => { + const instance = new LanguageProvider(datasource); + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(0); + }); + + it('returns default suggestions with history on emtpty context when history was provided', () => { + const instance = new LanguageProvider(datasource); + const value = Plain.deserialize(''); + const history = [ + { + query: { refId: '1', expr: '{app="foo"}' }, + }, + ]; + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }, { history }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toMatchObject([ + { + label: 'History', + items: [ + { + label: '{app="foo"}', + }, + ], + }, + ]); + }); + }); + + describe('label suggestions', () => { + it('returns default label suggestions on label context', () => { + const instance = new LanguageProvider(datasource); + const value = Plain.deserialize('{}'); + const range = value.selection.merge({ + anchorOffset: 1, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-labels'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-labels'); + expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'namespace' }], label: 'Labels' }]); + }); + }); +}); + +describe('Query imports', () => { + const datasource = { + metadataRequest: () => ({ data: { data: [] } }), + }; + + it('returns empty queries for unknown origin datasource', async () => { + const instance = new LanguageProvider(datasource); + const result = await instance.importQueries([{ refId: 'bar', expr: 'foo' }], 'unknown'); + expect(result).toEqual([{ refId: 'bar', expr: '' }]); + }); + + describe('prometheus query imports', () => { + it('returns empty query from metric-only query', async () => { + const instance = new LanguageProvider(datasource); + const result = await instance.importPrometheusQuery('foo'); + expect(result).toEqual(''); + }); + + it('returns empty query from selector query if label is not available', async () => { + const datasourceWithLabels = { + metadataRequest: url => (url === '/api/prom/label' ? { data: { data: ['other'] } } : { data: { data: [] } }), + }; + const instance = new LanguageProvider(datasourceWithLabels); + const result = await instance.importPrometheusQuery('{foo="bar"}'); + expect(result).toEqual('{}'); + }); + + it('returns selector query from selector query with common labels', async () => { + const datasourceWithLabels = { + metadataRequest: url => (url === '/api/prom/label' ? { data: { data: ['foo'] } } : { data: { data: [] } }), + }; + const instance = new LanguageProvider(datasourceWithLabels); + const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}'); + expect(result).toEqual('{foo="bar"}'); + }); + + it('returns selector query from selector query with all labels if logging label list is empty', async () => { + const datasourceWithLabels = { + metadataRequest: url => (url === '/api/prom/label' ? { data: { data: [] } } : { data: { data: [] } }), + }; + const instance = new LanguageProvider(datasourceWithLabels); + const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}'); + expect(result).toEqual('{baz="42",foo="bar"}'); + }); + }); +}); diff --git a/public/app/plugins/datasource/logging/language_provider.ts b/public/app/plugins/datasource/logging/language_provider.ts index 0896168ca56..21d2846ac63 100644 --- a/public/app/plugins/datasource/logging/language_provider.ts +++ b/public/app/plugins/datasource/logging/language_provider.ts @@ -7,10 +7,11 @@ import { LanguageProvider, TypeaheadInput, TypeaheadOutput, + HistoryItem, } from 'app/types/explore'; - -import { parseSelector } from 'app/plugins/datasource/prometheus/language_utils'; +import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils'; import PromqlSyntax from 'app/plugins/datasource/prometheus/promql'; +import { DataQuery } from 'app/types'; const DEFAULT_KEYS = ['job', 'namespace']; const EMPTY_SELECTOR = '{}'; @@ -19,9 +20,9 @@ const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h const wrapLabel = (label: string) => ({ label }); -export function addHistoryMetadata(item: CompletionItem, history: any[]): CompletionItem { +export function addHistoryMetadata(item: CompletionItem, history: HistoryItem[]): CompletionItem { const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF; - const historyForItem = history.filter(h => h.ts > cutoffTs && h.query === item.label); + const historyForItem = history.filter(h => h.ts > cutoffTs && (h.query.expr as string) === item.label); const count = historyForItem.length; const recent = historyForItem[0]; let hint = `Queried ${count} times in the last 24h.`; @@ -96,9 +97,10 @@ export default class LoggingLanguageProvider extends LanguageProvider { if (history && history.length > 0) { const historyItems = _.chain(history) - .uniqBy('query') + .map(h => h.query.expr) + .filter() + .uniq() .take(HISTORY_ITEM_COUNT) - .map(h => h.query) .map(wrapLabel) .map(item => addHistoryMetadata(item, history)) .value(); @@ -158,6 +160,67 @@ export default class LoggingLanguageProvider extends LanguageProvider { return { context, refresher, suggestions }; } + async importQueries(queries: DataQuery[], datasourceType: string): Promise { + if (datasourceType === 'prometheus') { + return Promise.all( + queries.map(async query => { + const expr = await this.importPrometheusQuery(query.expr); + return { + ...query, + expr, + }; + }) + ); + } + return queries.map(query => ({ + ...query, + expr: '', + })); + } + + async importPrometheusQuery(query: string): Promise { + if (!query) { + return ''; + } + + // Consider only first selector in query + const selectorMatch = query.match(selectorRegexp); + if (selectorMatch) { + const selector = selectorMatch[0]; + const labels = {}; + selector.replace(labelRegexp, (_, key, operator, value) => { + labels[key] = { value, operator }; + return ''; + }); + + // Keep only labels that exist on origin and target datasource + await this.start(); // fetches all existing label keys + const existingKeys = this.labelKeys[EMPTY_SELECTOR]; + let labelsToKeep = {}; + if (existingKeys && existingKeys.length > 0) { + // Check for common labels + for (const key in labels) { + if (existingKeys && existingKeys.indexOf(key) > -1) { + // Should we check for label value equality here? + labelsToKeep[key] = labels[key]; + } + } + } else { + // Keep all labels by default + labelsToKeep = labels; + } + + const labelKeys = Object.keys(labelsToKeep).sort(); + const cleanSelector = labelKeys + .map(key => `${key}${labelsToKeep[key].operator}${labelsToKeep[key].value}`) + .join(','); + + return ['{', cleanSelector, '}'].join(''); + } + + return ''; + } + async fetchLogLabels() { const url = '/api/prom/label'; try { diff --git a/public/app/plugins/datasource/logging/result_transformer.test.ts b/public/app/plugins/datasource/logging/result_transformer.test.ts index 6f88c301fcd..c57190d927d 100644 --- a/public/app/plugins/datasource/logging/result_transformer.test.ts +++ b/public/app/plugins/datasource/logging/result_transformer.test.ts @@ -11,11 +11,17 @@ import { describe('getLoglevel()', () => { it('returns no log level on empty line', () => { - expect(getLogLevel('')).toBe(LogLevel.none); + expect(getLogLevel('')).toBe(LogLevel.unkown); }); it('returns no log level on when level is part of a word', () => { - expect(getLogLevel('this is a warning')).toBe(LogLevel.none); + expect(getLogLevel('this is information')).toBe(LogLevel.unkown); + }); + + it('returns same log level for long and short version', () => { + expect(getLogLevel('[Warn]')).toBe(LogLevel.warning); + expect(getLogLevel('[Warning]')).toBe(LogLevel.warning); + expect(getLogLevel('[Warn]')).toBe('warning'); }); it('returns log level on line contains a log level', () => { @@ -102,7 +108,7 @@ describe('mergeStreamsToLogs()', () => { entry: 'WARN boooo', labels: '{foo="bar"}', key: 'EK1970-01-01T00:00:00Z{foo="bar"}', - logLevel: 'warn', + logLevel: 'warning', uniqueLabels: '', }, ]); @@ -141,7 +147,7 @@ describe('mergeStreamsToLogs()', () => { { entry: 'WARN boooo', labels: '{foo="bar", baz="1"}', - logLevel: 'warn', + logLevel: 'warning', uniqueLabels: '{baz="1"}', }, { diff --git a/public/app/plugins/datasource/logging/result_transformer.ts b/public/app/plugins/datasource/logging/result_transformer.ts index 3d04deb7ed2..a6b9ef70abb 100644 --- a/public/app/plugins/datasource/logging/result_transformer.ts +++ b/public/app/plugins/datasource/logging/result_transformer.ts @@ -14,13 +14,13 @@ import { DEFAULT_LIMIT } from './datasource'; /** * Returns the log level of a log line. - * Parse the line for level words. If no level is found, it returns `LogLevel.none`. + * Parse the line for level words. If no level is found, it returns `LogLevel.unknown`. * * Example: `getLogLevel('WARN 1999-12-31 this is great') // LogLevel.warn` */ export function getLogLevel(line: string): LogLevel { if (!line) { - return LogLevel.none; + return LogLevel.unkown; } let level: LogLevel; Object.keys(LogLevel).forEach(key => { @@ -32,7 +32,7 @@ export function getLogLevel(line: string): LogLevel { } }); if (!level) { - level = LogLevel.none; + level = LogLevel.unkown; } return level; } diff --git a/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx b/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx index a2d3a03d794..ea9a373e67a 100644 --- a/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx @@ -25,7 +25,10 @@ export default (props: any) => ( {CHEAT_SHEET_ITEMS.map(item => (
    {item.title}
    -
    props.onClickQuery(item.expression)}> +
    props.onClickExample({ refId: '1', expr: item.expression })} + > {item.expression}
    {item.label}
    diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index a7787096d85..6171c662127 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -10,7 +10,8 @@ import { TypeaheadOutput } from 'app/types/explore'; import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom'; import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; -import TypeaheadField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; +import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; +import { DataQuery } from 'app/types'; const HISTOGRAM_GROUP = '__histograms__'; const METRIC_MARK = 'metric'; @@ -87,13 +88,13 @@ interface CascaderOption { interface PromQueryFieldProps { datasource: any; error?: string | JSX.Element; + initialQuery: DataQuery; hint?: any; history?: any[]; - initialQuery?: string | null; metricsByPrefix?: CascaderOption[]; onClickHintFix?: (action: any) => void; onPressEnter?: () => void; - onQueryChange?: (value: string, override?: boolean) => void; + onQueryChange?: (value: DataQuery, override?: boolean) => void; } interface PromQueryFieldState { @@ -163,9 +164,13 @@ class PromQueryField extends React.PureComponent { // Send text change to parent - const { onQueryChange } = this.props; + const { initialQuery, onQueryChange } = this.props; if (onQueryChange) { - onQueryChange(value, override); + const query: DataQuery = { + ...initialQuery, + expr: value, + }; + onQueryChange(query, override); } }; @@ -230,7 +235,7 @@ class PromQueryField extends React.PureComponent @@ -242,10 +247,10 @@ class PromQueryField extends React.PureComponent
    - {
    - {active === 'start' && } + {active === 'start' && }
    ); diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 8514b6ca7d4..2c7c62d881b 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -10,6 +10,9 @@ import { BackendSrv } from 'app/core/services/backend_srv'; import addLabelToQuery from './add_label_to_query'; import { getQueryHints } from './query_hints'; +import { expandRecordingRules } from './language_utils'; +import { DataQuery } from 'app/types'; +import { ExploreUrlState } from 'app/types/explore'; export function alignRange(start, end, step) { const alignedEnd = Math.ceil(end / step) * step; @@ -418,24 +421,23 @@ export class PrometheusDatasource { }); } - getExploreState(targets: any[]) { - let state = {}; - if (targets && targets.length > 0) { - const queries = targets.map(t => ({ - query: this.templateSrv.replace(t.expr, {}, this.interpolateQueryExpr), - format: t.format, + getExploreState(queries: DataQuery[]): Partial { + let state: Partial = { datasource: this.name }; + if (queries && queries.length > 0) { + const expandedQueries = queries.map(query => ({ + ...query, + expr: this.templateSrv.replace(query.expr, {}, this.interpolateQueryExpr), })); state = { ...state, - queries, - datasource: this.name, + queries: expandedQueries, }; } return state; } - getQueryHints(query: string, result: any[]) { - return getQueryHints(query, result, this); + getQueryHints(query: DataQuery, result: any[]) { + return getQueryHints(query.expr || '', result, this); } loadRules() { @@ -453,31 +455,35 @@ export class PrometheusDatasource { }); } - modifyQuery(query: string, action: any): string { + modifyQuery(query: DataQuery, action: any): DataQuery { + let expression = query.expr || ''; switch (action.type) { case 'ADD_FILTER': { - return addLabelToQuery(query, action.key, action.value); + expression = addLabelToQuery(expression, action.key, action.value); + break; } case 'ADD_HISTOGRAM_QUANTILE': { - return `histogram_quantile(0.95, sum(rate(${query}[5m])) by (le))`; + expression = `histogram_quantile(0.95, sum(rate(${expression}[5m])) by (le))`; + break; } case 'ADD_RATE': { - return `rate(${query}[5m])`; + expression = `rate(${expression}[5m])`; + break; } case 'ADD_SUM': { - return `sum(${query.trim()}) by ($1)`; + expression = `sum(${expression.trim()}) by ($1)`; + break; } case 'EXPAND_RULES': { - const mapping = action.mapping; - if (mapping) { - const ruleNames = Object.keys(mapping); - const rulesRegex = new RegExp(`(\\s|^)(${ruleNames.join('|')})(\\s|$|\\()`, 'ig'); - return query.replace(rulesRegex, (match, pre, name, post) => mapping[name]); + if (action.mapping) { + expression = expandRecordingRules(expression, action.mapping); } + break; } default: - return query; + break; } + return { ...query, expr: expression }; } getPrometheusTime(date, roundUp) { diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index ac54b08526d..277ab267d70 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -9,8 +9,8 @@ import { TypeaheadOutput, } from 'app/types/explore'; -import { parseSelector, processLabels, RATE_RANGES } from './language_utils'; -import PromqlSyntax, { FUNCTIONS } from './promql'; +import { parseSelector, processLabels } from './language_utils'; +import PromqlSyntax, { FUNCTIONS, RATE_RANGES } from './promql'; const DEFAULT_KEYS = ['job', 'instance']; const EMPTY_SELECTOR = '{}'; @@ -78,9 +78,24 @@ export default class PromQlLanguageProvider extends LanguageProvider { }; // Keep this DOM-free for testing - provideCompletionItems({ prefix, wrapperClasses, text }: TypeaheadInput, context?: any): TypeaheadOutput { + provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput { + // Local text properties + const empty = value.document.text.length === 0; + const selectedLines = value.document.getTextsAtRangeAsArray(value.selection); + const currentLine = selectedLines.length === 1 ? selectedLines[0] : null; + const nextCharacter = currentLine ? currentLine.text[value.selection.anchorOffset] : null; + // Syntax spans have 3 classes by default. More indicate a recognized token const tokenRecognized = wrapperClasses.length > 3; + // Non-empty prefix, but not inside known token + const prefixUnrecognized = prefix && !tokenRecognized; + // Prevent suggestions in `function(|suffix)` + const noSuffix = !nextCharacter || nextCharacter === ')'; + // Empty prefix is safe if it does not immediately folllow a complete expression and has no text after it + const safeEmptyPrefix = prefix === '' && !text.match(/^[\]})\s]+$/) && noSuffix; + // About to type next operand if preceded by binary operator + const isNextOperand = text.match(/[+\-*/^%]/); + // Determine candidates by CSS context if (_.includes(wrapperClasses, 'context-range')) { // Suggestions for metric[|] @@ -89,14 +104,14 @@ export default class PromQlLanguageProvider extends LanguageProvider { // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|} return this.getLabelCompletionItems.apply(this, arguments); } else if (_.includes(wrapperClasses, 'context-aggregation')) { + // Suggestions for sum(metric) by (|) return this.getAggregationCompletionItems.apply(this, arguments); - } else if ( - // Show default suggestions in a couple of scenarios - (prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token - (prefix === '' && !text.match(/^[\]})\s]+$/)) || // Empty prefix, but not following a closing brace - text.match(/[+\-*/^%]/) // Anything after binary operator - ) { + } else if (empty) { + // Suggestions for empty query field return this.getEmptyCompletionItems(context || {}); + } else if (prefixUnrecognized || safeEmptyPrefix || isNextOperand) { + // Show term suggestions in a couple of scenarios + return this.getTermCompletionItems(); } return { @@ -106,14 +121,14 @@ export default class PromQlLanguageProvider extends LanguageProvider { getEmptyCompletionItems(context: any): TypeaheadOutput { const { history } = context; - const { metrics } = this; - const suggestions: CompletionItemGroup[] = []; + let suggestions: CompletionItemGroup[] = []; if (history && history.length > 0) { const historyItems = _.chain(history) - .uniqBy('query') + .map(h => h.query.expr) + .filter() + .uniq() .take(HISTORY_ITEM_COUNT) - .map(h => h.query) .map(wrapLabel) .map(item => addHistoryMetadata(item, history)) .value(); @@ -126,13 +141,23 @@ export default class PromQlLanguageProvider extends LanguageProvider { }); } + const termCompletionItems = this.getTermCompletionItems(); + suggestions = [...suggestions, ...termCompletionItems.suggestions]; + + return { suggestions }; + } + + getTermCompletionItems(): TypeaheadOutput { + const { metrics } = this; + const suggestions: CompletionItemGroup[] = []; + suggestions.push({ prefixMatch: true, label: 'Functions', items: FUNCTIONS.map(setFunctionKind), }); - if (metrics) { + if (metrics && metrics.length > 0) { suggestions.push({ label: 'Metrics', items: metrics.map(wrapLabel), @@ -147,7 +172,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { suggestions: [ { label: 'Range vector', - items: [...RATE_RANGES].map(wrapLabel), + items: [...RATE_RANGES], }, ], }; diff --git a/public/app/plugins/datasource/prometheus/language_utils.ts b/public/app/plugins/datasource/prometheus/language_utils.ts index 5995c427cd1..6332da26149 100644 --- a/public/app/plugins/datasource/prometheus/language_utils.ts +++ b/public/app/plugins/datasource/prometheus/language_utils.ts @@ -24,8 +24,8 @@ export function processLabels(labels, withName = false) { } // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/; -const selectorRegexp = /\{[^}]*?\}/; -const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g; +export const selectorRegexp = /\{[^}]*?\}/; +export const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g; export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } { if (!query.match(selectorRegexp)) { // Special matcher for metrics @@ -83,3 +83,9 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any return { labelKeys, selector: selectorString }; } + +export function expandRecordingRules(query: string, mapping: { [name: string]: string }): string { + const ruleNames = Object.keys(mapping); + const rulesRegex = new RegExp(`(\\s|^)(${ruleNames.join('|')})(\\s|$|\\(|\\[|\\{)`, 'ig'); + return query.replace(rulesRegex, (match, pre, name, post) => `${pre}${mapping[name]}${post}`); +} diff --git a/public/app/plugins/datasource/prometheus/promql.ts b/public/app/plugins/datasource/prometheus/promql.ts index 065e6bb08cc..41463877a8f 100644 --- a/public/app/plugins/datasource/prometheus/promql.ts +++ b/public/app/plugins/datasource/prometheus/promql.ts @@ -1,8 +1,19 @@ /* tslint:disable max-line-length */ +import { CompletionItem } from 'app/types/explore'; + +export const RATE_RANGES: CompletionItem[] = [ + { label: '1m', sortText: '00:01:00' }, + { label: '5m', sortText: '00:05:00' }, + { label: '10m', sortText: '00:10:00' }, + { label: '30m', sortText: '00:30:00' }, + { label: '1h', sortText: '01:00:00' }, + { label: '1d', sortText: '24:00:00' }, +]; + export const OPERATORS = ['by', 'group_left', 'group_right', 'ignoring', 'on', 'offset', 'without']; -const AGGREGATION_OPERATORS = [ +const AGGREGATION_OPERATORS: CompletionItem[] = [ { label: 'sum', insertText: 'sum', diff --git a/public/app/plugins/datasource/prometheus/query_hints.ts b/public/app/plugins/datasource/prometheus/query_hints.ts index ea505832468..ce019e3ddcb 100644 --- a/public/app/plugins/datasource/prometheus/query_hints.ts +++ b/public/app/plugins/datasource/prometheus/query_hints.ts @@ -96,7 +96,7 @@ export function getQueryHints(query: string, series?: any[], datasource?: any): } } - if (series.length >= SUM_HINT_THRESHOLD_COUNT) { + if (series && series.length >= SUM_HINT_THRESHOLD_COUNT) { const simpleMetric = query.trim().match(/^\w+$/); if (simpleMetric) { hints.push({ diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts index 784a8b59739..1d114a7ef28 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -7,23 +7,85 @@ describe('Language completion provider', () => { metadataRequest: () => ({ data: { data: [] } }), }; - it('returns default suggestions on emtpty context', () => { - const instance = new LanguageProvider(datasource); - const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] }); - expect(result.context).toBeUndefined(); - expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); + describe('empty query suggestions', () => { + it('returns default suggestions on emtpty context', () => { + const instance = new LanguageProvider(datasource); + const value = Plain.deserialize(''); + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + ]); + }); + + it('returns default suggestions with metrics on emtpty context when metrics were provided', () => { + const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); + const value = Plain.deserialize(''); + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + { + label: 'Metrics', + }, + ]); + }); + + it('returns default suggestions with history on emtpty context when history was provided', () => { + const instance = new LanguageProvider(datasource); + const value = Plain.deserialize(''); + const history = [ + { + query: { refId: '1', expr: 'metric' }, + }, + ]; + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }, { history }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toMatchObject([ + { + label: 'History', + items: [ + { + label: 'metric', + }, + ], + }, + { + label: 'Functions', + }, + ]); + }); }); describe('range suggestions', () => { it('returns range suggestions in range context', () => { const instance = new LanguageProvider(datasource); - const result = instance.provideCompletionItems({ text: '1', prefix: '1', wrapperClasses: ['context-range'] }); + const value = Plain.deserialize('1'); + const result = instance.provideCompletionItems({ + text: '1', + prefix: '1', + value, + wrapperClasses: ['context-range'], + }); expect(result.context).toBe('context-range'); expect(result.refresher).toBeUndefined(); - expect(result.suggestions).toEqual([ + expect(result.suggestions).toMatchObject([ { - items: [{ label: '1m' }, { label: '5m' }, { label: '10m' }, { label: '30m' }, { label: '1h' }], + items: [ + { label: '1m' }, + { label: '5m' }, + { label: '10m' }, + { label: '30m' }, + { label: '1h' }, + { label: '1d' }, + ], label: 'Range vector', }, ]); @@ -31,20 +93,54 @@ describe('Language completion provider', () => { }); describe('metric suggestions', () => { - it('returns metrics suggestions by default', () => { + it('returns metrics and function suggestions in an unknown context', () => { const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); - const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', wrapperClasses: [] }); + const value = Plain.deserialize('a'); + const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + { + label: 'Metrics', + }, + ]); }); - it('returns default suggestions after a binary operator', () => { + it('returns metrics and function suggestions after a binary operator', () => { const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); - const result = instance.provideCompletionItems({ text: '*', prefix: '', wrapperClasses: [] }); + const value = Plain.deserialize('*'); + const result = instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + { + label: 'Metrics', + }, + ]); + }); + + it('returns no suggestions at the beginning of a non-empty function', () => { + const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); + const value = Plain.deserialize('sum(up)'); + const range = value.selection.merge({ + anchorOffset: 4, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + value: valueWithSelection, + wrapperClasses: [], + }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(0); }); }); diff --git a/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts b/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts index 748217e21b7..b33c0094700 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts @@ -1,4 +1,4 @@ -import { parseSelector } from '../language_utils'; +import { expandRecordingRules, parseSelector } from '../language_utils'; describe('parseSelector()', () => { let parsed; @@ -62,3 +62,25 @@ describe('parseSelector()', () => { expect(parsed.selector).toBe('{__name__="bar:metric:1m"}'); }); }); + +describe('expandRecordingRules()', () => { + it('returns query w/o recording rules as is', () => { + expect(expandRecordingRules('metric', {})).toBe('metric'); + expect(expandRecordingRules('metric + metric', {})).toBe('metric + metric'); + expect(expandRecordingRules('metric{}', {})).toBe('metric{}'); + }); + + it('does not modify recording rules name in label values', () => { + expect(expandRecordingRules('{__name__="metric"} + bar', { metric: 'foo', bar: 'super' })).toBe( + '{__name__="metric"} + super' + ); + }); + + it('returns query with expanded recording rules', () => { + expect(expandRecordingRules('metric', { metric: 'foo' })).toBe('foo'); + expect(expandRecordingRules('metric + metric', { metric: 'foo' })).toBe('foo + foo'); + expect(expandRecordingRules('metric{}', { metric: 'foo' })).toBe('foo{}'); + expect(expandRecordingRules('metric[]', { metric: 'foo' })).toBe('foo[]'); + expect(expandRecordingRules('metric + foo', { metric: 'foo', foo: 'bar' })).toBe('foo + bar'); + }); +}); diff --git a/public/app/plugins/datasource/stackdriver/StackdriverMetricFindQuery.ts b/public/app/plugins/datasource/stackdriver/StackdriverMetricFindQuery.ts new file mode 100644 index 00000000000..f8fc2e796ce --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/StackdriverMetricFindQuery.ts @@ -0,0 +1,129 @@ +import isString from 'lodash/isString'; +import { alignmentPeriods } from './constants'; +import { MetricFindQueryTypes } from './types'; +import { + getMetricTypesByService, + getAlignmentOptionsByMetric, + getAggregationOptionsByMetric, + extractServicesFromMetricDescriptors, + getLabelKeys, +} from './functions'; + +export default class StackdriverMetricFindQuery { + constructor(private datasource) {} + + async execute(query: any) { + try { + switch (query.selectedQueryType) { + case MetricFindQueryTypes.Services: + return this.handleServiceQuery(); + case MetricFindQueryTypes.MetricTypes: + return this.handleMetricTypesQuery(query); + case MetricFindQueryTypes.LabelKeys: + return this.handleLabelKeysQuery(query); + case MetricFindQueryTypes.LabelValues: + return this.handleLabelValuesQuery(query); + case MetricFindQueryTypes.ResourceTypes: + return this.handleResourceTypeQuery(query); + case MetricFindQueryTypes.Aligners: + return this.handleAlignersQuery(query); + case MetricFindQueryTypes.AlignmentPeriods: + return this.handleAlignmentPeriodQuery(); + case MetricFindQueryTypes.Aggregations: + return this.handleAggregationQuery(query); + default: + return []; + } + } catch (error) { + console.error(`Could not run StackdriverMetricFindQuery ${query}`, error); + return []; + } + } + + async handleServiceQuery() { + const metricDescriptors = await this.datasource.getMetricTypes(this.datasource.projectName); + const services = extractServicesFromMetricDescriptors(metricDescriptors); + return services.map(s => ({ + text: s.serviceShortName, + value: s.service, + expandable: true, + })); + } + + async handleMetricTypesQuery({ selectedService }) { + if (!selectedService) { + return []; + } + const metricDescriptors = await this.datasource.getMetricTypes(this.datasource.projectName); + return getMetricTypesByService(metricDescriptors, this.datasource.templateSrv.replace(selectedService)).map(s => ({ + text: s.displayName, + value: s.type, + expandable: true, + })); + } + + async handleLabelKeysQuery({ selectedMetricType }) { + if (!selectedMetricType) { + return []; + } + const labelKeys = await getLabelKeys(this.datasource, selectedMetricType); + return labelKeys.map(this.toFindQueryResult); + } + + async handleLabelValuesQuery({ selectedMetricType, labelKey }) { + if (!selectedMetricType) { + return []; + } + const refId = 'handleLabelValuesQuery'; + const response = await this.datasource.getLabels(selectedMetricType, refId); + const interpolatedKey = this.datasource.templateSrv.replace(labelKey); + const [name] = interpolatedKey.split('.').reverse(); + let values = []; + if (response.meta && response.meta.metricLabels && response.meta.metricLabels.hasOwnProperty(name)) { + values = response.meta.metricLabels[name]; + } else if (response.meta && response.meta.resourceLabels && response.meta.resourceLabels.hasOwnProperty(name)) { + values = response.meta.resourceLabels[name]; + } + + return values.map(this.toFindQueryResult); + } + + async handleResourceTypeQuery({ selectedMetricType }) { + if (!selectedMetricType) { + return []; + } + const refId = 'handleResourceTypeQueryQueryType'; + const response = await this.datasource.getLabels(selectedMetricType, refId); + return response.meta.resourceTypes ? response.meta.resourceTypes.map(this.toFindQueryResult) : []; + } + + async handleAlignersQuery({ selectedMetricType }) { + if (!selectedMetricType) { + return []; + } + const metricDescriptors = await this.datasource.getMetricTypes(this.datasource.projectName); + const { valueType, metricKind } = metricDescriptors.find( + m => m.type === this.datasource.templateSrv.replace(selectedMetricType) + ); + return getAlignmentOptionsByMetric(valueType, metricKind).map(this.toFindQueryResult); + } + + async handleAggregationQuery({ selectedMetricType }) { + if (!selectedMetricType) { + return []; + } + const metricDescriptors = await this.datasource.getMetricTypes(this.datasource.projectName); + const { valueType, metricKind } = metricDescriptors.find( + m => m.type === this.datasource.templateSrv.replace(selectedMetricType) + ); + return getAggregationOptionsByMetric(valueType, metricKind).map(this.toFindQueryResult); + } + + handleAlignmentPeriodQuery() { + return alignmentPeriods.map(this.toFindQueryResult); + } + + toFindQueryResult(x) { + return isString(x) ? { text: x, expandable: true } : { ...x, expandable: true }; + } +} diff --git a/public/app/plugins/datasource/stackdriver/components/SimpleSelect.tsx b/public/app/plugins/datasource/stackdriver/components/SimpleSelect.tsx new file mode 100644 index 00000000000..3a4a0707a2c --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/components/SimpleSelect.tsx @@ -0,0 +1,28 @@ +import React, { SFC } from 'react'; + +interface Props { + onValueChange: (e) => void; + options: any[]; + value: string; + label: string; +} + +const SimpleSelect: SFC = props => { + const { label, onValueChange, value, options } = props; + return ( +
    + {label} +
    + +
    +
    + ); +}; + +export default SimpleSelect; diff --git a/public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.test.tsx new file mode 100644 index 00000000000..0f31d25ee4e --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.test.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { StackdriverVariableQueryEditor } from './VariableQueryEditor'; +import { VariableQueryProps } from 'app/types/plugins'; +import { MetricFindQueryTypes } from '../types'; + +jest.mock('../functions', () => ({ + getMetricTypes: () => ({ metricTypes: [], selectedMetricType: '' }), + extractServicesFromMetricDescriptors: () => [], +})); + +const props: VariableQueryProps = { + onChange: (query, definition) => {}, + query: {}, + datasource: { + getMetricTypes: async p => [], + }, + templateSrv: { replace: s => s, variables: [] }, +}; + +describe('VariableQueryEditor', () => { + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + describe('and a new variable is created', () => { + it('should trigger a query using the first query type in the array', done => { + props.onChange = (query, definition) => { + expect(definition).toBe('Stackdriver - Services'); + done(); + }; + renderer.create().toJSON(); + }); + }); + + describe('and an existing variable is edited', () => { + it('should trigger new query using the saved query type', done => { + props.query = { selectedQueryType: MetricFindQueryTypes.LabelKeys }; + props.onChange = (query, definition) => { + expect(definition).toBe('Stackdriver - Label Keys'); + done(); + }; + renderer.create().toJSON(); + }); + }); +}); diff --git a/public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx b/public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx new file mode 100644 index 00000000000..1f349ccab59 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx @@ -0,0 +1,196 @@ +import React, { PureComponent } from 'react'; +import { VariableQueryProps } from 'app/types/plugins'; +import SimpleSelect from './SimpleSelect'; +import { getMetricTypes, getLabelKeys, extractServicesFromMetricDescriptors } from '../functions'; +import { MetricFindQueryTypes, VariableQueryData } from '../types'; + +export class StackdriverVariableQueryEditor extends PureComponent { + queryTypes: Array<{ value: string; name: string }> = [ + { value: MetricFindQueryTypes.Services, name: 'Services' }, + { value: MetricFindQueryTypes.MetricTypes, name: 'Metric Types' }, + { value: MetricFindQueryTypes.LabelKeys, name: 'Label Keys' }, + { value: MetricFindQueryTypes.LabelValues, name: 'Label Values' }, + { value: MetricFindQueryTypes.ResourceTypes, name: 'Resource Types' }, + { value: MetricFindQueryTypes.Aggregations, name: 'Aggregations' }, + { value: MetricFindQueryTypes.Aligners, name: 'Aligners' }, + { value: MetricFindQueryTypes.AlignmentPeriods, name: 'Alignment Periods' }, + ]; + + defaults: VariableQueryData = { + selectedQueryType: this.queryTypes[0].value, + metricDescriptors: [], + selectedService: '', + selectedMetricType: '', + labels: [], + labelKey: '', + metricTypes: [], + services: [], + }; + + constructor(props: VariableQueryProps) { + super(props); + this.state = Object.assign(this.defaults, this.props.query); + } + + async componentDidMount() { + const metricDescriptors = await this.props.datasource.getMetricTypes(this.props.datasource.projectName); + const services = extractServicesFromMetricDescriptors(metricDescriptors).map(m => ({ + value: m.service, + name: m.serviceShortName, + })); + + let selectedService = ''; + if (services.some(s => s.value === this.props.templateSrv.replace(this.state.selectedService))) { + selectedService = this.state.selectedService; + } else if (services && services.length > 0) { + selectedService = services[0].value; + } + + const { metricTypes, selectedMetricType } = getMetricTypes( + metricDescriptors, + this.state.selectedMetricType, + this.props.templateSrv.replace(this.state.selectedMetricType), + this.props.templateSrv.replace(selectedService) + ); + const state: any = { + services, + selectedService, + metricTypes, + selectedMetricType, + metricDescriptors, + ...await this.getLabels(selectedMetricType), + }; + this.setState(state); + } + + async handleQueryTypeChange(event) { + const state: any = { + selectedQueryType: event.target.value, + ...await this.getLabels(this.state.selectedMetricType, event.target.value), + }; + this.setState(state); + } + + async onServiceChange(event) { + const { metricTypes, selectedMetricType } = getMetricTypes( + this.state.metricDescriptors, + this.state.selectedMetricType, + this.props.templateSrv.replace(this.state.selectedMetricType), + this.props.templateSrv.replace(event.target.value) + ); + const state: any = { + selectedService: event.target.value, + metricTypes, + selectedMetricType, + ...await this.getLabels(selectedMetricType), + }; + this.setState(state); + } + + async onMetricTypeChange(event) { + const state: any = { selectedMetricType: event.target.value, ...await this.getLabels(event.target.value) }; + this.setState(state); + } + + onLabelKeyChange(event) { + this.setState({ labelKey: event.target.value }); + } + + componentDidUpdate() { + const { metricDescriptors, labels, metricTypes, services, ...queryModel } = this.state; + const query = this.queryTypes.find(q => q.value === this.state.selectedQueryType); + this.props.onChange(queryModel, `Stackdriver - ${query.name}`); + } + + async getLabels(selectedMetricType, selectedQueryType = this.state.selectedQueryType) { + let result = { labels: this.state.labels, labelKey: this.state.labelKey }; + if (selectedMetricType && selectedQueryType === MetricFindQueryTypes.LabelValues) { + const labels = await getLabelKeys(this.props.datasource, selectedMetricType); + const labelKey = labels.some(l => l === this.props.templateSrv.replace(this.state.labelKey)) + ? this.state.labelKey + : labels[0]; + result = { labels, labelKey }; + } + return result; + } + + insertTemplateVariables(options) { + const templateVariables = this.props.templateSrv.variables.map(v => ({ name: `$${v.name}`, value: `$${v.name}` })); + return [...templateVariables, ...options]; + } + + renderQueryTypeSwitch(queryType) { + switch (queryType) { + case MetricFindQueryTypes.MetricTypes: + return ( + this.onServiceChange(e)} + label="Service" + /> + ); + case MetricFindQueryTypes.LabelKeys: + case MetricFindQueryTypes.LabelValues: + case MetricFindQueryTypes.ResourceTypes: + return ( + + this.onServiceChange(e)} + label="Service" + /> + this.onMetricTypeChange(e)} + label="Metric Type" + /> + {queryType === MetricFindQueryTypes.LabelValues && ( + ({ value: l, name: l })))} + onValueChange={e => this.onLabelKeyChange(e)} + label="Label Key" + /> + )} + + ); + case MetricFindQueryTypes.Aligners: + case MetricFindQueryTypes.Aggregations: + return ( + + this.onServiceChange(e)} + label="Service" + /> + this.onMetricTypeChange(e)} + label="Metric Type" + /> + + ); + default: + return ''; + } + } + + render() { + return ( + + this.handleQueryTypeChange(e)} + label="Query Type" + /> + {this.renderQueryTypeSwitch(this.state.selectedQueryType)} + + ); + } +} diff --git a/public/app/plugins/datasource/stackdriver/components/__snapshots__/VariableQueryEditor.test.tsx.snap b/public/app/plugins/datasource/stackdriver/components/__snapshots__/VariableQueryEditor.test.tsx.snap new file mode 100644 index 00000000000..e4ad23bc8e7 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/components/__snapshots__/VariableQueryEditor.test.tsx.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VariableQueryEditor renders correctly 1`] = ` +Array [ +
    + + Query Type + +
    + +
    +
    , + "", +] +`; diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 034333cbb86..d1545655652 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -1,6 +1,7 @@ import { stackdriverUnitMappings } from './constants'; import appEvents from 'app/core/app_events'; import _ from 'lodash'; +import StackdriverMetricFindQuery from './StackdriverMetricFindQuery'; export default class StackdriverDatasource { id: number; @@ -9,6 +10,7 @@ export default class StackdriverDatasource { projectName: string; authenticationType: string; queryPromise: Promise; + metricTypes: any[]; /** @ngInject */ constructor(instanceSettings, private backendSrv, private templateSrv, private timeSrv) { @@ -18,6 +20,7 @@ export default class StackdriverDatasource { this.id = instanceSettings.id; this.projectName = instanceSettings.jsonData.defaultProject || ''; this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt'; + this.metricTypes = []; } async getTimeSeries(options) { @@ -67,7 +70,7 @@ export default class StackdriverDatasource { } async getLabels(metricType, refId) { - return await this.getTimeSeries({ + const response = await this.getTimeSeries({ targets: [ { refId: refId, @@ -81,6 +84,8 @@ export default class StackdriverDatasource { ], range: this.timeSrv.timeRange(), }); + + return response.results[refId]; } interpolateGroupBys(groupBys: string[], scopedVars): string[] { @@ -177,8 +182,9 @@ export default class StackdriverDatasource { return results; } - metricFindQuery(query) { - throw new Error('Template variables support is not yet imlemented'); + async metricFindQuery(query) { + const stackdriverMetricFindQuery = new StackdriverMetricFindQuery(this); + return stackdriverMetricFindQuery.execute(query); } async testDatasource() { @@ -258,19 +264,21 @@ export default class StackdriverDatasource { async getMetricTypes(projectName: string) { try { - const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`; - const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`); + if (this.metricTypes.length === 0) { + const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`; + const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`); - const metrics = data.metricDescriptors.map(m => { - const [service] = m.type.split('/'); - const [serviceShortName] = service.split('.'); - m.service = service; - m.serviceShortName = serviceShortName; - m.displayName = m.displayName || m.type; - return m; - }); + this.metricTypes = data.metricDescriptors.map(m => { + const [service] = m.type.split('/'); + const [serviceShortName] = service.split('.'); + m.service = service; + m.serviceShortName = serviceShortName; + m.displayName = m.displayName || m.type; + return m; + }); + } - return metrics; + return this.metricTypes; } catch (error) { appEvents.emit('ds-request-error', this.formatStackdriverError(error)); return []; diff --git a/public/app/plugins/datasource/stackdriver/functions.ts b/public/app/plugins/datasource/stackdriver/functions.ts new file mode 100644 index 00000000000..e39a7d42508 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/functions.ts @@ -0,0 +1,48 @@ +import uniqBy from 'lodash/uniqBy'; +import { alignOptions, aggOptions } from './constants'; + +export const extractServicesFromMetricDescriptors = metricDescriptors => uniqBy(metricDescriptors, 'service'); + +export const getMetricTypesByService = (metricDescriptors, service) => + metricDescriptors.filter(m => m.service === service); + +export const getMetricTypes = (metricDescriptors, metricType, interpolatedMetricType, selectedService) => { + const metricTypes = getMetricTypesByService(metricDescriptors, selectedService).map(m => ({ + value: m.type, + name: m.displayName, + })); + const metricTypeExistInArray = metricTypes.some(m => m.value === interpolatedMetricType); + const selectedMetricType = metricTypeExistInArray ? metricType : metricTypes[0].value; + return { + metricTypes, + selectedMetricType, + }; +}; + +export const getAlignmentOptionsByMetric = (metricValueType, metricKind) => { + return !metricValueType + ? [] + : alignOptions.filter(i => { + return i.valueTypes.indexOf(metricValueType) !== -1 && i.metricKinds.indexOf(metricKind) !== -1; + }); +}; + +export const getAggregationOptionsByMetric = (valueType, metricKind) => { + return !metricKind + ? [] + : aggOptions.filter(i => { + return i.valueTypes.indexOf(valueType) !== -1 && i.metricKinds.indexOf(metricKind) !== -1; + }); +}; + +export const getLabelKeys = async (datasource, selectedMetricType) => { + const refId = 'handleLabelKeysQuery'; + const response = await datasource.getLabels(selectedMetricType, refId); + const labelKeys = response.meta + ? [ + ...Object.keys(response.meta.resourceLabels).map(l => `resource.label.${l}`), + ...Object.keys(response.meta.metricLabels).map(l => `metric.label.${l}`), + ] + : []; + return labelKeys; +}; diff --git a/public/app/plugins/datasource/stackdriver/module.ts b/public/app/plugins/datasource/stackdriver/module.ts index 183c5c9ff88..1b81d29af73 100644 --- a/public/app/plugins/datasource/stackdriver/module.ts +++ b/public/app/plugins/datasource/stackdriver/module.ts @@ -2,10 +2,12 @@ import StackdriverDatasource from './datasource'; import { StackdriverQueryCtrl } from './query_ctrl'; import { StackdriverConfigCtrl } from './config_ctrl'; import { StackdriverAnnotationsQueryCtrl } from './annotations_query_ctrl'; +import { StackdriverVariableQueryEditor } from './components/VariableQueryEditor'; export { StackdriverDatasource as Datasource, StackdriverQueryCtrl as QueryCtrl, StackdriverConfigCtrl as ConfigCtrl, StackdriverAnnotationsQueryCtrl as AnnotationsQueryCtrl, + StackdriverVariableQueryEditor as VariableQueryEditor, }; diff --git a/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html b/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html index 379b9a36dc3..5f16d3fc61d 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.aggregation.html @@ -2,8 +2,8 @@
    - +
    @@ -20,8 +20,8 @@
    - +
    @@ -33,8 +33,8 @@
    - +
    diff --git a/public/app/plugins/datasource/stackdriver/partials/query.editor.html b/public/app/plugins/datasource/stackdriver/partials/query.editor.html index 98c8fcc83e8..5c7bc8935b1 100755 --- a/public/app/plugins/datasource/stackdriver/partials/query.editor.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.editor.html @@ -14,7 +14,7 @@
    - Project + Project
    @@ -70,4 +70,4 @@
    {{ctrl.lastQueryError}}
    - + \ No newline at end of file diff --git a/public/app/plugins/datasource/stackdriver/partials/query.filter.html b/public/app/plugins/datasource/stackdriver/partials/query.filter.html index 5043161c492..b96b0720e33 100644 --- a/public/app/plugins/datasource/stackdriver/partials/query.filter.html +++ b/public/app/plugins/datasource/stackdriver/partials/query.filter.html @@ -1,37 +1,52 @@
    - Service - + Service +
    - Metric - -
    -
    -
    + Metric +
    +
    Filter
    - +
    -
    -
    -
    +
    Group By
    - +
    -
    -
    -
    +
    diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 6cd6c805463..628cc494242 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -1,6 +1,7 @@ import coreModule from 'app/core/core_module'; import _ from 'lodash'; import * as options from './constants'; +import { getAlignmentOptionsByMetric, getAggregationOptionsByMetric } from './functions'; import kbn from 'app/core/utils/kbn'; export class StackdriverAggregation { @@ -25,7 +26,7 @@ export class StackdriverAggregationCtrl { target: any; /** @ngInject */ - constructor(private $scope) { + constructor(private $scope, private templateSrv) { this.$scope.ctrl = this; this.target = $scope.target; this.alignmentPeriods = options.alignmentPeriods; @@ -41,28 +42,16 @@ export class StackdriverAggregationCtrl { } setAlignOptions() { - this.alignOptions = !this.target.valueType - ? [] - : options.alignOptions.filter(i => { - return ( - i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 - ); - }); - if (!this.alignOptions.find(o => o.value === this.target.aggregation.perSeriesAligner)) { + this.alignOptions = getAlignmentOptionsByMetric(this.target.valueType, this.target.metricKind); + if (!this.alignOptions.find(o => o.value === this.templateSrv.replace(this.target.aggregation.perSeriesAligner))) { this.target.aggregation.perSeriesAligner = this.alignOptions.length > 0 ? this.alignOptions[0].value : ''; } } setAggOptions() { - this.aggOptions = !this.target.metricKind - ? [] - : options.aggOptions.filter(i => { - return ( - i.valueTypes.indexOf(this.target.valueType) !== -1 && i.metricKinds.indexOf(this.target.metricKind) !== -1 - ); - }); + this.aggOptions = getAggregationOptionsByMetric(this.target.valueType, this.target.metricKind); - if (!this.aggOptions.find(o => o.value === this.target.aggregation.crossSeriesReducer)) { + if (!this.aggOptions.find(o => o.value === this.templateSrv.replace(this.target.aggregation.crossSeriesReducer))) { this.deselectAggregationOption('REDUCE_NONE'); } @@ -73,8 +62,12 @@ export class StackdriverAggregationCtrl { } formatAlignmentText() { - const selectedAlignment = this.alignOptions.find(ap => ap.value === this.target.aggregation.perSeriesAligner); - return `${kbn.secondsToHms(this.$scope.alignmentPeriod)} interval (${selectedAlignment.text})`; + const selectedAlignment = this.alignOptions.find( + ap => ap.value === this.templateSrv.replace(this.target.aggregation.perSeriesAligner) + ); + return `${kbn.secondsToHms(this.$scope.alignmentPeriod)} interval (${ + selectedAlignment ? selectedAlignment.text : '' + })`; } deselectAggregationOption(notValidOptionValue: string) { diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index 3a1961eb14e..c2607964456 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -62,7 +62,6 @@ export class StackdriverQueryCtrl extends QueryCtrl { constructor($scope, $injector) { super($scope, $injector); _.defaultsDeep(this.target, this.defaults); - this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); } diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index 4c383e5d09e..0f5dce559fd 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -139,7 +139,7 @@ export class StackdriverFilterCtrl { result = metrics.filter(m => m.service === this.target.service); } - if (result.find(m => m.value === this.target.metricType)) { + if (result.find(m => m.value === this.templateSrv.replace(this.target.metricType))) { this.metricType = this.target.metricType; } else if (result.length > 0) { this.metricType = this.target.metricType = result[0].value; @@ -150,10 +150,10 @@ export class StackdriverFilterCtrl { async getLabels() { this.loadLabelsPromise = new Promise(async resolve => { try { - const data = await this.datasource.getLabels(this.target.metricType, this.target.refId); - this.metricLabels = data.results[this.target.refId].meta.metricLabels; - this.resourceLabels = data.results[this.target.refId].meta.resourceLabels; - this.resourceTypes = data.results[this.target.refId].meta.resourceTypes; + const { meta } = await this.datasource.getLabels(this.target.metricType, this.target.refId); + this.metricLabels = meta.metricLabels; + this.resourceLabels = meta.resourceLabels; + this.resourceTypes = meta.resourceTypes; resolve(); } catch (error) { if (error.data && error.data.message) { @@ -187,7 +187,9 @@ export class StackdriverFilterCtrl { setMetricType() { this.target.metricType = this.metricType; - const { valueType, metricKind, unit } = this.metricDescriptors.find(m => m.type === this.target.metricType); + const { valueType, metricKind, unit } = this.metricDescriptors.find( + m => m.type === this.templateSrv.replace(this.metricType) + ); this.target.unit = unit; this.target.valueType = valueType; this.target.metricKind = metricKind; diff --git a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts index ac9ea2ac6bc..81011f5dfe0 100644 --- a/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts @@ -6,10 +6,19 @@ describe('StackdriverAggregationCtrl', () => { describe('when new query result is returned from the server', () => { describe('and result is double and gauge and no group by is used', () => { beforeEach(async () => { - ctrl = new StackdriverAggregationCtrl({ - $on: () => {}, - target: { valueType: 'DOUBLE', metricKind: 'GAUGE', aggregation: { crossSeriesReducer: '', groupBys: [] } }, - }); + ctrl = new StackdriverAggregationCtrl( + { + $on: () => {}, + target: { + valueType: 'DOUBLE', + metricKind: 'GAUGE', + aggregation: { crossSeriesReducer: '', groupBys: [] }, + }, + }, + { + replace: s => s, + } + ); }); it('should populate all aggregate options except two', () => { @@ -31,14 +40,19 @@ describe('StackdriverAggregationCtrl', () => { describe('and result is double and gauge and a group by is used', () => { beforeEach(async () => { - ctrl = new StackdriverAggregationCtrl({ - $on: () => {}, - target: { - valueType: 'DOUBLE', - metricKind: 'GAUGE', - aggregation: { crossSeriesReducer: 'REDUCE_NONE', groupBys: ['resource.label.projectid'] }, + ctrl = new StackdriverAggregationCtrl( + { + $on: () => {}, + target: { + valueType: 'DOUBLE', + metricKind: 'GAUGE', + aggregation: { crossSeriesReducer: 'REDUCE_NONE', groupBys: ['resource.label.projectid'] }, + }, }, - }); + { + replace: s => s, + } + ); }); it('should populate all aggregate options except three', () => { diff --git a/public/app/plugins/datasource/stackdriver/types.ts b/public/app/plugins/datasource/stackdriver/types.ts new file mode 100644 index 00000000000..df4c2886522 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/types.ts @@ -0,0 +1,21 @@ +export enum MetricFindQueryTypes { + Services = 'services', + MetricTypes = 'metricTypes', + LabelKeys = 'labelKeys', + LabelValues = 'labelValues', + ResourceTypes = 'resourceTypes', + Aggregations = 'aggregations', + Aligners = 'aligners', + AlignmentPeriods = 'alignmentPeriods', +} + +export interface VariableQueryData { + selectedQueryType: string; + metricDescriptors: any[]; + selectedService: string; + selectedMetricType: string; + labels: string[]; + labelKey: string; + metricTypes: Array<{ value: string; name: string }>; + services: Array<{ value: string; name: string }>; +} diff --git a/public/app/plugins/panel/alertlist/editor.html b/public/app/plugins/panel/alertlist/editor.html index c48b70e02c0..a05234cea3c 100644 --- a/public/app/plugins/panel/alertlist/editor.html +++ b/public/app/plugins/panel/alertlist/editor.html @@ -50,5 +50,6 @@ +
    diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx new file mode 100644 index 00000000000..48dc4d809f3 --- /dev/null +++ b/public/app/plugins/panel/gauge/module.tsx @@ -0,0 +1,23 @@ +import React, { PureComponent } from 'react'; +import Gauge from 'app/viz/Gauge'; +import { NullValueMode, PanelProps } from 'app/types'; +import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; + +export interface Options {} + +interface Props extends PanelProps {} + +export class GaugePanel extends PureComponent { + render() { + const { timeSeries } = this.props; + + const vmSeries = getTimeSeriesVMs({ + timeSeries: timeSeries, + nullValueMode: NullValueMode.Ignore, + }); + + return ; + } +} + +export { GaugePanel as PanelComponent }; diff --git a/public/app/plugins/panel/gauge/plugin.json b/public/app/plugins/panel/gauge/plugin.json new file mode 100644 index 00000000000..1fff76fdd3b --- /dev/null +++ b/public/app/plugins/panel/gauge/plugin.json @@ -0,0 +1,18 @@ +{ + "type": "panel", + "name": "Gauge", + "id": "gauge", + + "state": "alpha", + + "info": { + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + + } + } +} + diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 01afd0716e6..ff248d68201 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -16,6 +16,7 @@ import { tickStep } from 'app/core/utils/ticks'; import { appEvents, coreModule, updateLegendValues } from 'app/core/core'; import GraphTooltip from './graph_tooltip'; import { ThresholdManager } from './threshold_manager'; +import { TimeRegionManager } from './time_region_manager'; import { EventManager } from 'app/features/annotations/all'; import { convertToHistogramData } from './histogram'; import { alignYLevel } from './align_yaxes'; @@ -38,6 +39,7 @@ class GraphElement { panelWidth: number; eventManager: EventManager; thresholdManager: ThresholdManager; + timeRegionManager: TimeRegionManager; legendElem: HTMLElement; constructor(private scope, private elem, private timeSrv) { @@ -49,21 +51,14 @@ class GraphElement { this.panelWidth = 0; this.eventManager = new EventManager(this.ctrl); this.thresholdManager = new ThresholdManager(this.ctrl); + this.timeRegionManager = new TimeRegionManager(this.ctrl); this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => { return this.sortedSeries; }); // panel events this.ctrl.events.on('panel-teardown', this.onPanelTeardown.bind(this)); - - /** - * Split graph rendering into two parts. - * First, calculate series stats in buildFlotPairs() function. Then legend rendering started - * (see ctrl.events.on('render') in legend.ts). - * When legend is rendered it emits 'legend-rendering-complete' and graph rendered. - */ this.ctrl.events.on('render', this.onRender.bind(this)); - this.ctrl.events.on('legend-rendering-complete', this.onLegendRenderingComplete.bind(this)); // global events appEvents.on('graph-hover', this.onGraphHover.bind(this), scope); @@ -82,11 +77,20 @@ class GraphElement { if (!this.data) { return; } + this.annotations = this.ctrl.annotations || []; this.buildFlotPairs(this.data); const graphHeight = this.elem.height(); updateLegendValues(this.data, this.panel, graphHeight); + if (!this.panel.legend.show) { + if (this.legendElem.hasChildNodes()) { + ReactDOM.unmountComponentAtNode(this.legendElem); + } + this.renderPanel(); + return; + } + const { values, min, max, avg, current, total } = this.panel.legend; const { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero } = this.panel.legend; const legendOptions = { alignAsTable, rightSide, sideWidth, sort, sortDesc, hideEmpty, hideZero }; @@ -101,12 +105,9 @@ class GraphElement { onColorChange: this.ctrl.onColorChange, onToggleAxis: this.ctrl.onToggleAxis, }; - const legendReactElem = React.createElement(Legend, legendProps); - ReactDOM.render(legendReactElem, this.legendElem, () => this.onLegendRenderingComplete()); - } - onLegendRenderingComplete() { - this.render_panel(); + const legendReactElem = React.createElement(Legend, legendProps); + ReactDOM.render(legendReactElem, this.legendElem, () => this.renderPanel()); } onGraphHover(evt) { @@ -125,6 +126,7 @@ class GraphElement { onPanelTeardown() { this.thresholdManager = null; + this.timeRegionManager = null; if (this.plot) { this.plot.destroy(); @@ -215,6 +217,7 @@ class GraphElement { } this.thresholdManager.draw(plot); + this.timeRegionManager.draw(plot); } processOffsetHook(plot, gridMargin) { @@ -276,7 +279,7 @@ class GraphElement { } // Function for rendering panel - render_panel() { + renderPanel() { this.panelWidth = this.elem.width(); if (this.shouldAbortRender()) { return; @@ -293,6 +296,7 @@ class GraphElement { this.prepareXAxis(options, this.panel); this.configureYAxisOptions(this.data, options); this.thresholdManager.addFlotOptions(options, this.panel); + this.timeRegionManager.addFlotOptions(options, this.panel); this.eventManager.addFlotEvents(this.annotations, options); this.sortedSeries = this.sortSeries(this.data, this.panel); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index a6c5190d937..8f63765c66d 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -1,6 +1,7 @@ import './graph'; import './series_overrides_ctrl'; import './thresholds_form'; +import './time_regions_form'; import template from './template'; import _ from 'lodash'; @@ -111,6 +112,7 @@ class GraphCtrl extends MetricsPanelCtrl { // other style overrides seriesOverrides: [], thresholds: [], + timeRegions: [], }; /** @ngInject */ @@ -133,9 +135,9 @@ class GraphCtrl extends MetricsPanelCtrl { } onInitEditMode() { - this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); this.addEditorTab('Axes', axesEditorComponent, 2); this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); + this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); if (config.alertingEnabled) { this.addEditorTab('Alert', alertTab, 5); diff --git a/public/app/plugins/panel/graph/specs/graph.test.ts b/public/app/plugins/panel/graph/specs/graph.test.ts index d86e860b27d..be243587820 100644 --- a/public/app/plugins/panel/graph/specs/graph.test.ts +++ b/public/app/plugins/panel/graph/specs/graph.test.ts @@ -125,7 +125,7 @@ describe('grafanaGraph', () => { //Emulate functions called by event listeners link.buildFlotPairs(link.data); - link.render_panel(); + link.renderPanel(); ctx.plotData = ctrl.plot.mock.calls[0][1]; ctx.plotOptions = ctrl.plot.mock.calls[0][2]; diff --git a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts new file mode 100644 index 00000000000..35e48897282 --- /dev/null +++ b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts @@ -0,0 +1,262 @@ +import { TimeRegionManager, colorModes } from '../time_region_manager'; +import moment from 'moment'; + +describe('TimeRegionManager', () => { + function plotOptionsScenario(desc, func) { + describe(desc, () => { + const ctx: any = { + panel: { + timeRegions: [], + }, + options: { + grid: { markings: [] }, + }, + panelCtrl: { + range: {}, + dashboard: { + isTimezoneUtc: () => false, + }, + }, + }; + + ctx.setup = (regions, from, to) => { + ctx.panel.timeRegions = regions; + ctx.panelCtrl.range.from = from; + ctx.panelCtrl.range.to = to; + const manager = new TimeRegionManager(ctx.panelCtrl); + manager.addFlotOptions(ctx.options, ctx.panel); + }; + + ctx.printScenario = () => { + console.log( + `Time range: from=${ctx.panelCtrl.range.from.format()}, to=${ctx.panelCtrl.range.to.format()}`, + ctx.panelCtrl.range.from._isUTC + ); + ctx.options.grid.markings.forEach((m, i) => { + console.log( + `Marking (${i}): from=${moment(m.xaxis.from).format()}, to=${moment(m.xaxis.to).format()}, color=${m.color}` + ); + }); + }; + + func(ctx); + }); + } + + describe('When creating plot markings using local time', () => { + plotOptionsScenario('for day of week region', ctx => { + const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }]; + const from = moment('2018-01-01T00:00:00+01:00'); + const to = moment('2018-01-01T23:59:00+01:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add fill', () => { + const markings = ctx.options.grid.markings; + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-01T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-02T00:59:59+01:00').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + }); + + it('should add line before', () => { + const markings = ctx.options.grid.markings; + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-01T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-01T01:00:00+01:00').format()); + expect(markings[1].color).toBe(colorModes.red.color.line); + }); + + it('should add line after', () => { + const markings = ctx.options.grid.markings; + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-02T00:59:59+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-02T00:59:59+01:00').format()); + expect(markings[2].color).toBe(colorModes.red.color.line); + }); + }); + + plotOptionsScenario('for time from region', ctx => { + const regions = [{ from: '05:00', fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01T00:00+01:00'); + const to = moment('2018-01-03T23:59+01:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at 05:00 each day', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-01T06:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-01T06:00:00+01:00').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-02T06:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-02T06:00:00+01:00').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-03T06:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-03T06:00:00+01:00').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for time to region', ctx => { + const regions = [{ to: '05:00', fill: true, colorMode: 'red' }]; + const from = moment('2018-02-01T00:00+01:00'); + const to = moment('2018-02-03T23:59+01:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at 05:00 each day', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-02-01T06:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-02-01T06:00:00+01:00').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-02-02T06:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-02-02T06:00:00+01:00').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-02-03T06:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-02-03T06:00:00+01:00').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for day of week from/to region', ctx => { + const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01T18:45:05+01:00'); + const to = moment('2018-01-22T08:27:00+01:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for day of week from region', ctx => { + const regions = [{ fromDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01T18:45:05+01:00'); + const to = moment('2018-01-22T08:27:00+01:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for day of week to region', ctx => { + const regions = [{ toDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01T18:45:05+01:00'); + const to = moment('2018-01-22T08:27:00+01:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for day of week from/to time region with daylight saving time', ctx => { + const regions = [{ fromDayOfWeek: 7, from: '20:00', toDayOfWeek: 7, to: '23:00', fill: true, colorMode: 'red' }]; + const from = moment('2018-03-17T06:00:00+01:00'); + const to = moment('2018-04-03T06:00:00+02:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday between 20:00 and 23:00', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-03-18T21:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-03-19T00:00:00+01:00').format()); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-03-25T22:00:00+02:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-03-26T01:00:00+02:00').format()); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-04-01T22:00:00+02:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-04-02T01:00:00+02:00').format()); + }); + }); + + plotOptionsScenario('for each day of week with winter time', ctx => { + const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-10-20T14:50:11+02:00'); + const to = moment('2018-11-07T12:56:23+01:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-10-21T02:00:00+02:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-10-22T01:59:59+02:00').format()); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-10-28T02:00:00+02:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-10-29T00:59:59+01:00').format()); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-11-04T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-11-05T00:59:59+01:00').format()); + }); + }); + }); +}); diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html index ebc6cf9b18e..d407f30ffc8 100644 --- a/public/app/plugins/panel/graph/tab_display.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -14,6 +14,11 @@ Thresholds ({{ctrl.panel.thresholds.length}}) +
  6. + + Time regions ({{ctrl.panel.timeRegions.length}}) + +
  7. @@ -132,4 +137,8 @@
    +
    + +
    +
    diff --git a/public/app/plugins/panel/graph/thresholds_form.html b/public/app/plugins/panel/graph/thresholds_form.html new file mode 100644 index 00000000000..81877150a47 --- /dev/null +++ b/public/app/plugins/panel/graph/thresholds_form.html @@ -0,0 +1,77 @@ +
    +
    Thresholds
    +

    + Visual thresholds options disabled. + Visit the Alert tab update your thresholds.
    + To re-enable thresholds, the alert rule must be deleted from this panel. +

    +
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    + +
    + +
    +
    + + + +
    + + + + +
    + + + +
    + + + + +
    + +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    +
    \ No newline at end of file diff --git a/public/app/plugins/panel/graph/thresholds_form.ts b/public/app/plugins/panel/graph/thresholds_form.ts index 5f1edb8aa9a..4f480873d5b 100644 --- a/public/app/plugins/panel/graph/thresholds_form.ts +++ b/public/app/plugins/panel/graph/thresholds_form.ts @@ -58,90 +58,10 @@ export class ThresholdFormCtrl { } } -const template = ` -
    -
    Thresholds
    -

    - Visual thresholds options disabled. - Visit the Alert tab update your thresholds.
    - To re-enable thresholds, the alert rule must be deleted from this panel. -

    -
    -
    -
    - -
    - -
    -
    - -
    - -
    - -
    - -
    - -
    -
    - - - -
    - - - - -
    - - - -
    - - - - -
    - -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    -
    -`; - coreModule.directive('graphThresholdForm', () => { return { restrict: 'E', - template: template, + templateUrl: 'public/app/plugins/panel/graph/thresholds_form.html', controller: ThresholdFormCtrl, bindToController: true, controllerAs: 'ctrl', diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts new file mode 100644 index 00000000000..95987e40dbe --- /dev/null +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -0,0 +1,248 @@ +import 'vendor/flot/jquery.flot'; +import _ from 'lodash'; +import moment from 'moment'; +import config from 'app/core/config'; + +export const colorModes = { + gray: { + themeDependent: true, + title: 'Gray', + darkColor: { fill: 'rgba(255, 255, 255, 0.09)', line: 'rgba(255, 255, 255, 0.2)' }, + lightColor: { fill: 'rgba(0, 0, 0, 0.09)', line: 'rgba(0, 0, 0, 0.2)' }, + }, + red: { + title: 'Red', + color: { fill: 'rgba(234, 112, 112, 0.12)', line: 'rgba(237, 46, 24, 0.60)' }, + }, + green: { + title: 'Green', + color: { fill: 'rgba(11, 237, 50, 0.090)', line: 'rgba(6,163,69, 0.60)' }, + }, + blue: { + title: 'Blue', + color: { fill: 'rgba(11, 125, 238, 0.12)', line: 'rgba(11, 125, 238, 0.60)' }, + }, + yellow: { + title: 'Yellow', + color: { fill: 'rgba(235, 138, 14, 0.12)', line: 'rgba(247, 149, 32, 0.60)' }, + }, + custom: { title: 'Custom' }, +}; + +export function getColorModes() { + return _.map(Object.keys(colorModes), key => { + return { + key: key, + value: colorModes[key].title, + }; + }); +} + +function getColor(timeRegion) { + if (Object.keys(colorModes).indexOf(timeRegion.colorMode) === -1) { + timeRegion.colorMode = 'red'; + } + + if (timeRegion.colorMode === 'custom') { + return { + fill: timeRegion.fillColor, + line: timeRegion.lineColor, + }; + } + + const colorMode = colorModes[timeRegion.colorMode]; + if (colorMode.themeDependent === true) { + return config.bootData.user.lightTheme ? colorMode.lightColor : colorMode.darkColor; + } + + return colorMode.color; +} + +export class TimeRegionManager { + plot: any; + timeRegions: any; + + constructor(private panelCtrl) {} + + draw(plot) { + this.timeRegions = this.panelCtrl.panel.timeRegions; + this.plot = plot; + } + + addFlotOptions(options, panel) { + if (!panel.timeRegions || panel.timeRegions.length === 0) { + return; + } + + const tRange = { from: moment(this.panelCtrl.range.from).utc(), to: moment(this.panelCtrl.range.to).utc() }; + + let i, hRange, timeRegion, regions, fromStart, fromEnd, timeRegionColor; + + const timeRegionsCopy = panel.timeRegions.map(a => ({ ...a })); + + for (i = 0; i < timeRegionsCopy.length; i++) { + timeRegion = timeRegionsCopy[i]; + + if (!(timeRegion.fromDayOfWeek || timeRegion.from) && !(timeRegion.toDayOfWeek || timeRegion.to)) { + continue; + } + + hRange = { + from: this.parseTimeRange(timeRegion.from), + to: this.parseTimeRange(timeRegion.to), + }; + + if (!timeRegion.fromDayOfWeek && timeRegion.toDayOfWeek) { + timeRegion.fromDayOfWeek = timeRegion.toDayOfWeek; + } + + if (!timeRegion.toDayOfWeek && timeRegion.fromDayOfWeek) { + timeRegion.toDayOfWeek = timeRegion.fromDayOfWeek; + } + + if (timeRegion.fromDayOfWeek) { + hRange.from.dayOfWeek = Number(timeRegion.fromDayOfWeek); + } + + if (timeRegion.toDayOfWeek) { + hRange.to.dayOfWeek = Number(timeRegion.toDayOfWeek); + } + + if (!hRange.from.h && hRange.to.h) { + hRange.from = hRange.to; + } + + if (hRange.from.h && !hRange.to.h) { + hRange.to = hRange.from; + } + + if (hRange.from.dayOfWeek && !hRange.from.h && !hRange.from.m) { + hRange.from.h = 0; + hRange.from.m = 0; + hRange.from.s = 0; + } + + if (hRange.to.dayOfWeek && !hRange.to.h && !hRange.to.m) { + hRange.to.h = 23; + hRange.to.m = 59; + hRange.to.s = 59; + } + + if (!hRange.from || !hRange.to) { + continue; + } + + regions = []; + + if ( + hRange.from.h >= tRange.from.hour() && + hRange.from.h <= tRange.from.hour() && + hRange.from.m >= tRange.from.minute() && + hRange.from.m <= tRange.from.minute() && + hRange.to.h >= tRange.to.hour() && + hRange.to.h <= tRange.to.hour() && + hRange.to.m >= tRange.to.minute() && + hRange.to.m <= tRange.to.minute() + ) { + regions.push({ from: tRange.from.valueOf(), to: tRange.to.startOf('hour').valueOf() }); + } else { + fromStart = moment(tRange.from); + fromStart.set('hour', 0); + fromStart.set('minute', 0); + fromStart.set('second', 0); + fromStart.add(hRange.from.h, 'hours'); + fromStart.add(hRange.from.m, 'minutes'); + fromStart.add(hRange.from.s, 'seconds'); + + while (fromStart.unix() <= tRange.to.unix()) { + while (hRange.from.dayOfWeek && hRange.from.dayOfWeek !== fromStart.isoWeekday()) { + fromStart.add(24, 'hours'); + } + + if (fromStart.unix() > tRange.to.unix()) { + break; + } + + fromEnd = moment(fromStart); + + if (hRange.from.h <= hRange.to.h) { + fromEnd.add(hRange.to.h - hRange.from.h, 'hours'); + } else if (hRange.from.h + hRange.to.h < 23) { + fromEnd.add(hRange.to.h, 'hours'); + } else { + fromEnd.add(24 - hRange.from.h, 'hours'); + } + + fromEnd.set('minute', hRange.to.m); + fromEnd.set('second', hRange.to.s); + + while (hRange.to.dayOfWeek && hRange.to.dayOfWeek !== fromEnd.isoWeekday()) { + fromEnd.add(24, 'hours'); + } + + const outsideRange = + (fromStart.unix() < tRange.from.unix() && fromEnd.unix() < tRange.from.unix()) || + (fromStart.unix() > tRange.to.unix() && fromEnd.unix() > tRange.to.unix()); + + if (!outsideRange) { + regions.push({ from: fromStart.valueOf(), to: fromEnd.valueOf() }); + } + + fromStart.add(24, 'hours'); + } + } + + timeRegionColor = getColor(timeRegion); + + for (let j = 0; j < regions.length; j++) { + const r = regions[j]; + if (timeRegion.fill) { + options.grid.markings.push({ + xaxis: { from: r.from, to: r.to }, + color: timeRegionColor.fill, + }); + } + + if (timeRegion.line) { + options.grid.markings.push({ + xaxis: { from: r.from, to: r.from }, + color: timeRegionColor.line, + }); + options.grid.markings.push({ + xaxis: { from: r.to, to: r.to }, + color: timeRegionColor.line, + }); + } + } + } + } + + parseTimeRange(str) { + const timeRegex = /^([\d]+):?(\d{2})?/; + const result = { h: null, m: null }; + const match = timeRegex.exec(str); + + if (!match) { + return result; + } + + if (match.length > 1) { + result.h = Number(match[1]); + result.m = 0; + + if (match.length > 2 && match[2] !== undefined) { + result.m = Number(match[2]); + } + + if (result.h > 23) { + result.h = 23; + } + + if (result.m > 59) { + result.m = 59; + } + } + + return result; + } +} diff --git a/public/app/plugins/panel/graph/time_regions_form.html b/public/app/plugins/panel/graph/time_regions_form.html new file mode 100644 index 00000000000..7292c53ec80 --- /dev/null +++ b/public/app/plugins/panel/graph/time_regions_form.html @@ -0,0 +1,64 @@ +
    +
    Time regions All configured time regions refers to UTC time
    +
    +
    + +
    + +
    + +
    + +
    + + +
    + +
    + +
    + +
    + +
    + +
    +
    + + + +
    + + + + +
    + + + +
    + + + + +
    + +
    + +
    +
    + +
    + +
    +
    \ No newline at end of file diff --git a/public/app/plugins/panel/graph/time_regions_form.ts b/public/app/plugins/panel/graph/time_regions_form.ts new file mode 100644 index 00000000000..5dc9c4016eb --- /dev/null +++ b/public/app/plugins/panel/graph/time_regions_form.ts @@ -0,0 +1,73 @@ +import coreModule from 'app/core/core_module'; +import { getColorModes } from './time_region_manager'; + +export class TimeRegionFormCtrl { + panelCtrl: any; + panel: any; + disabled: boolean; + colorModes: any; + + /** @ngInject */ + constructor($scope) { + this.panel = this.panelCtrl.panel; + + const unbindDestroy = $scope.$on('$destroy', () => { + this.panelCtrl.editingTimeRegions = false; + this.panelCtrl.render(); + unbindDestroy(); + }); + + this.colorModes = getColorModes(); + this.panelCtrl.editingTimeRegions = true; + } + + render() { + this.panelCtrl.render(); + } + + addTimeRegion() { + this.panel.timeRegions.push({ + op: 'time', + fromDayOfWeek: undefined, + from: undefined, + toDayOfWeek: undefined, + to: undefined, + colorMode: 'background6', + fill: true, + line: false, + }); + this.panelCtrl.render(); + } + + removeTimeRegion(index) { + this.panel.timeRegions.splice(index, 1); + this.panelCtrl.render(); + } + + onFillColorChange(index) { + return newColor => { + this.panel.timeRegions[index].fillColor = newColor; + this.render(); + }; + } + + onLineColorChange(index) { + return newColor => { + this.panel.timeRegions[index].lineColor = newColor; + this.render(); + }; + } +} + +coreModule.directive('graphTimeRegionForm', () => { + return { + restrict: 'E', + templateUrl: 'public/app/plugins/panel/graph/time_regions_form.html', + controller: TimeRegionFormCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + panelCtrl: '=', + }, + }; +}); diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index eafa3cf23f4..26ce6b7722d 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -77,7 +77,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { }; /** @ngInject */ - constructor($scope, $injector, private linkSrv) { + constructor($scope, $injector, private linkSrv, private $sanitize) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); @@ -398,14 +398,15 @@ class SingleStatCtrl extends MetricsPanelCtrl { const $location = this.$location; const linkSrv = this.linkSrv; const $timeout = this.$timeout; + const $sanitize = this.$sanitize; const panel = ctrl.panel; const templateSrv = this.templateSrv; let data, linkInfo; const $panelContainer = elem.find('.panel-container'); elem = elem.find('.singlestat-panel'); - function applyColoringThresholds(value, valueString) { - const color = getColorForValue(data, value); + function applyColoringThresholds(valueString) { + const color = getColorForValue(data, data.value); if (color) { return '' + valueString + ''; } @@ -413,8 +414,9 @@ class SingleStatCtrl extends MetricsPanelCtrl { return valueString; } - function getSpan(className, fontSize, value) { - value = templateSrv.replace(value, data.scopedVars); + function getSpan(className, fontSize, applyColoring, value) { + value = $sanitize(templateSrv.replace(value, data.scopedVars)); + value = applyColoring ? applyColoringThresholds(value) : value; return '' + value + ''; } @@ -422,25 +424,13 @@ class SingleStatCtrl extends MetricsPanelCtrl { let body = '
    '; if (panel.prefix) { - let prefix = panel.prefix; - if (panel.colorPrefix) { - prefix = applyColoringThresholds(data.value, panel.prefix); - } - body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, prefix); + body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, panel.colorPrefix, panel.prefix); } - let value = data.valueFormatted; - if (panel.colorValue) { - value = applyColoringThresholds(data.value, value); - } - body += getSpan('singlestat-panel-value', panel.valueFontSize, value); + body += getSpan('singlestat-panel-value', panel.valueFontSize, panel.colorValue, data.valueFormatted); if (panel.postfix) { - let postfix = panel.postfix; - if (panel.colorPostfix) { - postfix = applyColoringThresholds(data.value, panel.postfix); - } - body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, postfix); + body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.colorPostfix, panel.postfix); } body += '
    '; diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts index 6003acd89a6..902d722f0ad 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts @@ -14,6 +14,8 @@ describe('SingleStatCtrl', () => { get: () => {}, }; + const $sanitize = {}; + SingleStatCtrl.prototype.panel = { events: { on: () => {}, @@ -31,7 +33,7 @@ describe('SingleStatCtrl', () => { describe(desc, () => { ctx.setup = setupFunc => { beforeEach(() => { - ctx.ctrl = new SingleStatCtrl($scope, $injector, {}); + ctx.ctrl = new SingleStatCtrl($scope, $injector, {}, $sanitize); setupFunc(); ctx.ctrl.onDataReceived(ctx.data); ctx.data = ctx.ctrl.data; diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index 5522f3a11ce..705d1f36a54 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -18,8 +18,6 @@ export interface DataSource { readOnly: boolean; meta?: PluginMeta; pluginExports?: PluginExports; - init?: () => void; - testDatasource?: () => Promise; } export interface DataSourcesState { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 5a9db7e9b53..f80a485fc29 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -1,6 +1,6 @@ import { Value } from 'slate'; -import { RawTimeRange } from './series'; +import { DataQuery, RawTimeRange } from './series'; export interface CompletionItem { /** @@ -79,7 +79,7 @@ interface ExploreDatasource { export interface HistoryItem { ts: number; - query: string; + query: DataQuery; } export abstract class LanguageProvider { @@ -107,11 +107,6 @@ export interface TypeaheadOutput { suggestions: CompletionItemGroup[]; } -export interface Query { - query: string; - key?: string; -} - export interface QueryFix { type: string; label: string; @@ -130,6 +125,10 @@ export interface QueryHint { fix?: QueryFix; } +export interface QueryHintGetter { + (query: DataQuery, results: any[], ...rest: any): QueryHint[]; +} + export interface QueryTransaction { id: string; done: boolean; @@ -137,7 +136,7 @@ export interface QueryTransaction { hints?: QueryHint[]; latency: number; options: any; - query: string; + query: DataQuery; result?: any; // Table model / Timeseries[] / Logs resultType: ResultType; rowIndex: number; @@ -160,19 +159,12 @@ export interface ExploreState { exploreDatasources: ExploreDatasource[]; graphRange: RawTimeRange; history: HistoryItem[]; - /** - * Initial rows of queries to push down the tree. - * Modifications do not end up here, but in `this.queryExpressions`. - * The only way to reset a query is to change its `key`. - */ - queries: Query[]; - /** - * Hints gathered for the query row. - */ + initialQueries: DataQuery[]; queryTransactions: QueryTransaction[]; range: RawTimeRange; showingGraph: boolean; showingLogs: boolean; + showingStartPage?: boolean; showingTable: boolean; supportsGraph: boolean | null; supportsLogs: boolean | null; @@ -181,7 +173,7 @@ export interface ExploreState { export interface ExploreUrlState { datasource: string; - queries: Query[]; + queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense range: RawTimeRange; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index fc176fed7e2..3bf6b4c9803 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -22,7 +22,7 @@ import { } from './series'; import { PanelProps, PanelOptionsProps } from './panel'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; -import { Organization, OrganizationPreferences, OrganizationState } from './organization'; +import { Organization, OrganizationState } from './organization'; import { AppNotification, AppNotificationSeverity, @@ -81,7 +81,6 @@ export { PluginDashboard, Organization, OrganizationState, - OrganizationPreferences, AppNotification, AppNotificationsState, AppNotificationSeverity, diff --git a/public/app/types/organization.ts b/public/app/types/organization.ts index 52cb130e082..da67ead34ae 100644 --- a/public/app/types/organization.ts +++ b/public/app/types/organization.ts @@ -3,13 +3,6 @@ export interface Organization { id: number; } -export interface OrganizationPreferences { - homeDashboardId: number; - theme: string; - timezone: string; -} - export interface OrganizationState { organization: Organization; - preferences: OrganizationPreferences; } diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 817777669d8..9e3264be70c 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -6,6 +6,7 @@ export interface PluginExports { QueryCtrl?: any; ConfigCtrl?: any; AnnotationsQueryCtrl?: any; + VariableQueryEditor?: any; ExploreQueryField?: any; ExploreStartPage?: any; @@ -98,3 +99,10 @@ export interface PluginsState { hasFetched: boolean; dashboards: PluginDashboard[]; } + +export interface VariableQueryProps { + query: any; + onChange: (query: any, definition: string) => void; + datasource: any; + templateSrv: any; +} diff --git a/public/app/types/series.ts b/public/app/types/series.ts index 5396880611b..18ebbc5f648 100644 --- a/public/app/types/series.ts +++ b/public/app/types/series.ts @@ -1,4 +1,5 @@ import { Moment } from 'moment'; +import { PluginMeta } from './plugins'; export enum LoadingState { NotStarted = 'NotStarted', @@ -70,6 +71,7 @@ export interface DataQueryResponse { export interface DataQuery { refId: string; + [key: string]: any; } export interface DataQueryOptions { @@ -87,5 +89,14 @@ export interface DataQueryOptions { } export interface DataSourceApi { + /** + * Imports queries from a different datasource + */ + importQueries?(queries: DataQuery[], originMeta: PluginMeta): Promise; + /** + * Initializes a datasource after instantiation + */ + init?: () => void; query(options: DataQueryOptions): Promise; + testDatasource?: () => Promise; } diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx new file mode 100644 index 00000000000..5e90fd870db --- /dev/null +++ b/public/app/viz/Gauge.tsx @@ -0,0 +1,133 @@ +import React, { PureComponent } from 'react'; +import $ from 'jquery'; +import { withSize } from 'react-sizeme'; +import { TimeSeriesVMs } from 'app/types'; +import config from '../core/config'; + +interface Props { + timeSeries: TimeSeriesVMs; + minValue: number; + maxValue: number; + showThresholdMarkers?: boolean; + thresholds?: number[]; + showThresholdLables?: boolean; + size?: { width: number; height: number }; +} + +const colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)']; + +export class Gauge extends PureComponent { + parentElement: any; + canvasElement: any; + + static defaultProps = { + minValue: 0, + maxValue: 100, + showThresholdMarkers: true, + showThresholdLables: false, + thresholds: [], + }; + + componentDidMount() { + this.draw(); + } + + componentDidUpdate(prevProps: Props) { + this.draw(); + } + + draw() { + const { maxValue, minValue, showThresholdLables, size, showThresholdMarkers, timeSeries, thresholds } = this.props; + + const width = size.width; + const height = size.height; + const dimension = Math.min(width, height * 1.3); + + const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; + const fontColor = config.bootData.user.lightTheme ? 'rgb(38,38,38)' : 'rgb(230,230,230)'; + const fontScale = parseInt('80', 10) / 100; + const fontSize = Math.min(dimension / 5, 100) * fontScale; + const gaugeWidth = Math.min(dimension / 6, 60); + const thresholdMarkersWidth = gaugeWidth / 5; + const thresholdLabelFontSize = fontSize / 2.5; + + const formattedThresholds = []; + + thresholds.forEach((threshold, index) => { + formattedThresholds.push({ + value: threshold, + color: colors[index], + }); + }); + + const options = { + series: { + gauges: { + gauge: { + min: minValue, + max: maxValue, + background: { color: backgroundColor }, + border: { color: null }, + shadow: { show: false }, + width: gaugeWidth, + }, + frame: { show: false }, + label: { show: false }, + layout: { margin: 0, thresholdWidth: 0 }, + cell: { border: { width: 0 } }, + threshold: { + values: formattedThresholds, + label: { + show: showThresholdLables, + margin: thresholdMarkersWidth + 1, + font: { size: thresholdLabelFontSize }, + }, + show: showThresholdMarkers, + width: thresholdMarkersWidth, + }, + value: { + color: fontColor, + formatter: () => { + return Math.round(timeSeries[0].stats.avg); + }, + font: { + size: fontSize, + family: '"Helvetica Neue", Helvetica, Arial, sans-serif', + }, + }, + show: true, + }, + }, + }; + + const plotSeries = { + data: [[0, timeSeries[0].stats.avg]], + }; + + try { + $.plot(this.canvasElement, [plotSeries], options); + } catch (err) { + console.log('Gauge rendering error', err, options, timeSeries); + } + } + + render() { + const { height, width } = this.props.size; + + return ( +
    (this.parentElement = element)}> +
    (this.canvasElement = element)} + /> +
    + ); + } +} + +export default withSize({ monitorHeight: true })(Gauge); diff --git a/public/app/viz/GaugeOptions.tsx b/public/app/viz/GaugeOptions.tsx new file mode 100644 index 00000000000..031855e1c78 --- /dev/null +++ b/public/app/viz/GaugeOptions.tsx @@ -0,0 +1,16 @@ +import React, { PureComponent } from 'react'; +import { PanelOptionsProps } from '../types'; + +interface Props {} + +export class GaugeOptions extends PureComponent> { + render() { + return ( +
    +
    +
    Draw Modes
    +
    +
    + ); + } +} diff --git a/public/sass/components/_footer.scss b/public/sass/components/_footer.scss index 9bc99c30d1d..893eea02914 100644 --- a/public/sass/components/_footer.scss +++ b/public/sass/components/_footer.scss @@ -4,7 +4,7 @@ .footer { color: $footer-link-color; - padding: 5rem 0 1rem 0; + padding: 1rem 0 1rem 0; font-size: $font-size-sm; position: relative; width: 98%; /* was causing horiz scrollbars - need to examine */ @@ -38,6 +38,7 @@ } } +// Keeping footer inside the graphic on Login screen .login-page { .footer { bottom: $spacer; diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index 6d83fc6cf0b..e2d5296c3aa 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -82,7 +82,7 @@ $input-border: 1px solid $input-border-color; align-content: flex-start; .gf-form + .gf-form { - margin-right: $gf-form-margin; + margin-left: $gf-form-margin; } } @@ -161,7 +161,6 @@ $input-border: 1px solid $input-border-color; display: block; width: 100%; padding: $input-padding-y $input-padding-x; - margin-right: $gf-form-margin; font-size: $font-size-md; line-height: $input-line-height; color: $input-color; @@ -255,7 +254,6 @@ $input-border: 1px solid $input-border-color; } .gf-form-select-wrapper { - margin-right: $gf-form-margin; position: relative; background-color: $input-bg; diff --git a/public/sass/components/_query_editor.scss b/public/sass/components/_query_editor.scss index 9fcfdf719ba..40cee199062 100644 --- a/public/sass/components/_query_editor.scss +++ b/public/sass/components/_query_editor.scss @@ -35,7 +35,7 @@ } .gf-form + .gf-form { - margin-right: 0; + margin-left: 0; } } diff --git a/public/sass/layout/_page.scss b/public/sass/layout/_page.scss index faa5b94d4ad..312248086fa 100644 --- a/public/sass/layout/_page.scss +++ b/public/sass/layout/_page.scss @@ -40,6 +40,29 @@ &--dashboard { height: calc(100% - 56px); } + + // Sticky footer + display: flex; + flex-direction: column; + + > div { + flex-grow: 1; + } + + > .footer { + flex-shrink: 0; + } + + // Render in correct position even ng-view div is not rendered yet + > .footer:first-child { + flex-grow: 1; + display: flex; + + > * { + width: 100%; + align-self: flex-end; + } + } } // fix for phantomjs diff --git a/public/sass/pages/_alerting.scss b/public/sass/pages/_alerting.scss index 90f2eb526f1..77752be11bc 100644 --- a/public/sass/pages/_alerting.scss +++ b/public/sass/pages/_alerting.scss @@ -66,6 +66,13 @@ content: '\e611'; } } + + &--pending { + .panel-alert-icon:before { + color: $warn; + content: '\e611'; + } + } } @keyframes alerting-panel { diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 4b07b04e330..23c6fbf0916 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -18,10 +18,39 @@ margin-left: 15px; } - // Graph panel needs a bit extra padding at top - .panel-container { + .explore-panel { + margin-top: $panel-margin; + } + + .explore-panel__body { padding: $panel-padding; - padding-top: 10px; + } + + .explore-panel__header { + padding: $panel-padding; + padding-top: 5px; + padding-bottom: 0; + display: flex; + cursor: pointer; + margin-bottom: 5px; + transition: all 0.1s linear; + } + + .explore-panel__header:hover { + transform: translateY(-1px); + } + + .explore-panel__header-label { + font-weight: 500; + margin-right: $panel-margin; + font-size: $font-size-h6; + box-shadow: $text-shadow-faint; + } + + .explore-panel__header-buttons { + margin-right: $panel-margin; + font-size: $font-size-lg; + line-height: $font-size-h6; } // Make sure wrap buttons around on small screens @@ -91,11 +120,16 @@ height: 2px; position: relative; overflow: hidden; - background: $text-color-faint; + background: none; margin: $panel-margin / 2; + transition: background-color 1s ease; } - .explore-panel__loader:after { + .explore-panel__loader--active { + background: $text-color-faint; + } + + .explore-panel__loader--active:after { content: ' '; display: block; width: 25%; @@ -221,17 +255,18 @@ .logs-controls { display: flex; + background-color: $page-bg; + padding: $panel-padding; + padding-top: 10px; + border-radius: $border-radius; + margin: 2*$panel-margin 0; + border: $panel-border; > * { margin-right: 1em; } } - .logs-options, - .logs-graph { - margin-bottom: $panel-margin; - } - .logs-meta { flex: 1; color: $text-color-weak; @@ -265,11 +300,12 @@ .logs-row-level { background-color: transparent; - margin: 6px 0; - border-radius: 2px; + margin: 2px 0; + position: relative; opacity: 0.8; } + .logs-row-level-critical, .logs-row-level-crit { background-color: #705da0; } @@ -279,6 +315,7 @@ background-color: #e24d42; } + .logs-row-level-warning, .logs-row-level-warn { background-color: #eab839; } @@ -287,10 +324,32 @@ background-color: #7eb26d; } - .logs-row-level-trace, .logs-row-level-debug { background-color: #1f78c1; } + + .logs-row-level-trace { + background-color: #6ed0e0; + } + + .logs-row-level__duplicates { + position: absolute; + width: 9px; + height: 100%; + top: 0; + left: 5px; + display: flex; + flex-wrap: wrap; + align-items: flex-start; + align-content: flex-start; + } + + .logs-row-level__duplicate { + width: 2px; + height: 3px; + background-color: #1f78c1; + margin: 0 1px 1px 0; + } } } @@ -320,8 +379,11 @@ .ReactTable { border: none; +} + +.ReactTable .rt-table { // Allow some space for the no-data text - min-height: 120px; + min-height: 90px; } .ReactTable .rt-thead.-header { @@ -350,6 +412,11 @@ .ReactTable .rt-tbody .rt-td:last-child { border-right: none; } +.ReactTable .-pagination { + border-top: none; + box-shadow: none; + margin-top: $panel-margin; +} .ReactTable .-pagination .-btn { color: $blue; background: $list-item-bg; @@ -371,6 +438,10 @@ .ReactTable .rt-tr .rt-td:last-child { text-align: right; } +.ReactTable .rt-noData { + top: 60px; + z-index: inherit; +} // React-component cascade fix: show "loading" even though item can expand diff --git a/public/vendor/flot/jquery.flot.gauge.js b/public/vendor/flot/jquery.flot.gauge.js index 14866dbef20..db1cb1ffe24 100644 --- a/public/vendor/flot/jquery.flot.gauge.js +++ b/public/vendor/flot/jquery.flot.gauge.js @@ -583,30 +583,31 @@ * @param {Number} [a] the angle of the value drawn */ function drawText(x, y, id, text, textOptions, a) { - var span = $("." + id, placeholder); + var span = $(placeholder).find("#" + id); var exists = span.length; if (!exists) { span = $("") span.attr("id", id); - span.css("position", "absolute"); - span.css("top", y + "px"); - if (textOptions.font.size) { - span.css("font-size", textOptions.font.size + "px"); - } - if (textOptions.font.family) { - span.css("font-family", textOptions.font.family); - } - if (textOptions.color) { - span.css("color", textOptions.color); - } - if (textOptions.background.color) { - span.css("background-color", textOptions.background.color); - } - if (textOptions.background.opacity) { - span.css("opacity", textOptions.background.opacity); - } placeholder.append(span); } + + span.css("position", "absolute"); + span.css("top", y + "px"); + if (textOptions.font.size) { + span.css("font-size", textOptions.font.size + "px"); + } + if (textOptions.font.family) { + span.css("font-family", textOptions.font.family); + } + if (textOptions.color) { + span.css("color", textOptions.color); + } + if (textOptions.background.color) { + span.css("background-color", textOptions.background.color); + } + if (textOptions.background.opacity) { + span.css("opacity", textOptions.background.opacity); + } span.text(text); // after append, readjust the left position span.css("left", x + "px"); // for redraw, resetting the left position is needed here diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index daa3ac6bb8e..8e0d7a1420f 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -2273,7 +2273,7 @@ Licensed under the MIT license. function drawOrphanedPoints(series) { /* Filters series data for points with no neighbors before or after - * and plots single 0.5 radius points for them so that they are displayed. + * and plots single 0.5 radius points for them so that they are displayed. */ var abandonedPoints = []; var beforeX = null; @@ -2301,7 +2301,7 @@ Licensed under the MIT license. } var olddatapoints = datapoints.points datapoints.points = abandonedPoints; - + series.points.radius = series.lines.lineWidth/2; // plot the orphan points with a radius of lineWidth/2 drawSeriesPoints(series); @@ -2310,12 +2310,13 @@ Licensed under the MIT license. } function drawSeries(series) { - if (series.lines.show) + if (series.lines.show) { drawSeriesLines(series); if (!series.points.show && !series.bars.show) { // not necessary if user wants points displayed for everything drawOrphanedPoints(series); } + } if (series.bars.show) drawSeriesBars(series); if (series.points.show) diff --git a/scripts/build/build-all.sh b/scripts/build/build-all.sh index 3e7058fa494..be0b297527b 100755 --- a/scripts/build/build-all.sh +++ b/scripts/build/build-all.sh @@ -32,9 +32,7 @@ echo "Build arguments: $OPT" go run build.go -goarch armv7 -cc ${CCARMV7} ${OPT} build go run build.go -goarch arm64 -cc ${CCARM64} ${OPT} build - -# MacOS build is broken atm. See Issue #13763 -#go run build.go -goos darwin -cc ${CCOSX64} ${OPT} build +go run build.go -goos darwin -cc ${CCOSX64} ${OPT} build go run build.go -goos windows -cc ${CCWIN64} ${OPT} build CC=${CCX64} go run build.go ${OPT} build diff --git a/scripts/build/publish.sh b/scripts/build/publish.sh index c03146eb910..264d930e51b 100755 --- a/scripts/build/publish.sh +++ b/scripts/build/publish.sh @@ -2,13 +2,15 @@ # no relation to publish.go -# Right now we hack this in into the publish script. +EXTRA_OPTS="$@" + +# Right now we hack this in into the publish script. # Eventually we might want to keep a list of all previous releases somewhere. -_releaseNoteUrl="https://community.grafana.com/t/release-notes-v5-3-x/10244" -_whatsNewUrl="http://docs.grafana.org/guides/whats-new-in-v5-3/" +_releaseNoteUrl="https://community.grafana.com/t/release-notes-v5-4-x/12215" +_whatsNewUrl="http://docs.grafana.org/guides/whats-new-in-v5-4/" ./scripts/build/release_publisher/release_publisher \ --wn ${_whatsNewUrl} \ --rn ${_releaseNoteUrl} \ --version ${CIRCLE_TAG} \ - --apikey ${GRAFANA_COM_API_KEY} + --apikey ${GRAFANA_COM_API_KEY} ${EXTRA_OPTS} diff --git a/scripts/build/release_publisher/externalrelease.go b/scripts/build/release_publisher/externalrelease.go index 327fb335dbe..992cba38f90 100644 --- a/scripts/build/release_publisher/externalrelease.go +++ b/scripts/build/release_publisher/externalrelease.go @@ -9,30 +9,38 @@ import ( ) type releaseFromExternalContent struct { - getter urlGetter - rawVersion string + getter urlGetter + rawVersion string artifactConfigurations []buildArtifact } func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) { version := re.rawVersion[1:] - isBeta := strings.Contains(version, "beta") + beta := strings.Contains(version, "beta") + var rt ReleaseType + if beta { + rt = BETA + } else if nightly { + rt = NIGHTLY + } else { + rt = STABLE + } builds := []build{} for _, ba := range re.artifactConfigurations { - sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(baseArchiveUrl, version, isBeta))) + sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(baseArchiveUrl, version, rt))) if err != nil { return nil, err } - builds = append(builds, newBuild(baseArchiveUrl, ba, version, isBeta, sha256)) + builds = append(builds, newBuild(baseArchiveUrl, ba, version, rt, sha256)) } r := release{ Version: version, ReleaseDate: time.Now().UTC(), - Stable: !isBeta && !nightly, - Beta: isBeta, - Nightly: nightly, + Stable: rt.stable(), + Beta: rt.beta(), + Nightly: rt.nightly(), WhatsNewUrl: whatsNewUrl, ReleaseNotesUrl: releaseNotesUrl, Builds: builds, diff --git a/scripts/build/release_publisher/localrelease.go b/scripts/build/release_publisher/localrelease.go index 7c0a3b1d085..4f4575c4ff4 100644 --- a/scripts/build/release_publisher/localrelease.go +++ b/scripts/build/release_publisher/localrelease.go @@ -13,11 +13,14 @@ import ( ) type releaseLocalSources struct { - path string + path string artifactConfigurations []buildArtifact } func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) { + if !nightly { + return nil, errors.New("Local releases only supported for nightly builds.") + } buildData := r.findBuilds(baseArchiveUrl) rel := release{ @@ -36,7 +39,7 @@ func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, type buildData struct { version string - builds []build + builds []build } func (r releaseLocalSources) findBuilds(baseArchiveUrl string) buildData { @@ -70,7 +73,7 @@ func createBuildWalker(path string, data *buildData, archiveTypes []buildArtifac data.version = version data.builds = append(data.builds, build{ Os: archive.os, - Url: archive.getUrl(baseArchiveUrl, version, false), + Url: archive.getUrl(baseArchiveUrl, version, NIGHTLY), Sha256: string(shaBytes), Arch: archive.arch, }) diff --git a/scripts/build/release_publisher/main.go b/scripts/build/release_publisher/main.go index c4004fe7b4c..90acb2d3e62 100644 --- a/scripts/build/release_publisher/main.go +++ b/scripts/build/release_publisher/main.go @@ -41,28 +41,41 @@ func main() { var builder releaseBuilder var product string - if fromLocal { - path, _ := os.Getwd() - builder = releaseLocalSources{ - path: path, - artifactConfigurations: buildArtifactConfigurations, - } - } else { - builder = releaseFromExternalContent{ - getter: getHttpContents{}, - rawVersion: version, - artifactConfigurations: buildArtifactConfigurations, - } - } - - archiveProviderRoot := "https://s3-us-west-2.amazonaws.com" + archiveProviderRoot := "https://dl.grafana.com" + buildArtifacts := completeBuildArtifactConfigurations if enterprise { product = "grafana-enterprise" - baseUrl = createBaseUrl(archiveProviderRoot, "grafana-enterprise-releases", product, nightly) + baseUrl = createBaseUrl(archiveProviderRoot, "enterprise", product, nightly) + var err error + buildArtifacts, err = filterBuildArtifacts([]artifactFilter{ + {os: "deb", arch: "amd64"}, + {os: "rhel", arch: "amd64"}, + {os: "linux", arch: "amd64"}, + {os: "win", arch: "amd64"}, + }) + + if err != nil { + log.Fatalf("Could not filter to the selected build artifacts, err=%v", err) + } + } else { product = "grafana" - baseUrl = createBaseUrl(archiveProviderRoot, "grafana-releases", product, nightly) + baseUrl = createBaseUrl(archiveProviderRoot, "oss", product, nightly) + } + + if fromLocal { + path, _ := os.Getwd() + builder = releaseLocalSources{ + path: path, + artifactConfigurations: buildArtifacts, + } + } else { + builder = releaseFromExternalContent{ + getter: getHttpContents{}, + rawVersion: version, + artifactConfigurations: buildArtifacts, + } } p := publisher{ diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go index d2c10d1640f..1d93c1e306e 100644 --- a/scripts/build/release_publisher/publisher.go +++ b/scripts/build/release_publisher/publisher.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/pkg/errors" "io/ioutil" "log" "net/http" @@ -61,13 +62,33 @@ func (p *publisher) postRelease(r *release) error { return nil } +type ReleaseType int + +const ( + STABLE ReleaseType = iota + 1 + BETA + NIGHTLY +) + +func (rt ReleaseType) beta() bool { + return rt == BETA +} + +func (rt ReleaseType) stable() bool { + return rt == STABLE +} + +func (rt ReleaseType) nightly() bool { + return rt == NIGHTLY +} + type buildArtifact struct { os string arch string urlPostfix string } -func (t buildArtifact) getUrl(baseArchiveUrl, version string, isBeta bool) string { +func (t buildArtifact) getUrl(baseArchiveUrl, version string, releaseType ReleaseType) string { prefix := "-" rhelReleaseExtra := "" @@ -75,7 +96,7 @@ func (t buildArtifact) getUrl(baseArchiveUrl, version string, isBeta bool) strin prefix = "_" } - if !isBeta && t.os == "rhel" { + if releaseType.stable() && t.os == "rhel" { rhelReleaseExtra = "-1" } @@ -83,7 +104,7 @@ func (t buildArtifact) getUrl(baseArchiveUrl, version string, isBeta bool) strin return url } -var buildArtifactConfigurations = []buildArtifact{ +var completeBuildArtifactConfigurations = []buildArtifact{ { os: "deb", arch: "arm64", @@ -141,10 +162,35 @@ var buildArtifactConfigurations = []buildArtifact{ }, } -func newBuild(baseArchiveUrl string, ba buildArtifact, version string, isBeta bool, sha256 string) build { +type artifactFilter struct { + os string + arch string +} + +func filterBuildArtifacts(filters []artifactFilter) ([]buildArtifact, error) { + var artifacts []buildArtifact + for _, f := range filters { + matched := false + + for _, a := range completeBuildArtifactConfigurations { + if f.os == a.os && f.arch == a.arch { + artifacts = append(artifacts, a) + matched = true + break + } + } + + if !matched { + return nil, errors.New(fmt.Sprintf("No buildArtifact for os=%v, arch=%v", f.os, f.arch)) + } + } + return artifacts, nil +} + +func newBuild(baseArchiveUrl string, ba buildArtifact, version string, rt ReleaseType, sha256 string) build { return build{ Os: ba.os, - Url: ba.getUrl(baseArchiveUrl, version, isBeta), + Url: ba.getUrl(baseArchiveUrl, version, rt), Sha256: sha256, Arch: ba.arch, } diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index ee491ce0b98..2aea55d5ee1 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -3,44 +3,99 @@ package main import "testing" func TestPreparingReleaseFromRemote(t *testing.T) { - versionIn := "v5.2.0-beta1" - expectedVersion := "5.2.0-beta1" - whatsNewUrl := "https://whatsnews.foo/" - relNotesUrl := "https://relnotes.foo/" - expectedArch := "amd64" - expectedOs := "linux" - buildArtifacts := []buildArtifact{{expectedOs,expectedArch, ".linux-amd64.tar.gz"}} - var builder releaseBuilder - - builder = releaseFromExternalContent{ - getter: mockHttpGetter{}, - rawVersion: versionIn, - artifactConfigurations: buildArtifactConfigurations, + cases := []struct { + version string + expectedVersion string + whatsNewUrl string + relNotesUrl string + nightly bool + expectedBeta bool + expectedStable bool + expectedArch string + expectedOs string + expectedUrl string + baseArchiveUrl string + buildArtifacts []buildArtifact + }{ + { + version: "v5.2.0-beta1", + expectedVersion: "5.2.0-beta1", + whatsNewUrl: "https://whatsnews.foo/", + relNotesUrl: "https://relnotes.foo/", + nightly: false, + expectedBeta: true, + expectedStable: false, + expectedArch: "amd64", + expectedOs: "linux", + expectedUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.2.0-beta1.linux-amd64.tar.gz", + baseArchiveUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", + buildArtifacts: []buildArtifact{{"linux", "amd64", ".linux-amd64.tar.gz"}}, + }, + { + version: "v5.2.3", + expectedVersion: "5.2.3", + whatsNewUrl: "https://whatsnews.foo/", + relNotesUrl: "https://relnotes.foo/", + nightly: false, + expectedBeta: false, + expectedStable: true, + expectedArch: "amd64", + expectedOs: "rhel", + expectedUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.2.3-1.x86_64.rpm", + baseArchiveUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", + buildArtifacts: []buildArtifact{{"rhel", "amd64", ".x86_64.rpm"}}, + }, + { + version: "v5.4.0-pre1asdf", + expectedVersion: "5.4.0-pre1asdf", + whatsNewUrl: "https://whatsnews.foo/", + relNotesUrl: "https://relnotes.foo/", + nightly: true, + expectedBeta: false, + expectedStable: false, + expectedArch: "amd64", + expectedOs: "rhel", + expectedUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.4.0-pre1asdf.x86_64.rpm", + baseArchiveUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", + buildArtifacts: []buildArtifact{{"rhel", "amd64", ".x86_64.rpm"}}, + }, } - rel, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", whatsNewUrl, relNotesUrl, false) + for _, test := range cases { + builder := releaseFromExternalContent{ + getter: mockHttpGetter{}, + rawVersion: test.version, + artifactConfigurations: test.buildArtifacts, + } - if !rel.Beta || rel.Stable { - t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", versionIn) - } + rel, _ := builder.prepareRelease(test.baseArchiveUrl, test.whatsNewUrl, test.relNotesUrl, test.nightly) - if rel.Version != expectedVersion { - t.Errorf("Expected version to be %s, but it was %s.", expectedVersion, rel.Version) - } + if rel.Beta != test.expectedBeta || rel.Stable != test.expectedStable { + t.Errorf("%s should have been tagged as beta=%v, stable=%v.", test.version, test.expectedBeta, test.expectedStable) + } - expectedBuilds := len(buildArtifacts) - if len(rel.Builds) != expectedBuilds { - t.Errorf("Expected %v builds, but got %v.", expectedBuilds, len(rel.Builds)) - } + if rel.Version != test.expectedVersion { + t.Errorf("Expected version to be %s, but it was %s.", test.expectedVersion, rel.Version) + } - build := rel.Builds[0] - if build.Arch != expectedArch { - t.Errorf("Expected arch to be %v, but it was %v", expectedArch, build.Arch) - } + expectedBuilds := len(test.buildArtifacts) + if len(rel.Builds) != expectedBuilds { + t.Errorf("Expected %v builds, but got %v.", expectedBuilds, len(rel.Builds)) + } - if build.Os != expectedOs { - t.Errorf("Expected arch to be %v, but it was %v", expectedOs, build.Os) + build := rel.Builds[0] + if build.Arch != test.expectedArch { + t.Errorf("Expected arch to be %v, but it was %v", test.expectedArch, build.Arch) + } + + if build.Os != test.expectedOs { + t.Errorf("Expected os to be %v, but it was %v", test.expectedOs, build.Os) + } + + if build.Url != test.expectedUrl { + t.Errorf("Expected url to be %v, but it was %v", test.expectedUrl, build.Url) + } } } @@ -50,7 +105,6 @@ func (mockHttpGetter) getContents(url string) (string, error) { return url, nil } - func TestPreparingReleaseFromLocal(t *testing.T) { whatsNewUrl := "https://whatsnews.foo/" relNotesUrl := "https://relnotes.foo/" @@ -61,7 +115,7 @@ func TestPreparingReleaseFromLocal(t *testing.T) { testDataPath := "testdata" builder = releaseLocalSources{ path: testDataPath, - artifactConfigurations: buildArtifactConfigurations, + artifactConfigurations: completeBuildArtifactConfigurations, } relAll, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, true) @@ -93,7 +147,7 @@ func TestPreparingReleaseFromLocal(t *testing.T) { expectedOs := "win" builder = releaseLocalSources{ - path: testDataPath, + path: testDataPath, artifactConfigurations: []buildArtifact{{ os: expectedOs, arch: expectedArch, @@ -116,4 +170,31 @@ func TestPreparingReleaseFromLocal(t *testing.T) { if build.Os != expectedOs { t.Fatalf("Expected os to be %s, but was %s", expectedOs, build.Os) } + + _, err := builder.prepareRelease("", "", "", false) + if err == nil { + t.Error("Error was nil, but expected an error as the local releaser only supports nightly builds.") + } +} + +func TestFilterBuildArtifacts(t *testing.T) { + buildArtifacts, _ := filterBuildArtifacts([]artifactFilter{ + {os: "deb", arch: "amd64"}, + {os: "rhel", arch: "amd64"}, + {os: "linux", arch: "amd64"}, + {os: "win", arch: "amd64"}, + }) + + if len(buildArtifacts) != 4 { + t.Errorf("Expected 4 build artifacts after filtering, but was %v", len(buildArtifacts)) + } + + _, err := filterBuildArtifacts([]artifactFilter{ + {os: "foobar", arch: "amd64"}, + }) + + if err == nil { + t.Errorf("Expected an error as a we tried to filter on a nonexiststant os.") + } + } diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 228df79b3f8..7e103b32606 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -85,7 +85,7 @@ module.exports = merge(common, { new HtmlWebpackPlugin({ filename: path.resolve(__dirname, '../../public/views/error.html'), template: path.resolve(__dirname, '../../public/views/error-template.html'), - inject: 'false', + inject: false, }), new HtmlWebpackPlugin({ filename: path.resolve(__dirname, '../../public/views/index.html'), diff --git a/scripts/webpack/webpack.prod.js b/scripts/webpack/webpack.prod.js index 5d3ffa61219..d45c78352b0 100644 --- a/scripts/webpack/webpack.prod.js +++ b/scripts/webpack/webpack.prod.js @@ -74,17 +74,17 @@ module.exports = merge(common, { filename: "grafana.[name].[hash].css" }), new ngAnnotatePlugin(), + new HtmlWebpackPlugin({ + filename: path.resolve(__dirname, '../../public/views/error.html'), + template: path.resolve(__dirname, '../../public/views/error-template.html'), + inject: false, + }), new HtmlWebpackPlugin({ filename: path.resolve(__dirname, '../../public/views/index.html'), template: path.resolve(__dirname, '../../public/views/index-template.html'), inject: 'body', chunks: ['vendor', 'app'], }), - new HtmlWebpackPlugin({ - filename: path.resolve(__dirname, '../../public/views/error.html'), - template: path.resolve(__dirname, '../../public/views/error-template.html'), - inject: false, - }), function () { this.hooks.done.tap('Done', function (stats) { if (stats.compilation.errors && stats.compilation.errors.length) { diff --git a/vendor/github.com/go-macaron/session/session.go b/vendor/github.com/go-macaron/session/session.go index 7e7b833c552..6bd5bdb1fdd 100644 --- a/vendor/github.com/go-macaron/session/session.go +++ b/vendor/github.com/go-macaron/session/session.go @@ -18,6 +18,7 @@ package session import ( "encoding/hex" + "errors" "fmt" "net/http" "net/url" @@ -26,7 +27,7 @@ import ( "gopkg.in/macaron.v1" ) -const _VERSION = "0.3.0" +const _VERSION = "0.5.0" func Version() string { return _VERSION @@ -245,20 +246,38 @@ func NewManager(name string, opt Options) (*Manager, error) { return &Manager{p, opt}, p.Init(opt.Maxlifetime, opt.ProviderConfig) } -// sessionId generates a new session ID with rand string, unix nano time, remote addr by hash function. -func (m *Manager) sessionId() string { +// sessionID generates a new session ID with rand string, unix nano time, remote addr by hash function. +func (m *Manager) sessionID() string { return hex.EncodeToString(generateRandomKey(m.opt.IDLength / 2)) } +// validSessionID tests whether a provided session ID is a valid session ID. +func (m *Manager) validSessionID(sid string) (bool, error) { + if len(sid) != m.opt.IDLength { + return false, errors.New("invalid 'sid': " + sid) + } + + for i := range sid { + switch { + case '0' <= sid[i] && sid[i] <= '9': + case 'a' <= sid[i] && sid[i] <= 'f': + default: + return false, errors.New("invalid 'sid': " + sid) + } + } + return true, nil +} + // Start starts a session by generating new one // or retrieve existence one by reading session ID from HTTP request if it's valid. func (m *Manager) Start(ctx *macaron.Context) (RawStore, error) { sid := ctx.GetCookie(m.opt.CookieName) - if len(sid) > 0 && m.provider.Exist(sid) { + valid, _ := m.validSessionID(sid) + if len(sid) > 0 && valid && m.provider.Exist(sid) { return m.provider.Read(sid) } - sid = m.sessionId() + sid = m.sessionID() sess, err := m.provider.Read(sid) if err != nil { return nil, err @@ -282,6 +301,11 @@ func (m *Manager) Start(ctx *macaron.Context) (RawStore, error) { // Read returns raw session store by session ID. func (m *Manager) Read(sid string) (RawStore, error) { + // Ensure we're trying to read a valid session ID + if _, err := m.validSessionID(sid); err != nil { + return nil, err + } + return m.provider.Read(sid) } @@ -292,6 +316,10 @@ func (m *Manager) Destory(ctx *macaron.Context) error { return nil } + if _, err := m.validSessionID(sid); err != nil { + return err + } + if err := m.provider.Destory(sid); err != nil { return err } @@ -308,8 +336,12 @@ func (m *Manager) Destory(ctx *macaron.Context) error { // RegenerateId regenerates a session store from old session ID to new one. func (m *Manager) RegenerateId(ctx *macaron.Context) (sess RawStore, err error) { - sid := m.sessionId() + sid := m.sessionID() oldsid := ctx.GetCookie(m.opt.CookieName) + _, err = m.validSessionID(oldsid) + if err != nil { + return nil, err + } sess, err = m.provider.Regenerate(oldsid, sid) if err != nil { return nil, err diff --git a/vendor/github.com/go-macaron/session/utils.go b/vendor/github.com/go-macaron/session/utils.go index 07a1283df94..90ca38064be 100644 --- a/vendor/github.com/go-macaron/session/utils.go +++ b/vendor/github.com/go-macaron/session/utils.go @@ -50,11 +50,14 @@ func DecodeGob(encoded []byte) (out map[interface{}]interface{}, err error) { return out, err } +// NOTE: A local copy in case of underlying package change +var alphanum = []byte("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") + // generateRandomKey creates a random key with the given strength. func generateRandomKey(strength int) []byte { k := make([]byte, strength) if n, err := io.ReadFull(rand.Reader, k); n != strength || err != nil { - return com.RandomCreateBytes(strength) + return com.RandomCreateBytes(strength, alphanum...) } return k } diff --git a/vendor/google.golang.org/appengine/LICENSE b/vendor/google.golang.org/appengine/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/vendor/google.golang.org/appengine/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/google.golang.org/appengine/appengine.go b/vendor/google.golang.org/appengine/appengine.go new file mode 100644 index 00000000000..475cf2e32e8 --- /dev/null +++ b/vendor/google.golang.org/appengine/appengine.go @@ -0,0 +1,112 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// Package appengine provides basic functionality for Google App Engine. +// +// For more information on how to write Go apps for Google App Engine, see: +// https://cloud.google.com/appengine/docs/go/ +package appengine // import "google.golang.org/appengine" + +import ( + "net/http" + + "github.com/golang/protobuf/proto" + "golang.org/x/net/context" + + "google.golang.org/appengine/internal" +) + +// The gophers party all night; the rabbits provide the beats. + +// Main is the principal entry point for an app running in App Engine. +// +// On App Engine Flexible it installs a trivial health checker if one isn't +// already registered, and starts listening on port 8080 (overridden by the +// $PORT environment variable). +// +// See https://cloud.google.com/appengine/docs/flexible/custom-runtimes#health_check_requests +// for details on how to do your own health checking. +// +// Main is not yet supported on App Engine Standard. +// +// Main never returns. +// +// Main is designed so that the app's main package looks like this: +// +// package main +// +// import ( +// "google.golang.org/appengine" +// +// _ "myapp/package0" +// _ "myapp/package1" +// ) +// +// func main() { +// appengine.Main() +// } +// +// The "myapp/packageX" packages are expected to register HTTP handlers +// in their init functions. +func Main() { + internal.Main() +} + +// IsDevAppServer reports whether the App Engine app is running in the +// development App Server. +func IsDevAppServer() bool { + return internal.IsDevAppServer() +} + +// NewContext returns a context for an in-flight HTTP request. +// This function is cheap. +func NewContext(req *http.Request) context.Context { + return WithContext(context.Background(), req) +} + +// WithContext returns a copy of the parent context +// and associates it with an in-flight HTTP request. +// This function is cheap. +func WithContext(parent context.Context, req *http.Request) context.Context { + return internal.WithContext(parent, req) +} + +// TODO(dsymonds): Add a Call function here? Otherwise other packages can't access internal.Call. + +// BlobKey is a key for a blobstore blob. +// +// Conceptually, this type belongs in the blobstore package, but it lives in +// the appengine package to avoid a circular dependency: blobstore depends on +// datastore, and datastore needs to refer to the BlobKey type. +type BlobKey string + +// GeoPoint represents a location as latitude/longitude in degrees. +type GeoPoint struct { + Lat, Lng float64 +} + +// Valid returns whether a GeoPoint is within [-90, 90] latitude and [-180, 180] longitude. +func (g GeoPoint) Valid() bool { + return -90 <= g.Lat && g.Lat <= 90 && -180 <= g.Lng && g.Lng <= 180 +} + +// APICallFunc defines a function type for handling an API call. +// See WithCallOverride. +type APICallFunc func(ctx context.Context, service, method string, in, out proto.Message) error + +// WithAPICallFunc returns a copy of the parent context +// that will cause API calls to invoke f instead of their normal operation. +// +// This is intended for advanced users only. +func WithAPICallFunc(ctx context.Context, f APICallFunc) context.Context { + return internal.WithCallOverride(ctx, internal.CallOverrideFunc(f)) +} + +// APICall performs an API call. +// +// This is not intended for general use; it is exported for use in conjunction +// with WithAPICallFunc. +func APICall(ctx context.Context, service, method string, in, out proto.Message) error { + return internal.Call(ctx, service, method, in, out) +} diff --git a/vendor/google.golang.org/appengine/appengine_vm.go b/vendor/google.golang.org/appengine/appengine_vm.go new file mode 100644 index 00000000000..f4b645aad3b --- /dev/null +++ b/vendor/google.golang.org/appengine/appengine_vm.go @@ -0,0 +1,20 @@ +// Copyright 2015 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// +build !appengine + +package appengine + +import ( + "golang.org/x/net/context" + + "google.golang.org/appengine/internal" +) + +// BackgroundContext returns a context not associated with a request. +// This should only be used when not servicing a request. +// This only works in App Engine "flexible environment". +func BackgroundContext() context.Context { + return internal.BackgroundContext() +} diff --git a/vendor/google.golang.org/appengine/cloudsql/cloudsql.go b/vendor/google.golang.org/appengine/cloudsql/cloudsql.go new file mode 100644 index 00000000000..7b27e6b12d4 --- /dev/null +++ b/vendor/google.golang.org/appengine/cloudsql/cloudsql.go @@ -0,0 +1,62 @@ +// Copyright 2013 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +/* +Package cloudsql exposes access to Google Cloud SQL databases. + +This package does not work in App Engine "flexible environment". + +This package is intended for MySQL drivers to make App Engine-specific +connections. Applications should use this package through database/sql: +Select a pure Go MySQL driver that supports this package, and use sql.Open +with protocol "cloudsql" and an address of the Cloud SQL instance. + +A Go MySQL driver that has been tested to work well with Cloud SQL +is the go-sql-driver: + import "database/sql" + import _ "github.com/go-sql-driver/mysql" + + db, err := sql.Open("mysql", "user@cloudsql(project-id:instance-name)/dbname") + + +Another driver that works well with Cloud SQL is the mymysql driver: + import "database/sql" + import _ "github.com/ziutek/mymysql/godrv" + + db, err := sql.Open("mymysql", "cloudsql:instance-name*dbname/user/password") + + +Using either of these drivers, you can perform a standard SQL query. +This example assumes there is a table named 'users' with +columns 'first_name' and 'last_name': + + rows, err := db.Query("SELECT first_name, last_name FROM users") + if err != nil { + log.Errorf(ctx, "db.Query: %v", err) + } + defer rows.Close() + + for rows.Next() { + var firstName string + var lastName string + if err := rows.Scan(&firstName, &lastName); err != nil { + log.Errorf(ctx, "rows.Scan: %v", err) + continue + } + log.Infof(ctx, "First: %v - Last: %v", firstName, lastName) + } + if err := rows.Err(); err != nil { + log.Errorf(ctx, "Row error: %v", err) + } +*/ +package cloudsql + +import ( + "net" +) + +// Dial connects to the named Cloud SQL instance. +func Dial(instance string) (net.Conn, error) { + return connect(instance) +} diff --git a/vendor/google.golang.org/appengine/cloudsql/cloudsql_classic.go b/vendor/google.golang.org/appengine/cloudsql/cloudsql_classic.go new file mode 100644 index 00000000000..af62dba1467 --- /dev/null +++ b/vendor/google.golang.org/appengine/cloudsql/cloudsql_classic.go @@ -0,0 +1,17 @@ +// Copyright 2013 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// +build appengine + +package cloudsql + +import ( + "net" + + "appengine/cloudsql" +) + +func connect(instance string) (net.Conn, error) { + return cloudsql.Dial(instance) +} diff --git a/vendor/google.golang.org/appengine/cloudsql/cloudsql_vm.go b/vendor/google.golang.org/appengine/cloudsql/cloudsql_vm.go new file mode 100644 index 00000000000..90fa7b31e08 --- /dev/null +++ b/vendor/google.golang.org/appengine/cloudsql/cloudsql_vm.go @@ -0,0 +1,16 @@ +// Copyright 2013 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// +build !appengine + +package cloudsql + +import ( + "errors" + "net" +) + +func connect(instance string) (net.Conn, error) { + return nil, errors.New(`cloudsql: not supported in App Engine "flexible environment"`) +} diff --git a/vendor/google.golang.org/appengine/errors.go b/vendor/google.golang.org/appengine/errors.go new file mode 100644 index 00000000000..16d0772e2a4 --- /dev/null +++ b/vendor/google.golang.org/appengine/errors.go @@ -0,0 +1,46 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// This file provides error functions for common API failure modes. + +package appengine + +import ( + "fmt" + + "google.golang.org/appengine/internal" +) + +// IsOverQuota reports whether err represents an API call failure +// due to insufficient available quota. +func IsOverQuota(err error) bool { + callErr, ok := err.(*internal.CallError) + return ok && callErr.Code == 4 +} + +// MultiError is returned by batch operations when there are errors with +// particular elements. Errors will be in a one-to-one correspondence with +// the input elements; successful elements will have a nil entry. +type MultiError []error + +func (m MultiError) Error() string { + s, n := "", 0 + for _, e := range m { + if e != nil { + if n == 0 { + s = e.Error() + } + n++ + } + } + switch n { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, n-1) +} diff --git a/vendor/google.golang.org/appengine/identity.go b/vendor/google.golang.org/appengine/identity.go new file mode 100644 index 00000000000..b8dcf8f3619 --- /dev/null +++ b/vendor/google.golang.org/appengine/identity.go @@ -0,0 +1,142 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package appengine + +import ( + "time" + + "golang.org/x/net/context" + + "google.golang.org/appengine/internal" + pb "google.golang.org/appengine/internal/app_identity" + modpb "google.golang.org/appengine/internal/modules" +) + +// AppID returns the application ID for the current application. +// The string will be a plain application ID (e.g. "appid"), with a +// domain prefix for custom domain deployments (e.g. "example.com:appid"). +func AppID(c context.Context) string { return internal.AppID(c) } + +// DefaultVersionHostname returns the standard hostname of the default version +// of the current application (e.g. "my-app.appspot.com"). This is suitable for +// use in constructing URLs. +func DefaultVersionHostname(c context.Context) string { + return internal.DefaultVersionHostname(c) +} + +// ModuleName returns the module name of the current instance. +func ModuleName(c context.Context) string { + return internal.ModuleName(c) +} + +// ModuleHostname returns a hostname of a module instance. +// If module is the empty string, it refers to the module of the current instance. +// If version is empty, it refers to the version of the current instance if valid, +// or the default version of the module of the current instance. +// If instance is empty, ModuleHostname returns the load-balancing hostname. +func ModuleHostname(c context.Context, module, version, instance string) (string, error) { + req := &modpb.GetHostnameRequest{} + if module != "" { + req.Module = &module + } + if version != "" { + req.Version = &version + } + if instance != "" { + req.Instance = &instance + } + res := &modpb.GetHostnameResponse{} + if err := internal.Call(c, "modules", "GetHostname", req, res); err != nil { + return "", err + } + return *res.Hostname, nil +} + +// VersionID returns the version ID for the current application. +// It will be of the form "X.Y", where X is specified in app.yaml, +// and Y is a number generated when each version of the app is uploaded. +// It does not include a module name. +func VersionID(c context.Context) string { return internal.VersionID(c) } + +// InstanceID returns a mostly-unique identifier for this instance. +func InstanceID() string { return internal.InstanceID() } + +// Datacenter returns an identifier for the datacenter that the instance is running in. +func Datacenter(c context.Context) string { return internal.Datacenter(c) } + +// ServerSoftware returns the App Engine release version. +// In production, it looks like "Google App Engine/X.Y.Z". +// In the development appserver, it looks like "Development/X.Y". +func ServerSoftware() string { return internal.ServerSoftware() } + +// RequestID returns a string that uniquely identifies the request. +func RequestID(c context.Context) string { return internal.RequestID(c) } + +// AccessToken generates an OAuth2 access token for the specified scopes on +// behalf of service account of this application. This token will expire after +// the returned time. +func AccessToken(c context.Context, scopes ...string) (token string, expiry time.Time, err error) { + req := &pb.GetAccessTokenRequest{Scope: scopes} + res := &pb.GetAccessTokenResponse{} + + err = internal.Call(c, "app_identity_service", "GetAccessToken", req, res) + if err != nil { + return "", time.Time{}, err + } + return res.GetAccessToken(), time.Unix(res.GetExpirationTime(), 0), nil +} + +// Certificate represents a public certificate for the app. +type Certificate struct { + KeyName string + Data []byte // PEM-encoded X.509 certificate +} + +// PublicCertificates retrieves the public certificates for the app. +// They can be used to verify a signature returned by SignBytes. +func PublicCertificates(c context.Context) ([]Certificate, error) { + req := &pb.GetPublicCertificateForAppRequest{} + res := &pb.GetPublicCertificateForAppResponse{} + if err := internal.Call(c, "app_identity_service", "GetPublicCertificatesForApp", req, res); err != nil { + return nil, err + } + var cs []Certificate + for _, pc := range res.PublicCertificateList { + cs = append(cs, Certificate{ + KeyName: pc.GetKeyName(), + Data: []byte(pc.GetX509CertificatePem()), + }) + } + return cs, nil +} + +// ServiceAccount returns a string representing the service account name, in +// the form of an email address (typically app_id@appspot.gserviceaccount.com). +func ServiceAccount(c context.Context) (string, error) { + req := &pb.GetServiceAccountNameRequest{} + res := &pb.GetServiceAccountNameResponse{} + + err := internal.Call(c, "app_identity_service", "GetServiceAccountName", req, res) + if err != nil { + return "", err + } + return res.GetServiceAccountName(), err +} + +// SignBytes signs bytes using a private key unique to your application. +func SignBytes(c context.Context, bytes []byte) (keyName string, signature []byte, err error) { + req := &pb.SignForAppRequest{BytesToSign: bytes} + res := &pb.SignForAppResponse{} + + if err := internal.Call(c, "app_identity_service", "SignForApp", req, res); err != nil { + return "", nil, err + } + return res.GetKeyName(), res.GetSignatureBytes(), nil +} + +func init() { + internal.RegisterErrorCodeMap("app_identity_service", pb.AppIdentityServiceError_ErrorCode_name) + internal.RegisterErrorCodeMap("modules", modpb.ModulesServiceError_ErrorCode_name) +} diff --git a/vendor/google.golang.org/appengine/internal/api.go b/vendor/google.golang.org/appengine/internal/api.go new file mode 100644 index 00000000000..ec5aa59b39b --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/api.go @@ -0,0 +1,646 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// +build !appengine + +package internal + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "net/url" + "os" + "runtime" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/golang/protobuf/proto" + netcontext "golang.org/x/net/context" + + basepb "google.golang.org/appengine/internal/base" + logpb "google.golang.org/appengine/internal/log" + remotepb "google.golang.org/appengine/internal/remote_api" +) + +const ( + apiPath = "/rpc_http" +) + +var ( + // Incoming headers. + ticketHeader = http.CanonicalHeaderKey("X-AppEngine-API-Ticket") + dapperHeader = http.CanonicalHeaderKey("X-Google-DapperTraceInfo") + traceHeader = http.CanonicalHeaderKey("X-Cloud-Trace-Context") + curNamespaceHeader = http.CanonicalHeaderKey("X-AppEngine-Current-Namespace") + userIPHeader = http.CanonicalHeaderKey("X-AppEngine-User-IP") + remoteAddrHeader = http.CanonicalHeaderKey("X-AppEngine-Remote-Addr") + + // Outgoing headers. + apiEndpointHeader = http.CanonicalHeaderKey("X-Google-RPC-Service-Endpoint") + apiEndpointHeaderValue = []string{"app-engine-apis"} + apiMethodHeader = http.CanonicalHeaderKey("X-Google-RPC-Service-Method") + apiMethodHeaderValue = []string{"/VMRemoteAPI.CallRemoteAPI"} + apiDeadlineHeader = http.CanonicalHeaderKey("X-Google-RPC-Service-Deadline") + apiContentType = http.CanonicalHeaderKey("Content-Type") + apiContentTypeValue = []string{"application/octet-stream"} + logFlushHeader = http.CanonicalHeaderKey("X-AppEngine-Log-Flush-Count") + + apiHTTPClient = &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: limitDial, + }, + } +) + +func apiURL() *url.URL { + host, port := "appengine.googleapis.internal", "10001" + if h := os.Getenv("API_HOST"); h != "" { + host = h + } + if p := os.Getenv("API_PORT"); p != "" { + port = p + } + return &url.URL{ + Scheme: "http", + Host: host + ":" + port, + Path: apiPath, + } +} + +func handleHTTP(w http.ResponseWriter, r *http.Request) { + c := &context{ + req: r, + outHeader: w.Header(), + apiURL: apiURL(), + } + stopFlushing := make(chan int) + + ctxs.Lock() + ctxs.m[r] = c + ctxs.Unlock() + defer func() { + ctxs.Lock() + delete(ctxs.m, r) + ctxs.Unlock() + }() + + // Patch up RemoteAddr so it looks reasonable. + if addr := r.Header.Get(userIPHeader); addr != "" { + r.RemoteAddr = addr + } else if addr = r.Header.Get(remoteAddrHeader); addr != "" { + r.RemoteAddr = addr + } else { + // Should not normally reach here, but pick a sensible default anyway. + r.RemoteAddr = "127.0.0.1" + } + // The address in the headers will most likely be of these forms: + // 123.123.123.123 + // 2001:db8::1 + // net/http.Request.RemoteAddr is specified to be in "IP:port" form. + if _, _, err := net.SplitHostPort(r.RemoteAddr); err != nil { + // Assume the remote address is only a host; add a default port. + r.RemoteAddr = net.JoinHostPort(r.RemoteAddr, "80") + } + + // Start goroutine responsible for flushing app logs. + // This is done after adding c to ctx.m (and stopped before removing it) + // because flushing logs requires making an API call. + go c.logFlusher(stopFlushing) + + executeRequestSafely(c, r) + c.outHeader = nil // make sure header changes aren't respected any more + + stopFlushing <- 1 // any logging beyond this point will be dropped + + // Flush any pending logs asynchronously. + c.pendingLogs.Lock() + flushes := c.pendingLogs.flushes + if len(c.pendingLogs.lines) > 0 { + flushes++ + } + c.pendingLogs.Unlock() + go c.flushLog(false) + w.Header().Set(logFlushHeader, strconv.Itoa(flushes)) + + // Avoid nil Write call if c.Write is never called. + if c.outCode != 0 { + w.WriteHeader(c.outCode) + } + if c.outBody != nil { + w.Write(c.outBody) + } +} + +func executeRequestSafely(c *context, r *http.Request) { + defer func() { + if x := recover(); x != nil { + logf(c, 4, "%s", renderPanic(x)) // 4 == critical + c.outCode = 500 + } + }() + + http.DefaultServeMux.ServeHTTP(c, r) +} + +func renderPanic(x interface{}) string { + buf := make([]byte, 16<<10) // 16 KB should be plenty + buf = buf[:runtime.Stack(buf, false)] + + // Remove the first few stack frames: + // this func + // the recover closure in the caller + // That will root the stack trace at the site of the panic. + const ( + skipStart = "internal.renderPanic" + skipFrames = 2 + ) + start := bytes.Index(buf, []byte(skipStart)) + p := start + for i := 0; i < skipFrames*2 && p+1 < len(buf); i++ { + p = bytes.IndexByte(buf[p+1:], '\n') + p + 1 + if p < 0 { + break + } + } + if p >= 0 { + // buf[start:p+1] is the block to remove. + // Copy buf[p+1:] over buf[start:] and shrink buf. + copy(buf[start:], buf[p+1:]) + buf = buf[:len(buf)-(p+1-start)] + } + + // Add panic heading. + head := fmt.Sprintf("panic: %v\n\n", x) + if len(head) > len(buf) { + // Extremely unlikely to happen. + return head + } + copy(buf[len(head):], buf) + copy(buf, head) + + return string(buf) +} + +var ctxs = struct { + sync.Mutex + m map[*http.Request]*context + bg *context // background context, lazily initialized + // dec is used by tests to decorate the netcontext.Context returned + // for a given request. This allows tests to add overrides (such as + // WithAppIDOverride) to the context. The map is nil outside tests. + dec map[*http.Request]func(netcontext.Context) netcontext.Context +}{ + m: make(map[*http.Request]*context), +} + +// context represents the context of an in-flight HTTP request. +// It implements the appengine.Context and http.ResponseWriter interfaces. +type context struct { + req *http.Request + + outCode int + outHeader http.Header + outBody []byte + + pendingLogs struct { + sync.Mutex + lines []*logpb.UserAppLogLine + flushes int + } + + apiURL *url.URL +} + +var contextKey = "holds a *context" + +func fromContext(ctx netcontext.Context) *context { + c, _ := ctx.Value(&contextKey).(*context) + return c +} + +func withContext(parent netcontext.Context, c *context) netcontext.Context { + ctx := netcontext.WithValue(parent, &contextKey, c) + if ns := c.req.Header.Get(curNamespaceHeader); ns != "" { + ctx = withNamespace(ctx, ns) + } + return ctx +} + +func toContext(c *context) netcontext.Context { + return withContext(netcontext.Background(), c) +} + +func IncomingHeaders(ctx netcontext.Context) http.Header { + if c := fromContext(ctx); c != nil { + return c.req.Header + } + return nil +} + +func WithContext(parent netcontext.Context, req *http.Request) netcontext.Context { + ctxs.Lock() + c := ctxs.m[req] + d := ctxs.dec[req] + ctxs.Unlock() + + if d != nil { + parent = d(parent) + } + + if c == nil { + // Someone passed in an http.Request that is not in-flight. + // We panic here rather than panicking at a later point + // so that stack traces will be more sensible. + log.Panic("appengine: NewContext passed an unknown http.Request") + } + return withContext(parent, c) +} + +func BackgroundContext() netcontext.Context { + ctxs.Lock() + defer ctxs.Unlock() + + if ctxs.bg != nil { + return toContext(ctxs.bg) + } + + // Compute background security ticket. + appID := partitionlessAppID() + escAppID := strings.Replace(strings.Replace(appID, ":", "_", -1), ".", "_", -1) + majVersion := VersionID(nil) + if i := strings.Index(majVersion, "."); i > 0 { + majVersion = majVersion[:i] + } + ticket := fmt.Sprintf("%s/%s.%s.%s", escAppID, ModuleName(nil), majVersion, InstanceID()) + + ctxs.bg = &context{ + req: &http.Request{ + Header: http.Header{ + ticketHeader: []string{ticket}, + }, + }, + apiURL: apiURL(), + } + + // TODO(dsymonds): Wire up the shutdown handler to do a final flush. + go ctxs.bg.logFlusher(make(chan int)) + + return toContext(ctxs.bg) +} + +// RegisterTestRequest registers the HTTP request req for testing, such that +// any API calls are sent to the provided URL. It returns a closure to delete +// the registration. +// It should only be used by aetest package. +func RegisterTestRequest(req *http.Request, apiURL *url.URL, decorate func(netcontext.Context) netcontext.Context) func() { + c := &context{ + req: req, + apiURL: apiURL, + } + ctxs.Lock() + defer ctxs.Unlock() + if _, ok := ctxs.m[req]; ok { + log.Panic("req already associated with context") + } + if _, ok := ctxs.dec[req]; ok { + log.Panic("req already associated with context") + } + if ctxs.dec == nil { + ctxs.dec = make(map[*http.Request]func(netcontext.Context) netcontext.Context) + } + ctxs.m[req] = c + ctxs.dec[req] = decorate + + return func() { + ctxs.Lock() + delete(ctxs.m, req) + delete(ctxs.dec, req) + ctxs.Unlock() + } +} + +var errTimeout = &CallError{ + Detail: "Deadline exceeded", + Code: int32(remotepb.RpcError_CANCELLED), + Timeout: true, +} + +func (c *context) Header() http.Header { return c.outHeader } + +// Copied from $GOROOT/src/pkg/net/http/transfer.go. Some response status +// codes do not permit a response body (nor response entity headers such as +// Content-Length, Content-Type, etc). +func bodyAllowedForStatus(status int) bool { + switch { + case status >= 100 && status <= 199: + return false + case status == 204: + return false + case status == 304: + return false + } + return true +} + +func (c *context) Write(b []byte) (int, error) { + if c.outCode == 0 { + c.WriteHeader(http.StatusOK) + } + if len(b) > 0 && !bodyAllowedForStatus(c.outCode) { + return 0, http.ErrBodyNotAllowed + } + c.outBody = append(c.outBody, b...) + return len(b), nil +} + +func (c *context) WriteHeader(code int) { + if c.outCode != 0 { + logf(c, 3, "WriteHeader called multiple times on request.") // error level + return + } + c.outCode = code +} + +func (c *context) post(body []byte, timeout time.Duration) (b []byte, err error) { + hreq := &http.Request{ + Method: "POST", + URL: c.apiURL, + Header: http.Header{ + apiEndpointHeader: apiEndpointHeaderValue, + apiMethodHeader: apiMethodHeaderValue, + apiContentType: apiContentTypeValue, + apiDeadlineHeader: []string{strconv.FormatFloat(timeout.Seconds(), 'f', -1, 64)}, + }, + Body: ioutil.NopCloser(bytes.NewReader(body)), + ContentLength: int64(len(body)), + Host: c.apiURL.Host, + } + if info := c.req.Header.Get(dapperHeader); info != "" { + hreq.Header.Set(dapperHeader, info) + } + if info := c.req.Header.Get(traceHeader); info != "" { + hreq.Header.Set(traceHeader, info) + } + + tr := apiHTTPClient.Transport.(*http.Transport) + + var timedOut int32 // atomic; set to 1 if timed out + t := time.AfterFunc(timeout, func() { + atomic.StoreInt32(&timedOut, 1) + tr.CancelRequest(hreq) + }) + defer t.Stop() + defer func() { + // Check if timeout was exceeded. + if atomic.LoadInt32(&timedOut) != 0 { + err = errTimeout + } + }() + + hresp, err := apiHTTPClient.Do(hreq) + if err != nil { + return nil, &CallError{ + Detail: fmt.Sprintf("service bridge HTTP failed: %v", err), + Code: int32(remotepb.RpcError_UNKNOWN), + } + } + defer hresp.Body.Close() + hrespBody, err := ioutil.ReadAll(hresp.Body) + if hresp.StatusCode != 200 { + return nil, &CallError{ + Detail: fmt.Sprintf("service bridge returned HTTP %d (%q)", hresp.StatusCode, hrespBody), + Code: int32(remotepb.RpcError_UNKNOWN), + } + } + if err != nil { + return nil, &CallError{ + Detail: fmt.Sprintf("service bridge response bad: %v", err), + Code: int32(remotepb.RpcError_UNKNOWN), + } + } + return hrespBody, nil +} + +func Call(ctx netcontext.Context, service, method string, in, out proto.Message) error { + if ns := NamespaceFromContext(ctx); ns != "" { + if fn, ok := NamespaceMods[service]; ok { + fn(in, ns) + } + } + + if f, ctx, ok := callOverrideFromContext(ctx); ok { + return f(ctx, service, method, in, out) + } + + // Handle already-done contexts quickly. + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + c := fromContext(ctx) + if c == nil { + // Give a good error message rather than a panic lower down. + return errors.New("not an App Engine context") + } + + // Apply transaction modifications if we're in a transaction. + if t := transactionFromContext(ctx); t != nil { + if t.finished { + return errors.New("transaction context has expired") + } + applyTransaction(in, &t.transaction) + } + + // Default RPC timeout is 60s. + timeout := 60 * time.Second + if deadline, ok := ctx.Deadline(); ok { + timeout = deadline.Sub(time.Now()) + } + + data, err := proto.Marshal(in) + if err != nil { + return err + } + + ticket := c.req.Header.Get(ticketHeader) + req := &remotepb.Request{ + ServiceName: &service, + Method: &method, + Request: data, + RequestId: &ticket, + } + hreqBody, err := proto.Marshal(req) + if err != nil { + return err + } + + hrespBody, err := c.post(hreqBody, timeout) + if err != nil { + return err + } + + res := &remotepb.Response{} + if err := proto.Unmarshal(hrespBody, res); err != nil { + return err + } + if res.RpcError != nil { + ce := &CallError{ + Detail: res.RpcError.GetDetail(), + Code: *res.RpcError.Code, + } + switch remotepb.RpcError_ErrorCode(ce.Code) { + case remotepb.RpcError_CANCELLED, remotepb.RpcError_DEADLINE_EXCEEDED: + ce.Timeout = true + } + return ce + } + if res.ApplicationError != nil { + return &APIError{ + Service: *req.ServiceName, + Detail: res.ApplicationError.GetDetail(), + Code: *res.ApplicationError.Code, + } + } + if res.Exception != nil || res.JavaException != nil { + // This shouldn't happen, but let's be defensive. + return &CallError{ + Detail: "service bridge returned exception", + Code: int32(remotepb.RpcError_UNKNOWN), + } + } + return proto.Unmarshal(res.Response, out) +} + +func (c *context) Request() *http.Request { + return c.req +} + +func (c *context) addLogLine(ll *logpb.UserAppLogLine) { + // Truncate long log lines. + // TODO(dsymonds): Check if this is still necessary. + const lim = 8 << 10 + if len(*ll.Message) > lim { + suffix := fmt.Sprintf("...(length %d)", len(*ll.Message)) + ll.Message = proto.String((*ll.Message)[:lim-len(suffix)] + suffix) + } + + c.pendingLogs.Lock() + c.pendingLogs.lines = append(c.pendingLogs.lines, ll) + c.pendingLogs.Unlock() +} + +var logLevelName = map[int64]string{ + 0: "DEBUG", + 1: "INFO", + 2: "WARNING", + 3: "ERROR", + 4: "CRITICAL", +} + +func logf(c *context, level int64, format string, args ...interface{}) { + s := fmt.Sprintf(format, args...) + s = strings.TrimRight(s, "\n") // Remove any trailing newline characters. + c.addLogLine(&logpb.UserAppLogLine{ + TimestampUsec: proto.Int64(time.Now().UnixNano() / 1e3), + Level: &level, + Message: &s, + }) + log.Print(logLevelName[level] + ": " + s) +} + +// flushLog attempts to flush any pending logs to the appserver. +// It should not be called concurrently. +func (c *context) flushLog(force bool) (flushed bool) { + c.pendingLogs.Lock() + // Grab up to 30 MB. We can get away with up to 32 MB, but let's be cautious. + n, rem := 0, 30<<20 + for ; n < len(c.pendingLogs.lines); n++ { + ll := c.pendingLogs.lines[n] + // Each log line will require about 3 bytes of overhead. + nb := proto.Size(ll) + 3 + if nb > rem { + break + } + rem -= nb + } + lines := c.pendingLogs.lines[:n] + c.pendingLogs.lines = c.pendingLogs.lines[n:] + c.pendingLogs.Unlock() + + if len(lines) == 0 && !force { + // Nothing to flush. + return false + } + + rescueLogs := false + defer func() { + if rescueLogs { + c.pendingLogs.Lock() + c.pendingLogs.lines = append(lines, c.pendingLogs.lines...) + c.pendingLogs.Unlock() + } + }() + + buf, err := proto.Marshal(&logpb.UserAppLogGroup{ + LogLine: lines, + }) + if err != nil { + log.Printf("internal.flushLog: marshaling UserAppLogGroup: %v", err) + rescueLogs = true + return false + } + + req := &logpb.FlushRequest{ + Logs: buf, + } + res := &basepb.VoidProto{} + c.pendingLogs.Lock() + c.pendingLogs.flushes++ + c.pendingLogs.Unlock() + if err := Call(toContext(c), "logservice", "Flush", req, res); err != nil { + log.Printf("internal.flushLog: Flush RPC: %v", err) + rescueLogs = true + return false + } + return true +} + +const ( + // Log flushing parameters. + flushInterval = 1 * time.Second + forceFlushInterval = 60 * time.Second +) + +func (c *context) logFlusher(stop <-chan int) { + lastFlush := time.Now() + tick := time.NewTicker(flushInterval) + for { + select { + case <-stop: + // Request finished. + tick.Stop() + return + case <-tick.C: + force := time.Now().Sub(lastFlush) > forceFlushInterval + if c.flushLog(force) { + lastFlush = time.Now() + } + } + } +} + +func ContextForTesting(req *http.Request) netcontext.Context { + return toContext(&context{req: req}) +} diff --git a/vendor/google.golang.org/appengine/internal/api_classic.go b/vendor/google.golang.org/appengine/internal/api_classic.go new file mode 100644 index 00000000000..597f66e6ea2 --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/api_classic.go @@ -0,0 +1,159 @@ +// Copyright 2015 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// +build appengine + +package internal + +import ( + "errors" + "fmt" + "net/http" + "time" + + "appengine" + "appengine_internal" + basepb "appengine_internal/base" + + "github.com/golang/protobuf/proto" + netcontext "golang.org/x/net/context" +) + +var contextKey = "holds an appengine.Context" + +func fromContext(ctx netcontext.Context) appengine.Context { + c, _ := ctx.Value(&contextKey).(appengine.Context) + return c +} + +// This is only for classic App Engine adapters. +func ClassicContextFromContext(ctx netcontext.Context) appengine.Context { + return fromContext(ctx) +} + +func withContext(parent netcontext.Context, c appengine.Context) netcontext.Context { + ctx := netcontext.WithValue(parent, &contextKey, c) + + s := &basepb.StringProto{} + c.Call("__go__", "GetNamespace", &basepb.VoidProto{}, s, nil) + if ns := s.GetValue(); ns != "" { + ctx = NamespacedContext(ctx, ns) + } + + return ctx +} + +func IncomingHeaders(ctx netcontext.Context) http.Header { + if c := fromContext(ctx); c != nil { + if req, ok := c.Request().(*http.Request); ok { + return req.Header + } + } + return nil +} + +func WithContext(parent netcontext.Context, req *http.Request) netcontext.Context { + c := appengine.NewContext(req) + return withContext(parent, c) +} + +type testingContext struct { + appengine.Context + + req *http.Request +} + +func (t *testingContext) FullyQualifiedAppID() string { return "dev~testcontext" } +func (t *testingContext) Call(service, method string, _, _ appengine_internal.ProtoMessage, _ *appengine_internal.CallOptions) error { + if service == "__go__" && method == "GetNamespace" { + return nil + } + return fmt.Errorf("testingContext: unsupported Call") +} +func (t *testingContext) Request() interface{} { return t.req } + +func ContextForTesting(req *http.Request) netcontext.Context { + return withContext(netcontext.Background(), &testingContext{req: req}) +} + +func Call(ctx netcontext.Context, service, method string, in, out proto.Message) error { + if ns := NamespaceFromContext(ctx); ns != "" { + if fn, ok := NamespaceMods[service]; ok { + fn(in, ns) + } + } + + if f, ctx, ok := callOverrideFromContext(ctx); ok { + return f(ctx, service, method, in, out) + } + + // Handle already-done contexts quickly. + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + c := fromContext(ctx) + if c == nil { + // Give a good error message rather than a panic lower down. + return errors.New("not an App Engine context") + } + + // Apply transaction modifications if we're in a transaction. + if t := transactionFromContext(ctx); t != nil { + if t.finished { + return errors.New("transaction context has expired") + } + applyTransaction(in, &t.transaction) + } + + var opts *appengine_internal.CallOptions + if d, ok := ctx.Deadline(); ok { + opts = &appengine_internal.CallOptions{ + Timeout: d.Sub(time.Now()), + } + } + + err := c.Call(service, method, in, out, opts) + switch v := err.(type) { + case *appengine_internal.APIError: + return &APIError{ + Service: v.Service, + Detail: v.Detail, + Code: v.Code, + } + case *appengine_internal.CallError: + return &CallError{ + Detail: v.Detail, + Code: v.Code, + Timeout: v.Timeout, + } + } + return err +} + +func handleHTTP(w http.ResponseWriter, r *http.Request) { + panic("handleHTTP called; this should be impossible") +} + +func logf(c appengine.Context, level int64, format string, args ...interface{}) { + var fn func(format string, args ...interface{}) + switch level { + case 0: + fn = c.Debugf + case 1: + fn = c.Infof + case 2: + fn = c.Warningf + case 3: + fn = c.Errorf + case 4: + fn = c.Criticalf + default: + // This shouldn't happen. + fn = c.Criticalf + } + fn(format, args...) +} diff --git a/vendor/google.golang.org/appengine/internal/api_common.go b/vendor/google.golang.org/appengine/internal/api_common.go new file mode 100644 index 00000000000..2db33a774bc --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/api_common.go @@ -0,0 +1,86 @@ +// Copyright 2015 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package internal + +import ( + "github.com/golang/protobuf/proto" + netcontext "golang.org/x/net/context" +) + +type CallOverrideFunc func(ctx netcontext.Context, service, method string, in, out proto.Message) error + +var callOverrideKey = "holds []CallOverrideFunc" + +func WithCallOverride(ctx netcontext.Context, f CallOverrideFunc) netcontext.Context { + // We avoid appending to any existing call override + // so we don't risk overwriting a popped stack below. + var cofs []CallOverrideFunc + if uf, ok := ctx.Value(&callOverrideKey).([]CallOverrideFunc); ok { + cofs = append(cofs, uf...) + } + cofs = append(cofs, f) + return netcontext.WithValue(ctx, &callOverrideKey, cofs) +} + +func callOverrideFromContext(ctx netcontext.Context) (CallOverrideFunc, netcontext.Context, bool) { + cofs, _ := ctx.Value(&callOverrideKey).([]CallOverrideFunc) + if len(cofs) == 0 { + return nil, nil, false + } + // We found a list of overrides; grab the last, and reconstitute a + // context that will hide it. + f := cofs[len(cofs)-1] + ctx = netcontext.WithValue(ctx, &callOverrideKey, cofs[:len(cofs)-1]) + return f, ctx, true +} + +type logOverrideFunc func(level int64, format string, args ...interface{}) + +var logOverrideKey = "holds a logOverrideFunc" + +func WithLogOverride(ctx netcontext.Context, f logOverrideFunc) netcontext.Context { + return netcontext.WithValue(ctx, &logOverrideKey, f) +} + +var appIDOverrideKey = "holds a string, being the full app ID" + +func WithAppIDOverride(ctx netcontext.Context, appID string) netcontext.Context { + return netcontext.WithValue(ctx, &appIDOverrideKey, appID) +} + +var namespaceKey = "holds the namespace string" + +func withNamespace(ctx netcontext.Context, ns string) netcontext.Context { + return netcontext.WithValue(ctx, &namespaceKey, ns) +} + +func NamespaceFromContext(ctx netcontext.Context) string { + // If there's no namespace, return the empty string. + ns, _ := ctx.Value(&namespaceKey).(string) + return ns +} + +// FullyQualifiedAppID returns the fully-qualified application ID. +// This may contain a partition prefix (e.g. "s~" for High Replication apps), +// or a domain prefix (e.g. "example.com:"). +func FullyQualifiedAppID(ctx netcontext.Context) string { + if id, ok := ctx.Value(&appIDOverrideKey).(string); ok { + return id + } + return fullyQualifiedAppID(ctx) +} + +func Logf(ctx netcontext.Context, level int64, format string, args ...interface{}) { + if f, ok := ctx.Value(&logOverrideKey).(logOverrideFunc); ok { + f(level, format, args...) + return + } + logf(fromContext(ctx), level, format, args...) +} + +// NamespacedContext wraps a Context to support namespaces. +func NamespacedContext(ctx netcontext.Context, namespace string) netcontext.Context { + return withNamespace(ctx, namespace) +} diff --git a/vendor/google.golang.org/appengine/internal/app_id.go b/vendor/google.golang.org/appengine/internal/app_id.go new file mode 100644 index 00000000000..11df8c07b53 --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/app_id.go @@ -0,0 +1,28 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package internal + +import ( + "strings" +) + +func parseFullAppID(appid string) (partition, domain, displayID string) { + if i := strings.Index(appid, "~"); i != -1 { + partition, appid = appid[:i], appid[i+1:] + } + if i := strings.Index(appid, ":"); i != -1 { + domain, appid = appid[:i], appid[i+1:] + } + return partition, domain, appid +} + +// appID returns "appid" or "domain.com:appid". +func appID(fullAppID string) string { + _, dom, dis := parseFullAppID(fullAppID) + if dom != "" { + return dom + ":" + dis + } + return dis +} diff --git a/vendor/google.golang.org/appengine/internal/app_identity/app_identity_service.pb.go b/vendor/google.golang.org/appengine/internal/app_identity/app_identity_service.pb.go new file mode 100644 index 00000000000..87d9701b8df --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/app_identity/app_identity_service.pb.go @@ -0,0 +1,296 @@ +// Code generated by protoc-gen-go. +// source: google.golang.org/appengine/internal/app_identity/app_identity_service.proto +// DO NOT EDIT! + +/* +Package app_identity is a generated protocol buffer package. + +It is generated from these files: + google.golang.org/appengine/internal/app_identity/app_identity_service.proto + +It has these top-level messages: + AppIdentityServiceError + SignForAppRequest + SignForAppResponse + GetPublicCertificateForAppRequest + PublicCertificate + GetPublicCertificateForAppResponse + GetServiceAccountNameRequest + GetServiceAccountNameResponse + GetAccessTokenRequest + GetAccessTokenResponse + GetDefaultGcsBucketNameRequest + GetDefaultGcsBucketNameResponse +*/ +package app_identity + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type AppIdentityServiceError_ErrorCode int32 + +const ( + AppIdentityServiceError_SUCCESS AppIdentityServiceError_ErrorCode = 0 + AppIdentityServiceError_UNKNOWN_SCOPE AppIdentityServiceError_ErrorCode = 9 + AppIdentityServiceError_BLOB_TOO_LARGE AppIdentityServiceError_ErrorCode = 1000 + AppIdentityServiceError_DEADLINE_EXCEEDED AppIdentityServiceError_ErrorCode = 1001 + AppIdentityServiceError_NOT_A_VALID_APP AppIdentityServiceError_ErrorCode = 1002 + AppIdentityServiceError_UNKNOWN_ERROR AppIdentityServiceError_ErrorCode = 1003 + AppIdentityServiceError_NOT_ALLOWED AppIdentityServiceError_ErrorCode = 1005 + AppIdentityServiceError_NOT_IMPLEMENTED AppIdentityServiceError_ErrorCode = 1006 +) + +var AppIdentityServiceError_ErrorCode_name = map[int32]string{ + 0: "SUCCESS", + 9: "UNKNOWN_SCOPE", + 1000: "BLOB_TOO_LARGE", + 1001: "DEADLINE_EXCEEDED", + 1002: "NOT_A_VALID_APP", + 1003: "UNKNOWN_ERROR", + 1005: "NOT_ALLOWED", + 1006: "NOT_IMPLEMENTED", +} +var AppIdentityServiceError_ErrorCode_value = map[string]int32{ + "SUCCESS": 0, + "UNKNOWN_SCOPE": 9, + "BLOB_TOO_LARGE": 1000, + "DEADLINE_EXCEEDED": 1001, + "NOT_A_VALID_APP": 1002, + "UNKNOWN_ERROR": 1003, + "NOT_ALLOWED": 1005, + "NOT_IMPLEMENTED": 1006, +} + +func (x AppIdentityServiceError_ErrorCode) Enum() *AppIdentityServiceError_ErrorCode { + p := new(AppIdentityServiceError_ErrorCode) + *p = x + return p +} +func (x AppIdentityServiceError_ErrorCode) String() string { + return proto.EnumName(AppIdentityServiceError_ErrorCode_name, int32(x)) +} +func (x *AppIdentityServiceError_ErrorCode) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(AppIdentityServiceError_ErrorCode_value, data, "AppIdentityServiceError_ErrorCode") + if err != nil { + return err + } + *x = AppIdentityServiceError_ErrorCode(value) + return nil +} + +type AppIdentityServiceError struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *AppIdentityServiceError) Reset() { *m = AppIdentityServiceError{} } +func (m *AppIdentityServiceError) String() string { return proto.CompactTextString(m) } +func (*AppIdentityServiceError) ProtoMessage() {} + +type SignForAppRequest struct { + BytesToSign []byte `protobuf:"bytes,1,opt,name=bytes_to_sign" json:"bytes_to_sign,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *SignForAppRequest) Reset() { *m = SignForAppRequest{} } +func (m *SignForAppRequest) String() string { return proto.CompactTextString(m) } +func (*SignForAppRequest) ProtoMessage() {} + +func (m *SignForAppRequest) GetBytesToSign() []byte { + if m != nil { + return m.BytesToSign + } + return nil +} + +type SignForAppResponse struct { + KeyName *string `protobuf:"bytes,1,opt,name=key_name" json:"key_name,omitempty"` + SignatureBytes []byte `protobuf:"bytes,2,opt,name=signature_bytes" json:"signature_bytes,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *SignForAppResponse) Reset() { *m = SignForAppResponse{} } +func (m *SignForAppResponse) String() string { return proto.CompactTextString(m) } +func (*SignForAppResponse) ProtoMessage() {} + +func (m *SignForAppResponse) GetKeyName() string { + if m != nil && m.KeyName != nil { + return *m.KeyName + } + return "" +} + +func (m *SignForAppResponse) GetSignatureBytes() []byte { + if m != nil { + return m.SignatureBytes + } + return nil +} + +type GetPublicCertificateForAppRequest struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetPublicCertificateForAppRequest) Reset() { *m = GetPublicCertificateForAppRequest{} } +func (m *GetPublicCertificateForAppRequest) String() string { return proto.CompactTextString(m) } +func (*GetPublicCertificateForAppRequest) ProtoMessage() {} + +type PublicCertificate struct { + KeyName *string `protobuf:"bytes,1,opt,name=key_name" json:"key_name,omitempty"` + X509CertificatePem *string `protobuf:"bytes,2,opt,name=x509_certificate_pem" json:"x509_certificate_pem,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *PublicCertificate) Reset() { *m = PublicCertificate{} } +func (m *PublicCertificate) String() string { return proto.CompactTextString(m) } +func (*PublicCertificate) ProtoMessage() {} + +func (m *PublicCertificate) GetKeyName() string { + if m != nil && m.KeyName != nil { + return *m.KeyName + } + return "" +} + +func (m *PublicCertificate) GetX509CertificatePem() string { + if m != nil && m.X509CertificatePem != nil { + return *m.X509CertificatePem + } + return "" +} + +type GetPublicCertificateForAppResponse struct { + PublicCertificateList []*PublicCertificate `protobuf:"bytes,1,rep,name=public_certificate_list" json:"public_certificate_list,omitempty"` + MaxClientCacheTimeInSecond *int64 `protobuf:"varint,2,opt,name=max_client_cache_time_in_second" json:"max_client_cache_time_in_second,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetPublicCertificateForAppResponse) Reset() { *m = GetPublicCertificateForAppResponse{} } +func (m *GetPublicCertificateForAppResponse) String() string { return proto.CompactTextString(m) } +func (*GetPublicCertificateForAppResponse) ProtoMessage() {} + +func (m *GetPublicCertificateForAppResponse) GetPublicCertificateList() []*PublicCertificate { + if m != nil { + return m.PublicCertificateList + } + return nil +} + +func (m *GetPublicCertificateForAppResponse) GetMaxClientCacheTimeInSecond() int64 { + if m != nil && m.MaxClientCacheTimeInSecond != nil { + return *m.MaxClientCacheTimeInSecond + } + return 0 +} + +type GetServiceAccountNameRequest struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetServiceAccountNameRequest) Reset() { *m = GetServiceAccountNameRequest{} } +func (m *GetServiceAccountNameRequest) String() string { return proto.CompactTextString(m) } +func (*GetServiceAccountNameRequest) ProtoMessage() {} + +type GetServiceAccountNameResponse struct { + ServiceAccountName *string `protobuf:"bytes,1,opt,name=service_account_name" json:"service_account_name,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetServiceAccountNameResponse) Reset() { *m = GetServiceAccountNameResponse{} } +func (m *GetServiceAccountNameResponse) String() string { return proto.CompactTextString(m) } +func (*GetServiceAccountNameResponse) ProtoMessage() {} + +func (m *GetServiceAccountNameResponse) GetServiceAccountName() string { + if m != nil && m.ServiceAccountName != nil { + return *m.ServiceAccountName + } + return "" +} + +type GetAccessTokenRequest struct { + Scope []string `protobuf:"bytes,1,rep,name=scope" json:"scope,omitempty"` + ServiceAccountId *int64 `protobuf:"varint,2,opt,name=service_account_id" json:"service_account_id,omitempty"` + ServiceAccountName *string `protobuf:"bytes,3,opt,name=service_account_name" json:"service_account_name,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetAccessTokenRequest) Reset() { *m = GetAccessTokenRequest{} } +func (m *GetAccessTokenRequest) String() string { return proto.CompactTextString(m) } +func (*GetAccessTokenRequest) ProtoMessage() {} + +func (m *GetAccessTokenRequest) GetScope() []string { + if m != nil { + return m.Scope + } + return nil +} + +func (m *GetAccessTokenRequest) GetServiceAccountId() int64 { + if m != nil && m.ServiceAccountId != nil { + return *m.ServiceAccountId + } + return 0 +} + +func (m *GetAccessTokenRequest) GetServiceAccountName() string { + if m != nil && m.ServiceAccountName != nil { + return *m.ServiceAccountName + } + return "" +} + +type GetAccessTokenResponse struct { + AccessToken *string `protobuf:"bytes,1,opt,name=access_token" json:"access_token,omitempty"` + ExpirationTime *int64 `protobuf:"varint,2,opt,name=expiration_time" json:"expiration_time,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetAccessTokenResponse) Reset() { *m = GetAccessTokenResponse{} } +func (m *GetAccessTokenResponse) String() string { return proto.CompactTextString(m) } +func (*GetAccessTokenResponse) ProtoMessage() {} + +func (m *GetAccessTokenResponse) GetAccessToken() string { + if m != nil && m.AccessToken != nil { + return *m.AccessToken + } + return "" +} + +func (m *GetAccessTokenResponse) GetExpirationTime() int64 { + if m != nil && m.ExpirationTime != nil { + return *m.ExpirationTime + } + return 0 +} + +type GetDefaultGcsBucketNameRequest struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetDefaultGcsBucketNameRequest) Reset() { *m = GetDefaultGcsBucketNameRequest{} } +func (m *GetDefaultGcsBucketNameRequest) String() string { return proto.CompactTextString(m) } +func (*GetDefaultGcsBucketNameRequest) ProtoMessage() {} + +type GetDefaultGcsBucketNameResponse struct { + DefaultGcsBucketName *string `protobuf:"bytes,1,opt,name=default_gcs_bucket_name" json:"default_gcs_bucket_name,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetDefaultGcsBucketNameResponse) Reset() { *m = GetDefaultGcsBucketNameResponse{} } +func (m *GetDefaultGcsBucketNameResponse) String() string { return proto.CompactTextString(m) } +func (*GetDefaultGcsBucketNameResponse) ProtoMessage() {} + +func (m *GetDefaultGcsBucketNameResponse) GetDefaultGcsBucketName() string { + if m != nil && m.DefaultGcsBucketName != nil { + return *m.DefaultGcsBucketName + } + return "" +} + +func init() { +} diff --git a/vendor/google.golang.org/appengine/internal/base/api_base.pb.go b/vendor/google.golang.org/appengine/internal/base/api_base.pb.go new file mode 100644 index 00000000000..36a195650a9 --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/base/api_base.pb.go @@ -0,0 +1,133 @@ +// Code generated by protoc-gen-go. +// source: google.golang.org/appengine/internal/base/api_base.proto +// DO NOT EDIT! + +/* +Package base is a generated protocol buffer package. + +It is generated from these files: + google.golang.org/appengine/internal/base/api_base.proto + +It has these top-level messages: + StringProto + Integer32Proto + Integer64Proto + BoolProto + DoubleProto + BytesProto + VoidProto +*/ +package base + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type StringProto struct { + Value *string `protobuf:"bytes,1,req,name=value" json:"value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *StringProto) Reset() { *m = StringProto{} } +func (m *StringProto) String() string { return proto.CompactTextString(m) } +func (*StringProto) ProtoMessage() {} + +func (m *StringProto) GetValue() string { + if m != nil && m.Value != nil { + return *m.Value + } + return "" +} + +type Integer32Proto struct { + Value *int32 `protobuf:"varint,1,req,name=value" json:"value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Integer32Proto) Reset() { *m = Integer32Proto{} } +func (m *Integer32Proto) String() string { return proto.CompactTextString(m) } +func (*Integer32Proto) ProtoMessage() {} + +func (m *Integer32Proto) GetValue() int32 { + if m != nil && m.Value != nil { + return *m.Value + } + return 0 +} + +type Integer64Proto struct { + Value *int64 `protobuf:"varint,1,req,name=value" json:"value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Integer64Proto) Reset() { *m = Integer64Proto{} } +func (m *Integer64Proto) String() string { return proto.CompactTextString(m) } +func (*Integer64Proto) ProtoMessage() {} + +func (m *Integer64Proto) GetValue() int64 { + if m != nil && m.Value != nil { + return *m.Value + } + return 0 +} + +type BoolProto struct { + Value *bool `protobuf:"varint,1,req,name=value" json:"value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *BoolProto) Reset() { *m = BoolProto{} } +func (m *BoolProto) String() string { return proto.CompactTextString(m) } +func (*BoolProto) ProtoMessage() {} + +func (m *BoolProto) GetValue() bool { + if m != nil && m.Value != nil { + return *m.Value + } + return false +} + +type DoubleProto struct { + Value *float64 `protobuf:"fixed64,1,req,name=value" json:"value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *DoubleProto) Reset() { *m = DoubleProto{} } +func (m *DoubleProto) String() string { return proto.CompactTextString(m) } +func (*DoubleProto) ProtoMessage() {} + +func (m *DoubleProto) GetValue() float64 { + if m != nil && m.Value != nil { + return *m.Value + } + return 0 +} + +type BytesProto struct { + Value []byte `protobuf:"bytes,1,req,name=value" json:"value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *BytesProto) Reset() { *m = BytesProto{} } +func (m *BytesProto) String() string { return proto.CompactTextString(m) } +func (*BytesProto) ProtoMessage() {} + +func (m *BytesProto) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +type VoidProto struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *VoidProto) Reset() { *m = VoidProto{} } +func (m *VoidProto) String() string { return proto.CompactTextString(m) } +func (*VoidProto) ProtoMessage() {} diff --git a/vendor/google.golang.org/appengine/internal/datastore/datastore_v3.pb.go b/vendor/google.golang.org/appengine/internal/datastore/datastore_v3.pb.go new file mode 100644 index 00000000000..8613cb7311a --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/datastore/datastore_v3.pb.go @@ -0,0 +1,2778 @@ +// Code generated by protoc-gen-go. +// source: google.golang.org/appengine/internal/datastore/datastore_v3.proto +// DO NOT EDIT! + +/* +Package datastore is a generated protocol buffer package. + +It is generated from these files: + google.golang.org/appengine/internal/datastore/datastore_v3.proto + +It has these top-level messages: + Action + PropertyValue + Property + Path + Reference + User + EntityProto + CompositeProperty + Index + CompositeIndex + IndexPostfix + IndexPosition + Snapshot + InternalHeader + Transaction + Query + CompiledQuery + CompiledCursor + Cursor + Error + Cost + GetRequest + GetResponse + PutRequest + PutResponse + TouchRequest + TouchResponse + DeleteRequest + DeleteResponse + NextRequest + QueryResult + AllocateIdsRequest + AllocateIdsResponse + CompositeIndices + AddActionsRequest + AddActionsResponse + BeginTransactionRequest + CommitResponse +*/ +package datastore + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type Property_Meaning int32 + +const ( + Property_NO_MEANING Property_Meaning = 0 + Property_BLOB Property_Meaning = 14 + Property_TEXT Property_Meaning = 15 + Property_BYTESTRING Property_Meaning = 16 + Property_ATOM_CATEGORY Property_Meaning = 1 + Property_ATOM_LINK Property_Meaning = 2 + Property_ATOM_TITLE Property_Meaning = 3 + Property_ATOM_CONTENT Property_Meaning = 4 + Property_ATOM_SUMMARY Property_Meaning = 5 + Property_ATOM_AUTHOR Property_Meaning = 6 + Property_GD_WHEN Property_Meaning = 7 + Property_GD_EMAIL Property_Meaning = 8 + Property_GEORSS_POINT Property_Meaning = 9 + Property_GD_IM Property_Meaning = 10 + Property_GD_PHONENUMBER Property_Meaning = 11 + Property_GD_POSTALADDRESS Property_Meaning = 12 + Property_GD_RATING Property_Meaning = 13 + Property_BLOBKEY Property_Meaning = 17 + Property_ENTITY_PROTO Property_Meaning = 19 + Property_INDEX_VALUE Property_Meaning = 18 +) + +var Property_Meaning_name = map[int32]string{ + 0: "NO_MEANING", + 14: "BLOB", + 15: "TEXT", + 16: "BYTESTRING", + 1: "ATOM_CATEGORY", + 2: "ATOM_LINK", + 3: "ATOM_TITLE", + 4: "ATOM_CONTENT", + 5: "ATOM_SUMMARY", + 6: "ATOM_AUTHOR", + 7: "GD_WHEN", + 8: "GD_EMAIL", + 9: "GEORSS_POINT", + 10: "GD_IM", + 11: "GD_PHONENUMBER", + 12: "GD_POSTALADDRESS", + 13: "GD_RATING", + 17: "BLOBKEY", + 19: "ENTITY_PROTO", + 18: "INDEX_VALUE", +} +var Property_Meaning_value = map[string]int32{ + "NO_MEANING": 0, + "BLOB": 14, + "TEXT": 15, + "BYTESTRING": 16, + "ATOM_CATEGORY": 1, + "ATOM_LINK": 2, + "ATOM_TITLE": 3, + "ATOM_CONTENT": 4, + "ATOM_SUMMARY": 5, + "ATOM_AUTHOR": 6, + "GD_WHEN": 7, + "GD_EMAIL": 8, + "GEORSS_POINT": 9, + "GD_IM": 10, + "GD_PHONENUMBER": 11, + "GD_POSTALADDRESS": 12, + "GD_RATING": 13, + "BLOBKEY": 17, + "ENTITY_PROTO": 19, + "INDEX_VALUE": 18, +} + +func (x Property_Meaning) Enum() *Property_Meaning { + p := new(Property_Meaning) + *p = x + return p +} +func (x Property_Meaning) String() string { + return proto.EnumName(Property_Meaning_name, int32(x)) +} +func (x *Property_Meaning) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(Property_Meaning_value, data, "Property_Meaning") + if err != nil { + return err + } + *x = Property_Meaning(value) + return nil +} + +type Property_FtsTokenizationOption int32 + +const ( + Property_HTML Property_FtsTokenizationOption = 1 + Property_ATOM Property_FtsTokenizationOption = 2 +) + +var Property_FtsTokenizationOption_name = map[int32]string{ + 1: "HTML", + 2: "ATOM", +} +var Property_FtsTokenizationOption_value = map[string]int32{ + "HTML": 1, + "ATOM": 2, +} + +func (x Property_FtsTokenizationOption) Enum() *Property_FtsTokenizationOption { + p := new(Property_FtsTokenizationOption) + *p = x + return p +} +func (x Property_FtsTokenizationOption) String() string { + return proto.EnumName(Property_FtsTokenizationOption_name, int32(x)) +} +func (x *Property_FtsTokenizationOption) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(Property_FtsTokenizationOption_value, data, "Property_FtsTokenizationOption") + if err != nil { + return err + } + *x = Property_FtsTokenizationOption(value) + return nil +} + +type EntityProto_Kind int32 + +const ( + EntityProto_GD_CONTACT EntityProto_Kind = 1 + EntityProto_GD_EVENT EntityProto_Kind = 2 + EntityProto_GD_MESSAGE EntityProto_Kind = 3 +) + +var EntityProto_Kind_name = map[int32]string{ + 1: "GD_CONTACT", + 2: "GD_EVENT", + 3: "GD_MESSAGE", +} +var EntityProto_Kind_value = map[string]int32{ + "GD_CONTACT": 1, + "GD_EVENT": 2, + "GD_MESSAGE": 3, +} + +func (x EntityProto_Kind) Enum() *EntityProto_Kind { + p := new(EntityProto_Kind) + *p = x + return p +} +func (x EntityProto_Kind) String() string { + return proto.EnumName(EntityProto_Kind_name, int32(x)) +} +func (x *EntityProto_Kind) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(EntityProto_Kind_value, data, "EntityProto_Kind") + if err != nil { + return err + } + *x = EntityProto_Kind(value) + return nil +} + +type Index_Property_Direction int32 + +const ( + Index_Property_ASCENDING Index_Property_Direction = 1 + Index_Property_DESCENDING Index_Property_Direction = 2 +) + +var Index_Property_Direction_name = map[int32]string{ + 1: "ASCENDING", + 2: "DESCENDING", +} +var Index_Property_Direction_value = map[string]int32{ + "ASCENDING": 1, + "DESCENDING": 2, +} + +func (x Index_Property_Direction) Enum() *Index_Property_Direction { + p := new(Index_Property_Direction) + *p = x + return p +} +func (x Index_Property_Direction) String() string { + return proto.EnumName(Index_Property_Direction_name, int32(x)) +} +func (x *Index_Property_Direction) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(Index_Property_Direction_value, data, "Index_Property_Direction") + if err != nil { + return err + } + *x = Index_Property_Direction(value) + return nil +} + +type CompositeIndex_State int32 + +const ( + CompositeIndex_WRITE_ONLY CompositeIndex_State = 1 + CompositeIndex_READ_WRITE CompositeIndex_State = 2 + CompositeIndex_DELETED CompositeIndex_State = 3 + CompositeIndex_ERROR CompositeIndex_State = 4 +) + +var CompositeIndex_State_name = map[int32]string{ + 1: "WRITE_ONLY", + 2: "READ_WRITE", + 3: "DELETED", + 4: "ERROR", +} +var CompositeIndex_State_value = map[string]int32{ + "WRITE_ONLY": 1, + "READ_WRITE": 2, + "DELETED": 3, + "ERROR": 4, +} + +func (x CompositeIndex_State) Enum() *CompositeIndex_State { + p := new(CompositeIndex_State) + *p = x + return p +} +func (x CompositeIndex_State) String() string { + return proto.EnumName(CompositeIndex_State_name, int32(x)) +} +func (x *CompositeIndex_State) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(CompositeIndex_State_value, data, "CompositeIndex_State") + if err != nil { + return err + } + *x = CompositeIndex_State(value) + return nil +} + +type Snapshot_Status int32 + +const ( + Snapshot_INACTIVE Snapshot_Status = 0 + Snapshot_ACTIVE Snapshot_Status = 1 +) + +var Snapshot_Status_name = map[int32]string{ + 0: "INACTIVE", + 1: "ACTIVE", +} +var Snapshot_Status_value = map[string]int32{ + "INACTIVE": 0, + "ACTIVE": 1, +} + +func (x Snapshot_Status) Enum() *Snapshot_Status { + p := new(Snapshot_Status) + *p = x + return p +} +func (x Snapshot_Status) String() string { + return proto.EnumName(Snapshot_Status_name, int32(x)) +} +func (x *Snapshot_Status) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(Snapshot_Status_value, data, "Snapshot_Status") + if err != nil { + return err + } + *x = Snapshot_Status(value) + return nil +} + +type Query_Hint int32 + +const ( + Query_ORDER_FIRST Query_Hint = 1 + Query_ANCESTOR_FIRST Query_Hint = 2 + Query_FILTER_FIRST Query_Hint = 3 +) + +var Query_Hint_name = map[int32]string{ + 1: "ORDER_FIRST", + 2: "ANCESTOR_FIRST", + 3: "FILTER_FIRST", +} +var Query_Hint_value = map[string]int32{ + "ORDER_FIRST": 1, + "ANCESTOR_FIRST": 2, + "FILTER_FIRST": 3, +} + +func (x Query_Hint) Enum() *Query_Hint { + p := new(Query_Hint) + *p = x + return p +} +func (x Query_Hint) String() string { + return proto.EnumName(Query_Hint_name, int32(x)) +} +func (x *Query_Hint) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(Query_Hint_value, data, "Query_Hint") + if err != nil { + return err + } + *x = Query_Hint(value) + return nil +} + +type Query_Filter_Operator int32 + +const ( + Query_Filter_LESS_THAN Query_Filter_Operator = 1 + Query_Filter_LESS_THAN_OR_EQUAL Query_Filter_Operator = 2 + Query_Filter_GREATER_THAN Query_Filter_Operator = 3 + Query_Filter_GREATER_THAN_OR_EQUAL Query_Filter_Operator = 4 + Query_Filter_EQUAL Query_Filter_Operator = 5 + Query_Filter_IN Query_Filter_Operator = 6 + Query_Filter_EXISTS Query_Filter_Operator = 7 +) + +var Query_Filter_Operator_name = map[int32]string{ + 1: "LESS_THAN", + 2: "LESS_THAN_OR_EQUAL", + 3: "GREATER_THAN", + 4: "GREATER_THAN_OR_EQUAL", + 5: "EQUAL", + 6: "IN", + 7: "EXISTS", +} +var Query_Filter_Operator_value = map[string]int32{ + "LESS_THAN": 1, + "LESS_THAN_OR_EQUAL": 2, + "GREATER_THAN": 3, + "GREATER_THAN_OR_EQUAL": 4, + "EQUAL": 5, + "IN": 6, + "EXISTS": 7, +} + +func (x Query_Filter_Operator) Enum() *Query_Filter_Operator { + p := new(Query_Filter_Operator) + *p = x + return p +} +func (x Query_Filter_Operator) String() string { + return proto.EnumName(Query_Filter_Operator_name, int32(x)) +} +func (x *Query_Filter_Operator) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(Query_Filter_Operator_value, data, "Query_Filter_Operator") + if err != nil { + return err + } + *x = Query_Filter_Operator(value) + return nil +} + +type Query_Order_Direction int32 + +const ( + Query_Order_ASCENDING Query_Order_Direction = 1 + Query_Order_DESCENDING Query_Order_Direction = 2 +) + +var Query_Order_Direction_name = map[int32]string{ + 1: "ASCENDING", + 2: "DESCENDING", +} +var Query_Order_Direction_value = map[string]int32{ + "ASCENDING": 1, + "DESCENDING": 2, +} + +func (x Query_Order_Direction) Enum() *Query_Order_Direction { + p := new(Query_Order_Direction) + *p = x + return p +} +func (x Query_Order_Direction) String() string { + return proto.EnumName(Query_Order_Direction_name, int32(x)) +} +func (x *Query_Order_Direction) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(Query_Order_Direction_value, data, "Query_Order_Direction") + if err != nil { + return err + } + *x = Query_Order_Direction(value) + return nil +} + +type Error_ErrorCode int32 + +const ( + Error_BAD_REQUEST Error_ErrorCode = 1 + Error_CONCURRENT_TRANSACTION Error_ErrorCode = 2 + Error_INTERNAL_ERROR Error_ErrorCode = 3 + Error_NEED_INDEX Error_ErrorCode = 4 + Error_TIMEOUT Error_ErrorCode = 5 + Error_PERMISSION_DENIED Error_ErrorCode = 6 + Error_BIGTABLE_ERROR Error_ErrorCode = 7 + Error_COMMITTED_BUT_STILL_APPLYING Error_ErrorCode = 8 + Error_CAPABILITY_DISABLED Error_ErrorCode = 9 + Error_TRY_ALTERNATE_BACKEND Error_ErrorCode = 10 + Error_SAFE_TIME_TOO_OLD Error_ErrorCode = 11 +) + +var Error_ErrorCode_name = map[int32]string{ + 1: "BAD_REQUEST", + 2: "CONCURRENT_TRANSACTION", + 3: "INTERNAL_ERROR", + 4: "NEED_INDEX", + 5: "TIMEOUT", + 6: "PERMISSION_DENIED", + 7: "BIGTABLE_ERROR", + 8: "COMMITTED_BUT_STILL_APPLYING", + 9: "CAPABILITY_DISABLED", + 10: "TRY_ALTERNATE_BACKEND", + 11: "SAFE_TIME_TOO_OLD", +} +var Error_ErrorCode_value = map[string]int32{ + "BAD_REQUEST": 1, + "CONCURRENT_TRANSACTION": 2, + "INTERNAL_ERROR": 3, + "NEED_INDEX": 4, + "TIMEOUT": 5, + "PERMISSION_DENIED": 6, + "BIGTABLE_ERROR": 7, + "COMMITTED_BUT_STILL_APPLYING": 8, + "CAPABILITY_DISABLED": 9, + "TRY_ALTERNATE_BACKEND": 10, + "SAFE_TIME_TOO_OLD": 11, +} + +func (x Error_ErrorCode) Enum() *Error_ErrorCode { + p := new(Error_ErrorCode) + *p = x + return p +} +func (x Error_ErrorCode) String() string { + return proto.EnumName(Error_ErrorCode_name, int32(x)) +} +func (x *Error_ErrorCode) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(Error_ErrorCode_value, data, "Error_ErrorCode") + if err != nil { + return err + } + *x = Error_ErrorCode(value) + return nil +} + +type PutRequest_AutoIdPolicy int32 + +const ( + PutRequest_CURRENT PutRequest_AutoIdPolicy = 0 + PutRequest_SEQUENTIAL PutRequest_AutoIdPolicy = 1 +) + +var PutRequest_AutoIdPolicy_name = map[int32]string{ + 0: "CURRENT", + 1: "SEQUENTIAL", +} +var PutRequest_AutoIdPolicy_value = map[string]int32{ + "CURRENT": 0, + "SEQUENTIAL": 1, +} + +func (x PutRequest_AutoIdPolicy) Enum() *PutRequest_AutoIdPolicy { + p := new(PutRequest_AutoIdPolicy) + *p = x + return p +} +func (x PutRequest_AutoIdPolicy) String() string { + return proto.EnumName(PutRequest_AutoIdPolicy_name, int32(x)) +} +func (x *PutRequest_AutoIdPolicy) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(PutRequest_AutoIdPolicy_value, data, "PutRequest_AutoIdPolicy") + if err != nil { + return err + } + *x = PutRequest_AutoIdPolicy(value) + return nil +} + +type Action struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *Action) Reset() { *m = Action{} } +func (m *Action) String() string { return proto.CompactTextString(m) } +func (*Action) ProtoMessage() {} + +type PropertyValue struct { + Int64Value *int64 `protobuf:"varint,1,opt,name=int64Value" json:"int64Value,omitempty"` + BooleanValue *bool `protobuf:"varint,2,opt,name=booleanValue" json:"booleanValue,omitempty"` + StringValue *string `protobuf:"bytes,3,opt,name=stringValue" json:"stringValue,omitempty"` + DoubleValue *float64 `protobuf:"fixed64,4,opt,name=doubleValue" json:"doubleValue,omitempty"` + Pointvalue *PropertyValue_PointValue `protobuf:"group,5,opt,name=PointValue" json:"pointvalue,omitempty"` + Uservalue *PropertyValue_UserValue `protobuf:"group,8,opt,name=UserValue" json:"uservalue,omitempty"` + Referencevalue *PropertyValue_ReferenceValue `protobuf:"group,12,opt,name=ReferenceValue" json:"referencevalue,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *PropertyValue) Reset() { *m = PropertyValue{} } +func (m *PropertyValue) String() string { return proto.CompactTextString(m) } +func (*PropertyValue) ProtoMessage() {} + +func (m *PropertyValue) GetInt64Value() int64 { + if m != nil && m.Int64Value != nil { + return *m.Int64Value + } + return 0 +} + +func (m *PropertyValue) GetBooleanValue() bool { + if m != nil && m.BooleanValue != nil { + return *m.BooleanValue + } + return false +} + +func (m *PropertyValue) GetStringValue() string { + if m != nil && m.StringValue != nil { + return *m.StringValue + } + return "" +} + +func (m *PropertyValue) GetDoubleValue() float64 { + if m != nil && m.DoubleValue != nil { + return *m.DoubleValue + } + return 0 +} + +func (m *PropertyValue) GetPointvalue() *PropertyValue_PointValue { + if m != nil { + return m.Pointvalue + } + return nil +} + +func (m *PropertyValue) GetUservalue() *PropertyValue_UserValue { + if m != nil { + return m.Uservalue + } + return nil +} + +func (m *PropertyValue) GetReferencevalue() *PropertyValue_ReferenceValue { + if m != nil { + return m.Referencevalue + } + return nil +} + +type PropertyValue_PointValue struct { + X *float64 `protobuf:"fixed64,6,req,name=x" json:"x,omitempty"` + Y *float64 `protobuf:"fixed64,7,req,name=y" json:"y,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *PropertyValue_PointValue) Reset() { *m = PropertyValue_PointValue{} } +func (m *PropertyValue_PointValue) String() string { return proto.CompactTextString(m) } +func (*PropertyValue_PointValue) ProtoMessage() {} + +func (m *PropertyValue_PointValue) GetX() float64 { + if m != nil && m.X != nil { + return *m.X + } + return 0 +} + +func (m *PropertyValue_PointValue) GetY() float64 { + if m != nil && m.Y != nil { + return *m.Y + } + return 0 +} + +type PropertyValue_UserValue struct { + Email *string `protobuf:"bytes,9,req,name=email" json:"email,omitempty"` + AuthDomain *string `protobuf:"bytes,10,req,name=auth_domain" json:"auth_domain,omitempty"` + Nickname *string `protobuf:"bytes,11,opt,name=nickname" json:"nickname,omitempty"` + FederatedIdentity *string `protobuf:"bytes,21,opt,name=federated_identity" json:"federated_identity,omitempty"` + FederatedProvider *string `protobuf:"bytes,22,opt,name=federated_provider" json:"federated_provider,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *PropertyValue_UserValue) Reset() { *m = PropertyValue_UserValue{} } +func (m *PropertyValue_UserValue) String() string { return proto.CompactTextString(m) } +func (*PropertyValue_UserValue) ProtoMessage() {} + +func (m *PropertyValue_UserValue) GetEmail() string { + if m != nil && m.Email != nil { + return *m.Email + } + return "" +} + +func (m *PropertyValue_UserValue) GetAuthDomain() string { + if m != nil && m.AuthDomain != nil { + return *m.AuthDomain + } + return "" +} + +func (m *PropertyValue_UserValue) GetNickname() string { + if m != nil && m.Nickname != nil { + return *m.Nickname + } + return "" +} + +func (m *PropertyValue_UserValue) GetFederatedIdentity() string { + if m != nil && m.FederatedIdentity != nil { + return *m.FederatedIdentity + } + return "" +} + +func (m *PropertyValue_UserValue) GetFederatedProvider() string { + if m != nil && m.FederatedProvider != nil { + return *m.FederatedProvider + } + return "" +} + +type PropertyValue_ReferenceValue struct { + App *string `protobuf:"bytes,13,req,name=app" json:"app,omitempty"` + NameSpace *string `protobuf:"bytes,20,opt,name=name_space" json:"name_space,omitempty"` + Pathelement []*PropertyValue_ReferenceValue_PathElement `protobuf:"group,14,rep,name=PathElement" json:"pathelement,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *PropertyValue_ReferenceValue) Reset() { *m = PropertyValue_ReferenceValue{} } +func (m *PropertyValue_ReferenceValue) String() string { return proto.CompactTextString(m) } +func (*PropertyValue_ReferenceValue) ProtoMessage() {} + +func (m *PropertyValue_ReferenceValue) GetApp() string { + if m != nil && m.App != nil { + return *m.App + } + return "" +} + +func (m *PropertyValue_ReferenceValue) GetNameSpace() string { + if m != nil && m.NameSpace != nil { + return *m.NameSpace + } + return "" +} + +func (m *PropertyValue_ReferenceValue) GetPathelement() []*PropertyValue_ReferenceValue_PathElement { + if m != nil { + return m.Pathelement + } + return nil +} + +type PropertyValue_ReferenceValue_PathElement struct { + Type *string `protobuf:"bytes,15,req,name=type" json:"type,omitempty"` + Id *int64 `protobuf:"varint,16,opt,name=id" json:"id,omitempty"` + Name *string `protobuf:"bytes,17,opt,name=name" json:"name,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *PropertyValue_ReferenceValue_PathElement) Reset() { + *m = PropertyValue_ReferenceValue_PathElement{} +} +func (m *PropertyValue_ReferenceValue_PathElement) String() string { return proto.CompactTextString(m) } +func (*PropertyValue_ReferenceValue_PathElement) ProtoMessage() {} + +func (m *PropertyValue_ReferenceValue_PathElement) GetType() string { + if m != nil && m.Type != nil { + return *m.Type + } + return "" +} + +func (m *PropertyValue_ReferenceValue_PathElement) GetId() int64 { + if m != nil && m.Id != nil { + return *m.Id + } + return 0 +} + +func (m *PropertyValue_ReferenceValue_PathElement) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +type Property struct { + Meaning *Property_Meaning `protobuf:"varint,1,opt,name=meaning,enum=appengine.Property_Meaning,def=0" json:"meaning,omitempty"` + MeaningUri *string `protobuf:"bytes,2,opt,name=meaning_uri" json:"meaning_uri,omitempty"` + Name *string `protobuf:"bytes,3,req,name=name" json:"name,omitempty"` + Value *PropertyValue `protobuf:"bytes,5,req,name=value" json:"value,omitempty"` + Multiple *bool `protobuf:"varint,4,req,name=multiple" json:"multiple,omitempty"` + Searchable *bool `protobuf:"varint,6,opt,name=searchable,def=0" json:"searchable,omitempty"` + FtsTokenizationOption *Property_FtsTokenizationOption `protobuf:"varint,8,opt,name=fts_tokenization_option,enum=appengine.Property_FtsTokenizationOption" json:"fts_tokenization_option,omitempty"` + Locale *string `protobuf:"bytes,9,opt,name=locale,def=en" json:"locale,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Property) Reset() { *m = Property{} } +func (m *Property) String() string { return proto.CompactTextString(m) } +func (*Property) ProtoMessage() {} + +const Default_Property_Meaning Property_Meaning = Property_NO_MEANING +const Default_Property_Searchable bool = false +const Default_Property_Locale string = "en" + +func (m *Property) GetMeaning() Property_Meaning { + if m != nil && m.Meaning != nil { + return *m.Meaning + } + return Default_Property_Meaning +} + +func (m *Property) GetMeaningUri() string { + if m != nil && m.MeaningUri != nil { + return *m.MeaningUri + } + return "" +} + +func (m *Property) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +func (m *Property) GetValue() *PropertyValue { + if m != nil { + return m.Value + } + return nil +} + +func (m *Property) GetMultiple() bool { + if m != nil && m.Multiple != nil { + return *m.Multiple + } + return false +} + +func (m *Property) GetSearchable() bool { + if m != nil && m.Searchable != nil { + return *m.Searchable + } + return Default_Property_Searchable +} + +func (m *Property) GetFtsTokenizationOption() Property_FtsTokenizationOption { + if m != nil && m.FtsTokenizationOption != nil { + return *m.FtsTokenizationOption + } + return Property_HTML +} + +func (m *Property) GetLocale() string { + if m != nil && m.Locale != nil { + return *m.Locale + } + return Default_Property_Locale +} + +type Path struct { + Element []*Path_Element `protobuf:"group,1,rep,name=Element" json:"element,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Path) Reset() { *m = Path{} } +func (m *Path) String() string { return proto.CompactTextString(m) } +func (*Path) ProtoMessage() {} + +func (m *Path) GetElement() []*Path_Element { + if m != nil { + return m.Element + } + return nil +} + +type Path_Element struct { + Type *string `protobuf:"bytes,2,req,name=type" json:"type,omitempty"` + Id *int64 `protobuf:"varint,3,opt,name=id" json:"id,omitempty"` + Name *string `protobuf:"bytes,4,opt,name=name" json:"name,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Path_Element) Reset() { *m = Path_Element{} } +func (m *Path_Element) String() string { return proto.CompactTextString(m) } +func (*Path_Element) ProtoMessage() {} + +func (m *Path_Element) GetType() string { + if m != nil && m.Type != nil { + return *m.Type + } + return "" +} + +func (m *Path_Element) GetId() int64 { + if m != nil && m.Id != nil { + return *m.Id + } + return 0 +} + +func (m *Path_Element) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +type Reference struct { + App *string `protobuf:"bytes,13,req,name=app" json:"app,omitempty"` + NameSpace *string `protobuf:"bytes,20,opt,name=name_space" json:"name_space,omitempty"` + Path *Path `protobuf:"bytes,14,req,name=path" json:"path,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Reference) Reset() { *m = Reference{} } +func (m *Reference) String() string { return proto.CompactTextString(m) } +func (*Reference) ProtoMessage() {} + +func (m *Reference) GetApp() string { + if m != nil && m.App != nil { + return *m.App + } + return "" +} + +func (m *Reference) GetNameSpace() string { + if m != nil && m.NameSpace != nil { + return *m.NameSpace + } + return "" +} + +func (m *Reference) GetPath() *Path { + if m != nil { + return m.Path + } + return nil +} + +type User struct { + Email *string `protobuf:"bytes,1,req,name=email" json:"email,omitempty"` + AuthDomain *string `protobuf:"bytes,2,req,name=auth_domain" json:"auth_domain,omitempty"` + Nickname *string `protobuf:"bytes,3,opt,name=nickname" json:"nickname,omitempty"` + FederatedIdentity *string `protobuf:"bytes,6,opt,name=federated_identity" json:"federated_identity,omitempty"` + FederatedProvider *string `protobuf:"bytes,7,opt,name=federated_provider" json:"federated_provider,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *User) Reset() { *m = User{} } +func (m *User) String() string { return proto.CompactTextString(m) } +func (*User) ProtoMessage() {} + +func (m *User) GetEmail() string { + if m != nil && m.Email != nil { + return *m.Email + } + return "" +} + +func (m *User) GetAuthDomain() string { + if m != nil && m.AuthDomain != nil { + return *m.AuthDomain + } + return "" +} + +func (m *User) GetNickname() string { + if m != nil && m.Nickname != nil { + return *m.Nickname + } + return "" +} + +func (m *User) GetFederatedIdentity() string { + if m != nil && m.FederatedIdentity != nil { + return *m.FederatedIdentity + } + return "" +} + +func (m *User) GetFederatedProvider() string { + if m != nil && m.FederatedProvider != nil { + return *m.FederatedProvider + } + return "" +} + +type EntityProto struct { + Key *Reference `protobuf:"bytes,13,req,name=key" json:"key,omitempty"` + EntityGroup *Path `protobuf:"bytes,16,req,name=entity_group" json:"entity_group,omitempty"` + Owner *User `protobuf:"bytes,17,opt,name=owner" json:"owner,omitempty"` + Kind *EntityProto_Kind `protobuf:"varint,4,opt,name=kind,enum=appengine.EntityProto_Kind" json:"kind,omitempty"` + KindUri *string `protobuf:"bytes,5,opt,name=kind_uri" json:"kind_uri,omitempty"` + Property []*Property `protobuf:"bytes,14,rep,name=property" json:"property,omitempty"` + RawProperty []*Property `protobuf:"bytes,15,rep,name=raw_property" json:"raw_property,omitempty"` + Rank *int32 `protobuf:"varint,18,opt,name=rank" json:"rank,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *EntityProto) Reset() { *m = EntityProto{} } +func (m *EntityProto) String() string { return proto.CompactTextString(m) } +func (*EntityProto) ProtoMessage() {} + +func (m *EntityProto) GetKey() *Reference { + if m != nil { + return m.Key + } + return nil +} + +func (m *EntityProto) GetEntityGroup() *Path { + if m != nil { + return m.EntityGroup + } + return nil +} + +func (m *EntityProto) GetOwner() *User { + if m != nil { + return m.Owner + } + return nil +} + +func (m *EntityProto) GetKind() EntityProto_Kind { + if m != nil && m.Kind != nil { + return *m.Kind + } + return EntityProto_GD_CONTACT +} + +func (m *EntityProto) GetKindUri() string { + if m != nil && m.KindUri != nil { + return *m.KindUri + } + return "" +} + +func (m *EntityProto) GetProperty() []*Property { + if m != nil { + return m.Property + } + return nil +} + +func (m *EntityProto) GetRawProperty() []*Property { + if m != nil { + return m.RawProperty + } + return nil +} + +func (m *EntityProto) GetRank() int32 { + if m != nil && m.Rank != nil { + return *m.Rank + } + return 0 +} + +type CompositeProperty struct { + IndexId *int64 `protobuf:"varint,1,req,name=index_id" json:"index_id,omitempty"` + Value []string `protobuf:"bytes,2,rep,name=value" json:"value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CompositeProperty) Reset() { *m = CompositeProperty{} } +func (m *CompositeProperty) String() string { return proto.CompactTextString(m) } +func (*CompositeProperty) ProtoMessage() {} + +func (m *CompositeProperty) GetIndexId() int64 { + if m != nil && m.IndexId != nil { + return *m.IndexId + } + return 0 +} + +func (m *CompositeProperty) GetValue() []string { + if m != nil { + return m.Value + } + return nil +} + +type Index struct { + EntityType *string `protobuf:"bytes,1,req,name=entity_type" json:"entity_type,omitempty"` + Ancestor *bool `protobuf:"varint,5,req,name=ancestor" json:"ancestor,omitempty"` + Property []*Index_Property `protobuf:"group,2,rep,name=Property" json:"property,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Index) Reset() { *m = Index{} } +func (m *Index) String() string { return proto.CompactTextString(m) } +func (*Index) ProtoMessage() {} + +func (m *Index) GetEntityType() string { + if m != nil && m.EntityType != nil { + return *m.EntityType + } + return "" +} + +func (m *Index) GetAncestor() bool { + if m != nil && m.Ancestor != nil { + return *m.Ancestor + } + return false +} + +func (m *Index) GetProperty() []*Index_Property { + if m != nil { + return m.Property + } + return nil +} + +type Index_Property struct { + Name *string `protobuf:"bytes,3,req,name=name" json:"name,omitempty"` + Direction *Index_Property_Direction `protobuf:"varint,4,opt,name=direction,enum=appengine.Index_Property_Direction,def=1" json:"direction,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Index_Property) Reset() { *m = Index_Property{} } +func (m *Index_Property) String() string { return proto.CompactTextString(m) } +func (*Index_Property) ProtoMessage() {} + +const Default_Index_Property_Direction Index_Property_Direction = Index_Property_ASCENDING + +func (m *Index_Property) GetName() string { + if m != nil && m.Name != nil { + return *m.Name + } + return "" +} + +func (m *Index_Property) GetDirection() Index_Property_Direction { + if m != nil && m.Direction != nil { + return *m.Direction + } + return Default_Index_Property_Direction +} + +type CompositeIndex struct { + AppId *string `protobuf:"bytes,1,req,name=app_id" json:"app_id,omitempty"` + Id *int64 `protobuf:"varint,2,req,name=id" json:"id,omitempty"` + Definition *Index `protobuf:"bytes,3,req,name=definition" json:"definition,omitempty"` + State *CompositeIndex_State `protobuf:"varint,4,req,name=state,enum=appengine.CompositeIndex_State" json:"state,omitempty"` + OnlyUseIfRequired *bool `protobuf:"varint,6,opt,name=only_use_if_required,def=0" json:"only_use_if_required,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CompositeIndex) Reset() { *m = CompositeIndex{} } +func (m *CompositeIndex) String() string { return proto.CompactTextString(m) } +func (*CompositeIndex) ProtoMessage() {} + +const Default_CompositeIndex_OnlyUseIfRequired bool = false + +func (m *CompositeIndex) GetAppId() string { + if m != nil && m.AppId != nil { + return *m.AppId + } + return "" +} + +func (m *CompositeIndex) GetId() int64 { + if m != nil && m.Id != nil { + return *m.Id + } + return 0 +} + +func (m *CompositeIndex) GetDefinition() *Index { + if m != nil { + return m.Definition + } + return nil +} + +func (m *CompositeIndex) GetState() CompositeIndex_State { + if m != nil && m.State != nil { + return *m.State + } + return CompositeIndex_WRITE_ONLY +} + +func (m *CompositeIndex) GetOnlyUseIfRequired() bool { + if m != nil && m.OnlyUseIfRequired != nil { + return *m.OnlyUseIfRequired + } + return Default_CompositeIndex_OnlyUseIfRequired +} + +type IndexPostfix struct { + IndexValue []*IndexPostfix_IndexValue `protobuf:"bytes,1,rep,name=index_value" json:"index_value,omitempty"` + Key *Reference `protobuf:"bytes,2,opt,name=key" json:"key,omitempty"` + Before *bool `protobuf:"varint,3,opt,name=before,def=1" json:"before,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *IndexPostfix) Reset() { *m = IndexPostfix{} } +func (m *IndexPostfix) String() string { return proto.CompactTextString(m) } +func (*IndexPostfix) ProtoMessage() {} + +const Default_IndexPostfix_Before bool = true + +func (m *IndexPostfix) GetIndexValue() []*IndexPostfix_IndexValue { + if m != nil { + return m.IndexValue + } + return nil +} + +func (m *IndexPostfix) GetKey() *Reference { + if m != nil { + return m.Key + } + return nil +} + +func (m *IndexPostfix) GetBefore() bool { + if m != nil && m.Before != nil { + return *m.Before + } + return Default_IndexPostfix_Before +} + +type IndexPostfix_IndexValue struct { + PropertyName *string `protobuf:"bytes,1,req,name=property_name" json:"property_name,omitempty"` + Value *PropertyValue `protobuf:"bytes,2,req,name=value" json:"value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *IndexPostfix_IndexValue) Reset() { *m = IndexPostfix_IndexValue{} } +func (m *IndexPostfix_IndexValue) String() string { return proto.CompactTextString(m) } +func (*IndexPostfix_IndexValue) ProtoMessage() {} + +func (m *IndexPostfix_IndexValue) GetPropertyName() string { + if m != nil && m.PropertyName != nil { + return *m.PropertyName + } + return "" +} + +func (m *IndexPostfix_IndexValue) GetValue() *PropertyValue { + if m != nil { + return m.Value + } + return nil +} + +type IndexPosition struct { + Key *string `protobuf:"bytes,1,opt,name=key" json:"key,omitempty"` + Before *bool `protobuf:"varint,2,opt,name=before,def=1" json:"before,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *IndexPosition) Reset() { *m = IndexPosition{} } +func (m *IndexPosition) String() string { return proto.CompactTextString(m) } +func (*IndexPosition) ProtoMessage() {} + +const Default_IndexPosition_Before bool = true + +func (m *IndexPosition) GetKey() string { + if m != nil && m.Key != nil { + return *m.Key + } + return "" +} + +func (m *IndexPosition) GetBefore() bool { + if m != nil && m.Before != nil { + return *m.Before + } + return Default_IndexPosition_Before +} + +type Snapshot struct { + Ts *int64 `protobuf:"varint,1,req,name=ts" json:"ts,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Snapshot) Reset() { *m = Snapshot{} } +func (m *Snapshot) String() string { return proto.CompactTextString(m) } +func (*Snapshot) ProtoMessage() {} + +func (m *Snapshot) GetTs() int64 { + if m != nil && m.Ts != nil { + return *m.Ts + } + return 0 +} + +type InternalHeader struct { + Qos *string `protobuf:"bytes,1,opt,name=qos" json:"qos,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *InternalHeader) Reset() { *m = InternalHeader{} } +func (m *InternalHeader) String() string { return proto.CompactTextString(m) } +func (*InternalHeader) ProtoMessage() {} + +func (m *InternalHeader) GetQos() string { + if m != nil && m.Qos != nil { + return *m.Qos + } + return "" +} + +type Transaction struct { + Header *InternalHeader `protobuf:"bytes,4,opt,name=header" json:"header,omitempty"` + Handle *uint64 `protobuf:"fixed64,1,req,name=handle" json:"handle,omitempty"` + App *string `protobuf:"bytes,2,req,name=app" json:"app,omitempty"` + MarkChanges *bool `protobuf:"varint,3,opt,name=mark_changes,def=0" json:"mark_changes,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Transaction) Reset() { *m = Transaction{} } +func (m *Transaction) String() string { return proto.CompactTextString(m) } +func (*Transaction) ProtoMessage() {} + +const Default_Transaction_MarkChanges bool = false + +func (m *Transaction) GetHeader() *InternalHeader { + if m != nil { + return m.Header + } + return nil +} + +func (m *Transaction) GetHandle() uint64 { + if m != nil && m.Handle != nil { + return *m.Handle + } + return 0 +} + +func (m *Transaction) GetApp() string { + if m != nil && m.App != nil { + return *m.App + } + return "" +} + +func (m *Transaction) GetMarkChanges() bool { + if m != nil && m.MarkChanges != nil { + return *m.MarkChanges + } + return Default_Transaction_MarkChanges +} + +type Query struct { + Header *InternalHeader `protobuf:"bytes,39,opt,name=header" json:"header,omitempty"` + App *string `protobuf:"bytes,1,req,name=app" json:"app,omitempty"` + NameSpace *string `protobuf:"bytes,29,opt,name=name_space" json:"name_space,omitempty"` + Kind *string `protobuf:"bytes,3,opt,name=kind" json:"kind,omitempty"` + Ancestor *Reference `protobuf:"bytes,17,opt,name=ancestor" json:"ancestor,omitempty"` + Filter []*Query_Filter `protobuf:"group,4,rep,name=Filter" json:"filter,omitempty"` + SearchQuery *string `protobuf:"bytes,8,opt,name=search_query" json:"search_query,omitempty"` + Order []*Query_Order `protobuf:"group,9,rep,name=Order" json:"order,omitempty"` + Hint *Query_Hint `protobuf:"varint,18,opt,name=hint,enum=appengine.Query_Hint" json:"hint,omitempty"` + Count *int32 `protobuf:"varint,23,opt,name=count" json:"count,omitempty"` + Offset *int32 `protobuf:"varint,12,opt,name=offset,def=0" json:"offset,omitempty"` + Limit *int32 `protobuf:"varint,16,opt,name=limit" json:"limit,omitempty"` + CompiledCursor *CompiledCursor `protobuf:"bytes,30,opt,name=compiled_cursor" json:"compiled_cursor,omitempty"` + EndCompiledCursor *CompiledCursor `protobuf:"bytes,31,opt,name=end_compiled_cursor" json:"end_compiled_cursor,omitempty"` + CompositeIndex []*CompositeIndex `protobuf:"bytes,19,rep,name=composite_index" json:"composite_index,omitempty"` + RequirePerfectPlan *bool `protobuf:"varint,20,opt,name=require_perfect_plan,def=0" json:"require_perfect_plan,omitempty"` + KeysOnly *bool `protobuf:"varint,21,opt,name=keys_only,def=0" json:"keys_only,omitempty"` + Transaction *Transaction `protobuf:"bytes,22,opt,name=transaction" json:"transaction,omitempty"` + Compile *bool `protobuf:"varint,25,opt,name=compile,def=0" json:"compile,omitempty"` + FailoverMs *int64 `protobuf:"varint,26,opt,name=failover_ms" json:"failover_ms,omitempty"` + Strong *bool `protobuf:"varint,32,opt,name=strong" json:"strong,omitempty"` + PropertyName []string `protobuf:"bytes,33,rep,name=property_name" json:"property_name,omitempty"` + GroupByPropertyName []string `protobuf:"bytes,34,rep,name=group_by_property_name" json:"group_by_property_name,omitempty"` + Distinct *bool `protobuf:"varint,24,opt,name=distinct" json:"distinct,omitempty"` + MinSafeTimeSeconds *int64 `protobuf:"varint,35,opt,name=min_safe_time_seconds" json:"min_safe_time_seconds,omitempty"` + SafeReplicaName []string `protobuf:"bytes,36,rep,name=safe_replica_name" json:"safe_replica_name,omitempty"` + PersistOffset *bool `protobuf:"varint,37,opt,name=persist_offset,def=0" json:"persist_offset,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Query) Reset() { *m = Query{} } +func (m *Query) String() string { return proto.CompactTextString(m) } +func (*Query) ProtoMessage() {} + +const Default_Query_Offset int32 = 0 +const Default_Query_RequirePerfectPlan bool = false +const Default_Query_KeysOnly bool = false +const Default_Query_Compile bool = false +const Default_Query_PersistOffset bool = false + +func (m *Query) GetHeader() *InternalHeader { + if m != nil { + return m.Header + } + return nil +} + +func (m *Query) GetApp() string { + if m != nil && m.App != nil { + return *m.App + } + return "" +} + +func (m *Query) GetNameSpace() string { + if m != nil && m.NameSpace != nil { + return *m.NameSpace + } + return "" +} + +func (m *Query) GetKind() string { + if m != nil && m.Kind != nil { + return *m.Kind + } + return "" +} + +func (m *Query) GetAncestor() *Reference { + if m != nil { + return m.Ancestor + } + return nil +} + +func (m *Query) GetFilter() []*Query_Filter { + if m != nil { + return m.Filter + } + return nil +} + +func (m *Query) GetSearchQuery() string { + if m != nil && m.SearchQuery != nil { + return *m.SearchQuery + } + return "" +} + +func (m *Query) GetOrder() []*Query_Order { + if m != nil { + return m.Order + } + return nil +} + +func (m *Query) GetHint() Query_Hint { + if m != nil && m.Hint != nil { + return *m.Hint + } + return Query_ORDER_FIRST +} + +func (m *Query) GetCount() int32 { + if m != nil && m.Count != nil { + return *m.Count + } + return 0 +} + +func (m *Query) GetOffset() int32 { + if m != nil && m.Offset != nil { + return *m.Offset + } + return Default_Query_Offset +} + +func (m *Query) GetLimit() int32 { + if m != nil && m.Limit != nil { + return *m.Limit + } + return 0 +} + +func (m *Query) GetCompiledCursor() *CompiledCursor { + if m != nil { + return m.CompiledCursor + } + return nil +} + +func (m *Query) GetEndCompiledCursor() *CompiledCursor { + if m != nil { + return m.EndCompiledCursor + } + return nil +} + +func (m *Query) GetCompositeIndex() []*CompositeIndex { + if m != nil { + return m.CompositeIndex + } + return nil +} + +func (m *Query) GetRequirePerfectPlan() bool { + if m != nil && m.RequirePerfectPlan != nil { + return *m.RequirePerfectPlan + } + return Default_Query_RequirePerfectPlan +} + +func (m *Query) GetKeysOnly() bool { + if m != nil && m.KeysOnly != nil { + return *m.KeysOnly + } + return Default_Query_KeysOnly +} + +func (m *Query) GetTransaction() *Transaction { + if m != nil { + return m.Transaction + } + return nil +} + +func (m *Query) GetCompile() bool { + if m != nil && m.Compile != nil { + return *m.Compile + } + return Default_Query_Compile +} + +func (m *Query) GetFailoverMs() int64 { + if m != nil && m.FailoverMs != nil { + return *m.FailoverMs + } + return 0 +} + +func (m *Query) GetStrong() bool { + if m != nil && m.Strong != nil { + return *m.Strong + } + return false +} + +func (m *Query) GetPropertyName() []string { + if m != nil { + return m.PropertyName + } + return nil +} + +func (m *Query) GetGroupByPropertyName() []string { + if m != nil { + return m.GroupByPropertyName + } + return nil +} + +func (m *Query) GetDistinct() bool { + if m != nil && m.Distinct != nil { + return *m.Distinct + } + return false +} + +func (m *Query) GetMinSafeTimeSeconds() int64 { + if m != nil && m.MinSafeTimeSeconds != nil { + return *m.MinSafeTimeSeconds + } + return 0 +} + +func (m *Query) GetSafeReplicaName() []string { + if m != nil { + return m.SafeReplicaName + } + return nil +} + +func (m *Query) GetPersistOffset() bool { + if m != nil && m.PersistOffset != nil { + return *m.PersistOffset + } + return Default_Query_PersistOffset +} + +type Query_Filter struct { + Op *Query_Filter_Operator `protobuf:"varint,6,req,name=op,enum=appengine.Query_Filter_Operator" json:"op,omitempty"` + Property []*Property `protobuf:"bytes,14,rep,name=property" json:"property,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Query_Filter) Reset() { *m = Query_Filter{} } +func (m *Query_Filter) String() string { return proto.CompactTextString(m) } +func (*Query_Filter) ProtoMessage() {} + +func (m *Query_Filter) GetOp() Query_Filter_Operator { + if m != nil && m.Op != nil { + return *m.Op + } + return Query_Filter_LESS_THAN +} + +func (m *Query_Filter) GetProperty() []*Property { + if m != nil { + return m.Property + } + return nil +} + +type Query_Order struct { + Property *string `protobuf:"bytes,10,req,name=property" json:"property,omitempty"` + Direction *Query_Order_Direction `protobuf:"varint,11,opt,name=direction,enum=appengine.Query_Order_Direction,def=1" json:"direction,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Query_Order) Reset() { *m = Query_Order{} } +func (m *Query_Order) String() string { return proto.CompactTextString(m) } +func (*Query_Order) ProtoMessage() {} + +const Default_Query_Order_Direction Query_Order_Direction = Query_Order_ASCENDING + +func (m *Query_Order) GetProperty() string { + if m != nil && m.Property != nil { + return *m.Property + } + return "" +} + +func (m *Query_Order) GetDirection() Query_Order_Direction { + if m != nil && m.Direction != nil { + return *m.Direction + } + return Default_Query_Order_Direction +} + +type CompiledQuery struct { + Primaryscan *CompiledQuery_PrimaryScan `protobuf:"group,1,req,name=PrimaryScan" json:"primaryscan,omitempty"` + Mergejoinscan []*CompiledQuery_MergeJoinScan `protobuf:"group,7,rep,name=MergeJoinScan" json:"mergejoinscan,omitempty"` + IndexDef *Index `protobuf:"bytes,21,opt,name=index_def" json:"index_def,omitempty"` + Offset *int32 `protobuf:"varint,10,opt,name=offset,def=0" json:"offset,omitempty"` + Limit *int32 `protobuf:"varint,11,opt,name=limit" json:"limit,omitempty"` + KeysOnly *bool `protobuf:"varint,12,req,name=keys_only" json:"keys_only,omitempty"` + PropertyName []string `protobuf:"bytes,24,rep,name=property_name" json:"property_name,omitempty"` + DistinctInfixSize *int32 `protobuf:"varint,25,opt,name=distinct_infix_size" json:"distinct_infix_size,omitempty"` + Entityfilter *CompiledQuery_EntityFilter `protobuf:"group,13,opt,name=EntityFilter" json:"entityfilter,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CompiledQuery) Reset() { *m = CompiledQuery{} } +func (m *CompiledQuery) String() string { return proto.CompactTextString(m) } +func (*CompiledQuery) ProtoMessage() {} + +const Default_CompiledQuery_Offset int32 = 0 + +func (m *CompiledQuery) GetPrimaryscan() *CompiledQuery_PrimaryScan { + if m != nil { + return m.Primaryscan + } + return nil +} + +func (m *CompiledQuery) GetMergejoinscan() []*CompiledQuery_MergeJoinScan { + if m != nil { + return m.Mergejoinscan + } + return nil +} + +func (m *CompiledQuery) GetIndexDef() *Index { + if m != nil { + return m.IndexDef + } + return nil +} + +func (m *CompiledQuery) GetOffset() int32 { + if m != nil && m.Offset != nil { + return *m.Offset + } + return Default_CompiledQuery_Offset +} + +func (m *CompiledQuery) GetLimit() int32 { + if m != nil && m.Limit != nil { + return *m.Limit + } + return 0 +} + +func (m *CompiledQuery) GetKeysOnly() bool { + if m != nil && m.KeysOnly != nil { + return *m.KeysOnly + } + return false +} + +func (m *CompiledQuery) GetPropertyName() []string { + if m != nil { + return m.PropertyName + } + return nil +} + +func (m *CompiledQuery) GetDistinctInfixSize() int32 { + if m != nil && m.DistinctInfixSize != nil { + return *m.DistinctInfixSize + } + return 0 +} + +func (m *CompiledQuery) GetEntityfilter() *CompiledQuery_EntityFilter { + if m != nil { + return m.Entityfilter + } + return nil +} + +type CompiledQuery_PrimaryScan struct { + IndexName *string `protobuf:"bytes,2,opt,name=index_name" json:"index_name,omitempty"` + StartKey *string `protobuf:"bytes,3,opt,name=start_key" json:"start_key,omitempty"` + StartInclusive *bool `protobuf:"varint,4,opt,name=start_inclusive" json:"start_inclusive,omitempty"` + EndKey *string `protobuf:"bytes,5,opt,name=end_key" json:"end_key,omitempty"` + EndInclusive *bool `protobuf:"varint,6,opt,name=end_inclusive" json:"end_inclusive,omitempty"` + StartPostfixValue []string `protobuf:"bytes,22,rep,name=start_postfix_value" json:"start_postfix_value,omitempty"` + EndPostfixValue []string `protobuf:"bytes,23,rep,name=end_postfix_value" json:"end_postfix_value,omitempty"` + EndUnappliedLogTimestampUs *int64 `protobuf:"varint,19,opt,name=end_unapplied_log_timestamp_us" json:"end_unapplied_log_timestamp_us,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CompiledQuery_PrimaryScan) Reset() { *m = CompiledQuery_PrimaryScan{} } +func (m *CompiledQuery_PrimaryScan) String() string { return proto.CompactTextString(m) } +func (*CompiledQuery_PrimaryScan) ProtoMessage() {} + +func (m *CompiledQuery_PrimaryScan) GetIndexName() string { + if m != nil && m.IndexName != nil { + return *m.IndexName + } + return "" +} + +func (m *CompiledQuery_PrimaryScan) GetStartKey() string { + if m != nil && m.StartKey != nil { + return *m.StartKey + } + return "" +} + +func (m *CompiledQuery_PrimaryScan) GetStartInclusive() bool { + if m != nil && m.StartInclusive != nil { + return *m.StartInclusive + } + return false +} + +func (m *CompiledQuery_PrimaryScan) GetEndKey() string { + if m != nil && m.EndKey != nil { + return *m.EndKey + } + return "" +} + +func (m *CompiledQuery_PrimaryScan) GetEndInclusive() bool { + if m != nil && m.EndInclusive != nil { + return *m.EndInclusive + } + return false +} + +func (m *CompiledQuery_PrimaryScan) GetStartPostfixValue() []string { + if m != nil { + return m.StartPostfixValue + } + return nil +} + +func (m *CompiledQuery_PrimaryScan) GetEndPostfixValue() []string { + if m != nil { + return m.EndPostfixValue + } + return nil +} + +func (m *CompiledQuery_PrimaryScan) GetEndUnappliedLogTimestampUs() int64 { + if m != nil && m.EndUnappliedLogTimestampUs != nil { + return *m.EndUnappliedLogTimestampUs + } + return 0 +} + +type CompiledQuery_MergeJoinScan struct { + IndexName *string `protobuf:"bytes,8,req,name=index_name" json:"index_name,omitempty"` + PrefixValue []string `protobuf:"bytes,9,rep,name=prefix_value" json:"prefix_value,omitempty"` + ValuePrefix *bool `protobuf:"varint,20,opt,name=value_prefix,def=0" json:"value_prefix,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CompiledQuery_MergeJoinScan) Reset() { *m = CompiledQuery_MergeJoinScan{} } +func (m *CompiledQuery_MergeJoinScan) String() string { return proto.CompactTextString(m) } +func (*CompiledQuery_MergeJoinScan) ProtoMessage() {} + +const Default_CompiledQuery_MergeJoinScan_ValuePrefix bool = false + +func (m *CompiledQuery_MergeJoinScan) GetIndexName() string { + if m != nil && m.IndexName != nil { + return *m.IndexName + } + return "" +} + +func (m *CompiledQuery_MergeJoinScan) GetPrefixValue() []string { + if m != nil { + return m.PrefixValue + } + return nil +} + +func (m *CompiledQuery_MergeJoinScan) GetValuePrefix() bool { + if m != nil && m.ValuePrefix != nil { + return *m.ValuePrefix + } + return Default_CompiledQuery_MergeJoinScan_ValuePrefix +} + +type CompiledQuery_EntityFilter struct { + Distinct *bool `protobuf:"varint,14,opt,name=distinct,def=0" json:"distinct,omitempty"` + Kind *string `protobuf:"bytes,17,opt,name=kind" json:"kind,omitempty"` + Ancestor *Reference `protobuf:"bytes,18,opt,name=ancestor" json:"ancestor,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CompiledQuery_EntityFilter) Reset() { *m = CompiledQuery_EntityFilter{} } +func (m *CompiledQuery_EntityFilter) String() string { return proto.CompactTextString(m) } +func (*CompiledQuery_EntityFilter) ProtoMessage() {} + +const Default_CompiledQuery_EntityFilter_Distinct bool = false + +func (m *CompiledQuery_EntityFilter) GetDistinct() bool { + if m != nil && m.Distinct != nil { + return *m.Distinct + } + return Default_CompiledQuery_EntityFilter_Distinct +} + +func (m *CompiledQuery_EntityFilter) GetKind() string { + if m != nil && m.Kind != nil { + return *m.Kind + } + return "" +} + +func (m *CompiledQuery_EntityFilter) GetAncestor() *Reference { + if m != nil { + return m.Ancestor + } + return nil +} + +type CompiledCursor struct { + Position *CompiledCursor_Position `protobuf:"group,2,opt,name=Position" json:"position,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CompiledCursor) Reset() { *m = CompiledCursor{} } +func (m *CompiledCursor) String() string { return proto.CompactTextString(m) } +func (*CompiledCursor) ProtoMessage() {} + +func (m *CompiledCursor) GetPosition() *CompiledCursor_Position { + if m != nil { + return m.Position + } + return nil +} + +type CompiledCursor_Position struct { + StartKey *string `protobuf:"bytes,27,opt,name=start_key" json:"start_key,omitempty"` + Indexvalue []*CompiledCursor_Position_IndexValue `protobuf:"group,29,rep,name=IndexValue" json:"indexvalue,omitempty"` + Key *Reference `protobuf:"bytes,32,opt,name=key" json:"key,omitempty"` + StartInclusive *bool `protobuf:"varint,28,opt,name=start_inclusive,def=1" json:"start_inclusive,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CompiledCursor_Position) Reset() { *m = CompiledCursor_Position{} } +func (m *CompiledCursor_Position) String() string { return proto.CompactTextString(m) } +func (*CompiledCursor_Position) ProtoMessage() {} + +const Default_CompiledCursor_Position_StartInclusive bool = true + +func (m *CompiledCursor_Position) GetStartKey() string { + if m != nil && m.StartKey != nil { + return *m.StartKey + } + return "" +} + +func (m *CompiledCursor_Position) GetIndexvalue() []*CompiledCursor_Position_IndexValue { + if m != nil { + return m.Indexvalue + } + return nil +} + +func (m *CompiledCursor_Position) GetKey() *Reference { + if m != nil { + return m.Key + } + return nil +} + +func (m *CompiledCursor_Position) GetStartInclusive() bool { + if m != nil && m.StartInclusive != nil { + return *m.StartInclusive + } + return Default_CompiledCursor_Position_StartInclusive +} + +type CompiledCursor_Position_IndexValue struct { + Property *string `protobuf:"bytes,30,opt,name=property" json:"property,omitempty"` + Value *PropertyValue `protobuf:"bytes,31,req,name=value" json:"value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CompiledCursor_Position_IndexValue) Reset() { *m = CompiledCursor_Position_IndexValue{} } +func (m *CompiledCursor_Position_IndexValue) String() string { return proto.CompactTextString(m) } +func (*CompiledCursor_Position_IndexValue) ProtoMessage() {} + +func (m *CompiledCursor_Position_IndexValue) GetProperty() string { + if m != nil && m.Property != nil { + return *m.Property + } + return "" +} + +func (m *CompiledCursor_Position_IndexValue) GetValue() *PropertyValue { + if m != nil { + return m.Value + } + return nil +} + +type Cursor struct { + Cursor *uint64 `protobuf:"fixed64,1,req,name=cursor" json:"cursor,omitempty"` + App *string `protobuf:"bytes,2,opt,name=app" json:"app,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Cursor) Reset() { *m = Cursor{} } +func (m *Cursor) String() string { return proto.CompactTextString(m) } +func (*Cursor) ProtoMessage() {} + +func (m *Cursor) GetCursor() uint64 { + if m != nil && m.Cursor != nil { + return *m.Cursor + } + return 0 +} + +func (m *Cursor) GetApp() string { + if m != nil && m.App != nil { + return *m.App + } + return "" +} + +type Error struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *Error) Reset() { *m = Error{} } +func (m *Error) String() string { return proto.CompactTextString(m) } +func (*Error) ProtoMessage() {} + +type Cost struct { + IndexWrites *int32 `protobuf:"varint,1,opt,name=index_writes" json:"index_writes,omitempty"` + IndexWriteBytes *int32 `protobuf:"varint,2,opt,name=index_write_bytes" json:"index_write_bytes,omitempty"` + EntityWrites *int32 `protobuf:"varint,3,opt,name=entity_writes" json:"entity_writes,omitempty"` + EntityWriteBytes *int32 `protobuf:"varint,4,opt,name=entity_write_bytes" json:"entity_write_bytes,omitempty"` + Commitcost *Cost_CommitCost `protobuf:"group,5,opt,name=CommitCost" json:"commitcost,omitempty"` + ApproximateStorageDelta *int32 `protobuf:"varint,8,opt,name=approximate_storage_delta" json:"approximate_storage_delta,omitempty"` + IdSequenceUpdates *int32 `protobuf:"varint,9,opt,name=id_sequence_updates" json:"id_sequence_updates,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Cost) Reset() { *m = Cost{} } +func (m *Cost) String() string { return proto.CompactTextString(m) } +func (*Cost) ProtoMessage() {} + +func (m *Cost) GetIndexWrites() int32 { + if m != nil && m.IndexWrites != nil { + return *m.IndexWrites + } + return 0 +} + +func (m *Cost) GetIndexWriteBytes() int32 { + if m != nil && m.IndexWriteBytes != nil { + return *m.IndexWriteBytes + } + return 0 +} + +func (m *Cost) GetEntityWrites() int32 { + if m != nil && m.EntityWrites != nil { + return *m.EntityWrites + } + return 0 +} + +func (m *Cost) GetEntityWriteBytes() int32 { + if m != nil && m.EntityWriteBytes != nil { + return *m.EntityWriteBytes + } + return 0 +} + +func (m *Cost) GetCommitcost() *Cost_CommitCost { + if m != nil { + return m.Commitcost + } + return nil +} + +func (m *Cost) GetApproximateStorageDelta() int32 { + if m != nil && m.ApproximateStorageDelta != nil { + return *m.ApproximateStorageDelta + } + return 0 +} + +func (m *Cost) GetIdSequenceUpdates() int32 { + if m != nil && m.IdSequenceUpdates != nil { + return *m.IdSequenceUpdates + } + return 0 +} + +type Cost_CommitCost struct { + RequestedEntityPuts *int32 `protobuf:"varint,6,opt,name=requested_entity_puts" json:"requested_entity_puts,omitempty"` + RequestedEntityDeletes *int32 `protobuf:"varint,7,opt,name=requested_entity_deletes" json:"requested_entity_deletes,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Cost_CommitCost) Reset() { *m = Cost_CommitCost{} } +func (m *Cost_CommitCost) String() string { return proto.CompactTextString(m) } +func (*Cost_CommitCost) ProtoMessage() {} + +func (m *Cost_CommitCost) GetRequestedEntityPuts() int32 { + if m != nil && m.RequestedEntityPuts != nil { + return *m.RequestedEntityPuts + } + return 0 +} + +func (m *Cost_CommitCost) GetRequestedEntityDeletes() int32 { + if m != nil && m.RequestedEntityDeletes != nil { + return *m.RequestedEntityDeletes + } + return 0 +} + +type GetRequest struct { + Header *InternalHeader `protobuf:"bytes,6,opt,name=header" json:"header,omitempty"` + Key []*Reference `protobuf:"bytes,1,rep,name=key" json:"key,omitempty"` + Transaction *Transaction `protobuf:"bytes,2,opt,name=transaction" json:"transaction,omitempty"` + FailoverMs *int64 `protobuf:"varint,3,opt,name=failover_ms" json:"failover_ms,omitempty"` + Strong *bool `protobuf:"varint,4,opt,name=strong" json:"strong,omitempty"` + AllowDeferred *bool `protobuf:"varint,5,opt,name=allow_deferred,def=0" json:"allow_deferred,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetRequest) Reset() { *m = GetRequest{} } +func (m *GetRequest) String() string { return proto.CompactTextString(m) } +func (*GetRequest) ProtoMessage() {} + +const Default_GetRequest_AllowDeferred bool = false + +func (m *GetRequest) GetHeader() *InternalHeader { + if m != nil { + return m.Header + } + return nil +} + +func (m *GetRequest) GetKey() []*Reference { + if m != nil { + return m.Key + } + return nil +} + +func (m *GetRequest) GetTransaction() *Transaction { + if m != nil { + return m.Transaction + } + return nil +} + +func (m *GetRequest) GetFailoverMs() int64 { + if m != nil && m.FailoverMs != nil { + return *m.FailoverMs + } + return 0 +} + +func (m *GetRequest) GetStrong() bool { + if m != nil && m.Strong != nil { + return *m.Strong + } + return false +} + +func (m *GetRequest) GetAllowDeferred() bool { + if m != nil && m.AllowDeferred != nil { + return *m.AllowDeferred + } + return Default_GetRequest_AllowDeferred +} + +type GetResponse struct { + Entity []*GetResponse_Entity `protobuf:"group,1,rep,name=Entity" json:"entity,omitempty"` + Deferred []*Reference `protobuf:"bytes,5,rep,name=deferred" json:"deferred,omitempty"` + InOrder *bool `protobuf:"varint,6,opt,name=in_order,def=1" json:"in_order,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetResponse) Reset() { *m = GetResponse{} } +func (m *GetResponse) String() string { return proto.CompactTextString(m) } +func (*GetResponse) ProtoMessage() {} + +const Default_GetResponse_InOrder bool = true + +func (m *GetResponse) GetEntity() []*GetResponse_Entity { + if m != nil { + return m.Entity + } + return nil +} + +func (m *GetResponse) GetDeferred() []*Reference { + if m != nil { + return m.Deferred + } + return nil +} + +func (m *GetResponse) GetInOrder() bool { + if m != nil && m.InOrder != nil { + return *m.InOrder + } + return Default_GetResponse_InOrder +} + +type GetResponse_Entity struct { + Entity *EntityProto `protobuf:"bytes,2,opt,name=entity" json:"entity,omitempty"` + Key *Reference `protobuf:"bytes,4,opt,name=key" json:"key,omitempty"` + Version *int64 `protobuf:"varint,3,opt,name=version" json:"version,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetResponse_Entity) Reset() { *m = GetResponse_Entity{} } +func (m *GetResponse_Entity) String() string { return proto.CompactTextString(m) } +func (*GetResponse_Entity) ProtoMessage() {} + +func (m *GetResponse_Entity) GetEntity() *EntityProto { + if m != nil { + return m.Entity + } + return nil +} + +func (m *GetResponse_Entity) GetKey() *Reference { + if m != nil { + return m.Key + } + return nil +} + +func (m *GetResponse_Entity) GetVersion() int64 { + if m != nil && m.Version != nil { + return *m.Version + } + return 0 +} + +type PutRequest struct { + Header *InternalHeader `protobuf:"bytes,11,opt,name=header" json:"header,omitempty"` + Entity []*EntityProto `protobuf:"bytes,1,rep,name=entity" json:"entity,omitempty"` + Transaction *Transaction `protobuf:"bytes,2,opt,name=transaction" json:"transaction,omitempty"` + CompositeIndex []*CompositeIndex `protobuf:"bytes,3,rep,name=composite_index" json:"composite_index,omitempty"` + Trusted *bool `protobuf:"varint,4,opt,name=trusted,def=0" json:"trusted,omitempty"` + Force *bool `protobuf:"varint,7,opt,name=force,def=0" json:"force,omitempty"` + MarkChanges *bool `protobuf:"varint,8,opt,name=mark_changes,def=0" json:"mark_changes,omitempty"` + Snapshot []*Snapshot `protobuf:"bytes,9,rep,name=snapshot" json:"snapshot,omitempty"` + AutoIdPolicy *PutRequest_AutoIdPolicy `protobuf:"varint,10,opt,name=auto_id_policy,enum=appengine.PutRequest_AutoIdPolicy,def=0" json:"auto_id_policy,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *PutRequest) Reset() { *m = PutRequest{} } +func (m *PutRequest) String() string { return proto.CompactTextString(m) } +func (*PutRequest) ProtoMessage() {} + +const Default_PutRequest_Trusted bool = false +const Default_PutRequest_Force bool = false +const Default_PutRequest_MarkChanges bool = false +const Default_PutRequest_AutoIdPolicy PutRequest_AutoIdPolicy = PutRequest_CURRENT + +func (m *PutRequest) GetHeader() *InternalHeader { + if m != nil { + return m.Header + } + return nil +} + +func (m *PutRequest) GetEntity() []*EntityProto { + if m != nil { + return m.Entity + } + return nil +} + +func (m *PutRequest) GetTransaction() *Transaction { + if m != nil { + return m.Transaction + } + return nil +} + +func (m *PutRequest) GetCompositeIndex() []*CompositeIndex { + if m != nil { + return m.CompositeIndex + } + return nil +} + +func (m *PutRequest) GetTrusted() bool { + if m != nil && m.Trusted != nil { + return *m.Trusted + } + return Default_PutRequest_Trusted +} + +func (m *PutRequest) GetForce() bool { + if m != nil && m.Force != nil { + return *m.Force + } + return Default_PutRequest_Force +} + +func (m *PutRequest) GetMarkChanges() bool { + if m != nil && m.MarkChanges != nil { + return *m.MarkChanges + } + return Default_PutRequest_MarkChanges +} + +func (m *PutRequest) GetSnapshot() []*Snapshot { + if m != nil { + return m.Snapshot + } + return nil +} + +func (m *PutRequest) GetAutoIdPolicy() PutRequest_AutoIdPolicy { + if m != nil && m.AutoIdPolicy != nil { + return *m.AutoIdPolicy + } + return Default_PutRequest_AutoIdPolicy +} + +type PutResponse struct { + Key []*Reference `protobuf:"bytes,1,rep,name=key" json:"key,omitempty"` + Cost *Cost `protobuf:"bytes,2,opt,name=cost" json:"cost,omitempty"` + Version []int64 `protobuf:"varint,3,rep,name=version" json:"version,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *PutResponse) Reset() { *m = PutResponse{} } +func (m *PutResponse) String() string { return proto.CompactTextString(m) } +func (*PutResponse) ProtoMessage() {} + +func (m *PutResponse) GetKey() []*Reference { + if m != nil { + return m.Key + } + return nil +} + +func (m *PutResponse) GetCost() *Cost { + if m != nil { + return m.Cost + } + return nil +} + +func (m *PutResponse) GetVersion() []int64 { + if m != nil { + return m.Version + } + return nil +} + +type TouchRequest struct { + Header *InternalHeader `protobuf:"bytes,10,opt,name=header" json:"header,omitempty"` + Key []*Reference `protobuf:"bytes,1,rep,name=key" json:"key,omitempty"` + CompositeIndex []*CompositeIndex `protobuf:"bytes,2,rep,name=composite_index" json:"composite_index,omitempty"` + Force *bool `protobuf:"varint,3,opt,name=force,def=0" json:"force,omitempty"` + Snapshot []*Snapshot `protobuf:"bytes,9,rep,name=snapshot" json:"snapshot,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *TouchRequest) Reset() { *m = TouchRequest{} } +func (m *TouchRequest) String() string { return proto.CompactTextString(m) } +func (*TouchRequest) ProtoMessage() {} + +const Default_TouchRequest_Force bool = false + +func (m *TouchRequest) GetHeader() *InternalHeader { + if m != nil { + return m.Header + } + return nil +} + +func (m *TouchRequest) GetKey() []*Reference { + if m != nil { + return m.Key + } + return nil +} + +func (m *TouchRequest) GetCompositeIndex() []*CompositeIndex { + if m != nil { + return m.CompositeIndex + } + return nil +} + +func (m *TouchRequest) GetForce() bool { + if m != nil && m.Force != nil { + return *m.Force + } + return Default_TouchRequest_Force +} + +func (m *TouchRequest) GetSnapshot() []*Snapshot { + if m != nil { + return m.Snapshot + } + return nil +} + +type TouchResponse struct { + Cost *Cost `protobuf:"bytes,1,opt,name=cost" json:"cost,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *TouchResponse) Reset() { *m = TouchResponse{} } +func (m *TouchResponse) String() string { return proto.CompactTextString(m) } +func (*TouchResponse) ProtoMessage() {} + +func (m *TouchResponse) GetCost() *Cost { + if m != nil { + return m.Cost + } + return nil +} + +type DeleteRequest struct { + Header *InternalHeader `protobuf:"bytes,10,opt,name=header" json:"header,omitempty"` + Key []*Reference `protobuf:"bytes,6,rep,name=key" json:"key,omitempty"` + Transaction *Transaction `protobuf:"bytes,5,opt,name=transaction" json:"transaction,omitempty"` + Trusted *bool `protobuf:"varint,4,opt,name=trusted,def=0" json:"trusted,omitempty"` + Force *bool `protobuf:"varint,7,opt,name=force,def=0" json:"force,omitempty"` + MarkChanges *bool `protobuf:"varint,8,opt,name=mark_changes,def=0" json:"mark_changes,omitempty"` + Snapshot []*Snapshot `protobuf:"bytes,9,rep,name=snapshot" json:"snapshot,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *DeleteRequest) Reset() { *m = DeleteRequest{} } +func (m *DeleteRequest) String() string { return proto.CompactTextString(m) } +func (*DeleteRequest) ProtoMessage() {} + +const Default_DeleteRequest_Trusted bool = false +const Default_DeleteRequest_Force bool = false +const Default_DeleteRequest_MarkChanges bool = false + +func (m *DeleteRequest) GetHeader() *InternalHeader { + if m != nil { + return m.Header + } + return nil +} + +func (m *DeleteRequest) GetKey() []*Reference { + if m != nil { + return m.Key + } + return nil +} + +func (m *DeleteRequest) GetTransaction() *Transaction { + if m != nil { + return m.Transaction + } + return nil +} + +func (m *DeleteRequest) GetTrusted() bool { + if m != nil && m.Trusted != nil { + return *m.Trusted + } + return Default_DeleteRequest_Trusted +} + +func (m *DeleteRequest) GetForce() bool { + if m != nil && m.Force != nil { + return *m.Force + } + return Default_DeleteRequest_Force +} + +func (m *DeleteRequest) GetMarkChanges() bool { + if m != nil && m.MarkChanges != nil { + return *m.MarkChanges + } + return Default_DeleteRequest_MarkChanges +} + +func (m *DeleteRequest) GetSnapshot() []*Snapshot { + if m != nil { + return m.Snapshot + } + return nil +} + +type DeleteResponse struct { + Cost *Cost `protobuf:"bytes,1,opt,name=cost" json:"cost,omitempty"` + Version []int64 `protobuf:"varint,3,rep,name=version" json:"version,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *DeleteResponse) Reset() { *m = DeleteResponse{} } +func (m *DeleteResponse) String() string { return proto.CompactTextString(m) } +func (*DeleteResponse) ProtoMessage() {} + +func (m *DeleteResponse) GetCost() *Cost { + if m != nil { + return m.Cost + } + return nil +} + +func (m *DeleteResponse) GetVersion() []int64 { + if m != nil { + return m.Version + } + return nil +} + +type NextRequest struct { + Header *InternalHeader `protobuf:"bytes,5,opt,name=header" json:"header,omitempty"` + Cursor *Cursor `protobuf:"bytes,1,req,name=cursor" json:"cursor,omitempty"` + Count *int32 `protobuf:"varint,2,opt,name=count" json:"count,omitempty"` + Offset *int32 `protobuf:"varint,4,opt,name=offset,def=0" json:"offset,omitempty"` + Compile *bool `protobuf:"varint,3,opt,name=compile,def=0" json:"compile,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *NextRequest) Reset() { *m = NextRequest{} } +func (m *NextRequest) String() string { return proto.CompactTextString(m) } +func (*NextRequest) ProtoMessage() {} + +const Default_NextRequest_Offset int32 = 0 +const Default_NextRequest_Compile bool = false + +func (m *NextRequest) GetHeader() *InternalHeader { + if m != nil { + return m.Header + } + return nil +} + +func (m *NextRequest) GetCursor() *Cursor { + if m != nil { + return m.Cursor + } + return nil +} + +func (m *NextRequest) GetCount() int32 { + if m != nil && m.Count != nil { + return *m.Count + } + return 0 +} + +func (m *NextRequest) GetOffset() int32 { + if m != nil && m.Offset != nil { + return *m.Offset + } + return Default_NextRequest_Offset +} + +func (m *NextRequest) GetCompile() bool { + if m != nil && m.Compile != nil { + return *m.Compile + } + return Default_NextRequest_Compile +} + +type QueryResult struct { + Cursor *Cursor `protobuf:"bytes,1,opt,name=cursor" json:"cursor,omitempty"` + Result []*EntityProto `protobuf:"bytes,2,rep,name=result" json:"result,omitempty"` + SkippedResults *int32 `protobuf:"varint,7,opt,name=skipped_results" json:"skipped_results,omitempty"` + MoreResults *bool `protobuf:"varint,3,req,name=more_results" json:"more_results,omitempty"` + KeysOnly *bool `protobuf:"varint,4,opt,name=keys_only" json:"keys_only,omitempty"` + IndexOnly *bool `protobuf:"varint,9,opt,name=index_only" json:"index_only,omitempty"` + SmallOps *bool `protobuf:"varint,10,opt,name=small_ops" json:"small_ops,omitempty"` + CompiledQuery *CompiledQuery `protobuf:"bytes,5,opt,name=compiled_query" json:"compiled_query,omitempty"` + CompiledCursor *CompiledCursor `protobuf:"bytes,6,opt,name=compiled_cursor" json:"compiled_cursor,omitempty"` + Index []*CompositeIndex `protobuf:"bytes,8,rep,name=index" json:"index,omitempty"` + Version []int64 `protobuf:"varint,11,rep,name=version" json:"version,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *QueryResult) Reset() { *m = QueryResult{} } +func (m *QueryResult) String() string { return proto.CompactTextString(m) } +func (*QueryResult) ProtoMessage() {} + +func (m *QueryResult) GetCursor() *Cursor { + if m != nil { + return m.Cursor + } + return nil +} + +func (m *QueryResult) GetResult() []*EntityProto { + if m != nil { + return m.Result + } + return nil +} + +func (m *QueryResult) GetSkippedResults() int32 { + if m != nil && m.SkippedResults != nil { + return *m.SkippedResults + } + return 0 +} + +func (m *QueryResult) GetMoreResults() bool { + if m != nil && m.MoreResults != nil { + return *m.MoreResults + } + return false +} + +func (m *QueryResult) GetKeysOnly() bool { + if m != nil && m.KeysOnly != nil { + return *m.KeysOnly + } + return false +} + +func (m *QueryResult) GetIndexOnly() bool { + if m != nil && m.IndexOnly != nil { + return *m.IndexOnly + } + return false +} + +func (m *QueryResult) GetSmallOps() bool { + if m != nil && m.SmallOps != nil { + return *m.SmallOps + } + return false +} + +func (m *QueryResult) GetCompiledQuery() *CompiledQuery { + if m != nil { + return m.CompiledQuery + } + return nil +} + +func (m *QueryResult) GetCompiledCursor() *CompiledCursor { + if m != nil { + return m.CompiledCursor + } + return nil +} + +func (m *QueryResult) GetIndex() []*CompositeIndex { + if m != nil { + return m.Index + } + return nil +} + +func (m *QueryResult) GetVersion() []int64 { + if m != nil { + return m.Version + } + return nil +} + +type AllocateIdsRequest struct { + Header *InternalHeader `protobuf:"bytes,4,opt,name=header" json:"header,omitempty"` + ModelKey *Reference `protobuf:"bytes,1,opt,name=model_key" json:"model_key,omitempty"` + Size *int64 `protobuf:"varint,2,opt,name=size" json:"size,omitempty"` + Max *int64 `protobuf:"varint,3,opt,name=max" json:"max,omitempty"` + Reserve []*Reference `protobuf:"bytes,5,rep,name=reserve" json:"reserve,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *AllocateIdsRequest) Reset() { *m = AllocateIdsRequest{} } +func (m *AllocateIdsRequest) String() string { return proto.CompactTextString(m) } +func (*AllocateIdsRequest) ProtoMessage() {} + +func (m *AllocateIdsRequest) GetHeader() *InternalHeader { + if m != nil { + return m.Header + } + return nil +} + +func (m *AllocateIdsRequest) GetModelKey() *Reference { + if m != nil { + return m.ModelKey + } + return nil +} + +func (m *AllocateIdsRequest) GetSize() int64 { + if m != nil && m.Size != nil { + return *m.Size + } + return 0 +} + +func (m *AllocateIdsRequest) GetMax() int64 { + if m != nil && m.Max != nil { + return *m.Max + } + return 0 +} + +func (m *AllocateIdsRequest) GetReserve() []*Reference { + if m != nil { + return m.Reserve + } + return nil +} + +type AllocateIdsResponse struct { + Start *int64 `protobuf:"varint,1,req,name=start" json:"start,omitempty"` + End *int64 `protobuf:"varint,2,req,name=end" json:"end,omitempty"` + Cost *Cost `protobuf:"bytes,3,opt,name=cost" json:"cost,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *AllocateIdsResponse) Reset() { *m = AllocateIdsResponse{} } +func (m *AllocateIdsResponse) String() string { return proto.CompactTextString(m) } +func (*AllocateIdsResponse) ProtoMessage() {} + +func (m *AllocateIdsResponse) GetStart() int64 { + if m != nil && m.Start != nil { + return *m.Start + } + return 0 +} + +func (m *AllocateIdsResponse) GetEnd() int64 { + if m != nil && m.End != nil { + return *m.End + } + return 0 +} + +func (m *AllocateIdsResponse) GetCost() *Cost { + if m != nil { + return m.Cost + } + return nil +} + +type CompositeIndices struct { + Index []*CompositeIndex `protobuf:"bytes,1,rep,name=index" json:"index,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CompositeIndices) Reset() { *m = CompositeIndices{} } +func (m *CompositeIndices) String() string { return proto.CompactTextString(m) } +func (*CompositeIndices) ProtoMessage() {} + +func (m *CompositeIndices) GetIndex() []*CompositeIndex { + if m != nil { + return m.Index + } + return nil +} + +type AddActionsRequest struct { + Header *InternalHeader `protobuf:"bytes,3,opt,name=header" json:"header,omitempty"` + Transaction *Transaction `protobuf:"bytes,1,req,name=transaction" json:"transaction,omitempty"` + Action []*Action `protobuf:"bytes,2,rep,name=action" json:"action,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *AddActionsRequest) Reset() { *m = AddActionsRequest{} } +func (m *AddActionsRequest) String() string { return proto.CompactTextString(m) } +func (*AddActionsRequest) ProtoMessage() {} + +func (m *AddActionsRequest) GetHeader() *InternalHeader { + if m != nil { + return m.Header + } + return nil +} + +func (m *AddActionsRequest) GetTransaction() *Transaction { + if m != nil { + return m.Transaction + } + return nil +} + +func (m *AddActionsRequest) GetAction() []*Action { + if m != nil { + return m.Action + } + return nil +} + +type AddActionsResponse struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *AddActionsResponse) Reset() { *m = AddActionsResponse{} } +func (m *AddActionsResponse) String() string { return proto.CompactTextString(m) } +func (*AddActionsResponse) ProtoMessage() {} + +type BeginTransactionRequest struct { + Header *InternalHeader `protobuf:"bytes,3,opt,name=header" json:"header,omitempty"` + App *string `protobuf:"bytes,1,req,name=app" json:"app,omitempty"` + AllowMultipleEg *bool `protobuf:"varint,2,opt,name=allow_multiple_eg,def=0" json:"allow_multiple_eg,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *BeginTransactionRequest) Reset() { *m = BeginTransactionRequest{} } +func (m *BeginTransactionRequest) String() string { return proto.CompactTextString(m) } +func (*BeginTransactionRequest) ProtoMessage() {} + +const Default_BeginTransactionRequest_AllowMultipleEg bool = false + +func (m *BeginTransactionRequest) GetHeader() *InternalHeader { + if m != nil { + return m.Header + } + return nil +} + +func (m *BeginTransactionRequest) GetApp() string { + if m != nil && m.App != nil { + return *m.App + } + return "" +} + +func (m *BeginTransactionRequest) GetAllowMultipleEg() bool { + if m != nil && m.AllowMultipleEg != nil { + return *m.AllowMultipleEg + } + return Default_BeginTransactionRequest_AllowMultipleEg +} + +type CommitResponse struct { + Cost *Cost `protobuf:"bytes,1,opt,name=cost" json:"cost,omitempty"` + Version []*CommitResponse_Version `protobuf:"group,3,rep,name=Version" json:"version,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CommitResponse) Reset() { *m = CommitResponse{} } +func (m *CommitResponse) String() string { return proto.CompactTextString(m) } +func (*CommitResponse) ProtoMessage() {} + +func (m *CommitResponse) GetCost() *Cost { + if m != nil { + return m.Cost + } + return nil +} + +func (m *CommitResponse) GetVersion() []*CommitResponse_Version { + if m != nil { + return m.Version + } + return nil +} + +type CommitResponse_Version struct { + RootEntityKey *Reference `protobuf:"bytes,4,req,name=root_entity_key" json:"root_entity_key,omitempty"` + Version *int64 `protobuf:"varint,5,req,name=version" json:"version,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *CommitResponse_Version) Reset() { *m = CommitResponse_Version{} } +func (m *CommitResponse_Version) String() string { return proto.CompactTextString(m) } +func (*CommitResponse_Version) ProtoMessage() {} + +func (m *CommitResponse_Version) GetRootEntityKey() *Reference { + if m != nil { + return m.RootEntityKey + } + return nil +} + +func (m *CommitResponse_Version) GetVersion() int64 { + if m != nil && m.Version != nil { + return *m.Version + } + return 0 +} + +func init() { +} diff --git a/vendor/google.golang.org/appengine/internal/identity.go b/vendor/google.golang.org/appengine/internal/identity.go new file mode 100644 index 00000000000..d538701ab3b --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/identity.go @@ -0,0 +1,14 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package internal + +import netcontext "golang.org/x/net/context" + +// These functions are implementations of the wrapper functions +// in ../appengine/identity.go. See that file for commentary. + +func AppID(c netcontext.Context) string { + return appID(FullyQualifiedAppID(c)) +} diff --git a/vendor/google.golang.org/appengine/internal/identity_classic.go b/vendor/google.golang.org/appengine/internal/identity_classic.go new file mode 100644 index 00000000000..e6b9227c566 --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/identity_classic.go @@ -0,0 +1,27 @@ +// Copyright 2015 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// +build appengine + +package internal + +import ( + "appengine" + + netcontext "golang.org/x/net/context" +) + +func DefaultVersionHostname(ctx netcontext.Context) string { + return appengine.DefaultVersionHostname(fromContext(ctx)) +} + +func RequestID(ctx netcontext.Context) string { return appengine.RequestID(fromContext(ctx)) } +func Datacenter(_ netcontext.Context) string { return appengine.Datacenter() } +func ServerSoftware() string { return appengine.ServerSoftware() } +func ModuleName(ctx netcontext.Context) string { return appengine.ModuleName(fromContext(ctx)) } +func VersionID(ctx netcontext.Context) string { return appengine.VersionID(fromContext(ctx)) } +func InstanceID() string { return appengine.InstanceID() } +func IsDevAppServer() bool { return appengine.IsDevAppServer() } + +func fullyQualifiedAppID(ctx netcontext.Context) string { return fromContext(ctx).FullyQualifiedAppID() } diff --git a/vendor/google.golang.org/appengine/internal/identity_vm.go b/vendor/google.golang.org/appengine/internal/identity_vm.go new file mode 100644 index 00000000000..ebe68b785bb --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/identity_vm.go @@ -0,0 +1,97 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// +build !appengine + +package internal + +import ( + "net/http" + "os" + + netcontext "golang.org/x/net/context" +) + +// These functions are implementations of the wrapper functions +// in ../appengine/identity.go. See that file for commentary. + +const ( + hDefaultVersionHostname = "X-AppEngine-Default-Version-Hostname" + hRequestLogId = "X-AppEngine-Request-Log-Id" + hDatacenter = "X-AppEngine-Datacenter" +) + +func ctxHeaders(ctx netcontext.Context) http.Header { + return fromContext(ctx).Request().Header +} + +func DefaultVersionHostname(ctx netcontext.Context) string { + return ctxHeaders(ctx).Get(hDefaultVersionHostname) +} + +func RequestID(ctx netcontext.Context) string { + return ctxHeaders(ctx).Get(hRequestLogId) +} + +func Datacenter(ctx netcontext.Context) string { + return ctxHeaders(ctx).Get(hDatacenter) +} + +func ServerSoftware() string { + // TODO(dsymonds): Remove fallback when we've verified this. + if s := os.Getenv("SERVER_SOFTWARE"); s != "" { + return s + } + return "Google App Engine/1.x.x" +} + +// TODO(dsymonds): Remove the metadata fetches. + +func ModuleName(_ netcontext.Context) string { + if s := os.Getenv("GAE_MODULE_NAME"); s != "" { + return s + } + return string(mustGetMetadata("instance/attributes/gae_backend_name")) +} + +func VersionID(_ netcontext.Context) string { + if s1, s2 := os.Getenv("GAE_MODULE_VERSION"), os.Getenv("GAE_MINOR_VERSION"); s1 != "" && s2 != "" { + return s1 + "." + s2 + } + return string(mustGetMetadata("instance/attributes/gae_backend_version")) + "." + string(mustGetMetadata("instance/attributes/gae_backend_minor_version")) +} + +func InstanceID() string { + if s := os.Getenv("GAE_MODULE_INSTANCE"); s != "" { + return s + } + return string(mustGetMetadata("instance/attributes/gae_backend_instance")) +} + +func partitionlessAppID() string { + // gae_project has everything except the partition prefix. + appID := os.Getenv("GAE_LONG_APP_ID") + if appID == "" { + appID = string(mustGetMetadata("instance/attributes/gae_project")) + } + return appID +} + +func fullyQualifiedAppID(_ netcontext.Context) string { + appID := partitionlessAppID() + + part := os.Getenv("GAE_PARTITION") + if part == "" { + part = string(mustGetMetadata("instance/attributes/gae_partition")) + } + + if part != "" { + appID = part + "~" + appID + } + return appID +} + +func IsDevAppServer() bool { + return os.Getenv("RUN_WITH_DEVAPPSERVER") != "" +} diff --git a/vendor/google.golang.org/appengine/internal/internal.go b/vendor/google.golang.org/appengine/internal/internal.go new file mode 100644 index 00000000000..051ea3980ab --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/internal.go @@ -0,0 +1,110 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// Package internal provides support for package appengine. +// +// Programs should not use this package directly. Its API is not stable. +// Use packages appengine and appengine/* instead. +package internal + +import ( + "fmt" + + "github.com/golang/protobuf/proto" + + remotepb "google.golang.org/appengine/internal/remote_api" +) + +// errorCodeMaps is a map of service name to the error code map for the service. +var errorCodeMaps = make(map[string]map[int32]string) + +// RegisterErrorCodeMap is called from API implementations to register their +// error code map. This should only be called from init functions. +func RegisterErrorCodeMap(service string, m map[int32]string) { + errorCodeMaps[service] = m +} + +type timeoutCodeKey struct { + service string + code int32 +} + +// timeoutCodes is the set of service+code pairs that represent timeouts. +var timeoutCodes = make(map[timeoutCodeKey]bool) + +func RegisterTimeoutErrorCode(service string, code int32) { + timeoutCodes[timeoutCodeKey{service, code}] = true +} + +// APIError is the type returned by appengine.Context's Call method +// when an API call fails in an API-specific way. This may be, for instance, +// a taskqueue API call failing with TaskQueueServiceError::UNKNOWN_QUEUE. +type APIError struct { + Service string + Detail string + Code int32 // API-specific error code +} + +func (e *APIError) Error() string { + if e.Code == 0 { + if e.Detail == "" { + return "APIError " + } + return e.Detail + } + s := fmt.Sprintf("API error %d", e.Code) + if m, ok := errorCodeMaps[e.Service]; ok { + s += " (" + e.Service + ": " + m[e.Code] + ")" + } else { + // Shouldn't happen, but provide a bit more detail if it does. + s = e.Service + " " + s + } + if e.Detail != "" { + s += ": " + e.Detail + } + return s +} + +func (e *APIError) IsTimeout() bool { + return timeoutCodes[timeoutCodeKey{e.Service, e.Code}] +} + +// CallError is the type returned by appengine.Context's Call method when an +// API call fails in a generic way, such as RpcError::CAPABILITY_DISABLED. +type CallError struct { + Detail string + Code int32 + // TODO: Remove this if we get a distinguishable error code. + Timeout bool +} + +func (e *CallError) Error() string { + var msg string + switch remotepb.RpcError_ErrorCode(e.Code) { + case remotepb.RpcError_UNKNOWN: + return e.Detail + case remotepb.RpcError_OVER_QUOTA: + msg = "Over quota" + case remotepb.RpcError_CAPABILITY_DISABLED: + msg = "Capability disabled" + case remotepb.RpcError_CANCELLED: + msg = "Canceled" + default: + msg = fmt.Sprintf("Call error %d", e.Code) + } + s := msg + ": " + e.Detail + if e.Timeout { + s += " (timeout)" + } + return s +} + +func (e *CallError) IsTimeout() bool { + return e.Timeout +} + +// NamespaceMods is a map from API service to a function that will mutate an RPC request to attach a namespace. +// The function should be prepared to be called on the same message more than once; it should only modify the +// RPC request the first time. +var NamespaceMods = make(map[string]func(m proto.Message, namespace string)) diff --git a/vendor/google.golang.org/appengine/internal/log/log_service.pb.go b/vendor/google.golang.org/appengine/internal/log/log_service.pb.go new file mode 100644 index 00000000000..20c595be30a --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/log/log_service.pb.go @@ -0,0 +1,899 @@ +// Code generated by protoc-gen-go. +// source: google.golang.org/appengine/internal/log/log_service.proto +// DO NOT EDIT! + +/* +Package log is a generated protocol buffer package. + +It is generated from these files: + google.golang.org/appengine/internal/log/log_service.proto + +It has these top-level messages: + LogServiceError + UserAppLogLine + UserAppLogGroup + FlushRequest + SetStatusRequest + LogOffset + LogLine + RequestLog + LogModuleVersion + LogReadRequest + LogReadResponse + LogUsageRecord + LogUsageRequest + LogUsageResponse +*/ +package log + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type LogServiceError_ErrorCode int32 + +const ( + LogServiceError_OK LogServiceError_ErrorCode = 0 + LogServiceError_INVALID_REQUEST LogServiceError_ErrorCode = 1 + LogServiceError_STORAGE_ERROR LogServiceError_ErrorCode = 2 +) + +var LogServiceError_ErrorCode_name = map[int32]string{ + 0: "OK", + 1: "INVALID_REQUEST", + 2: "STORAGE_ERROR", +} +var LogServiceError_ErrorCode_value = map[string]int32{ + "OK": 0, + "INVALID_REQUEST": 1, + "STORAGE_ERROR": 2, +} + +func (x LogServiceError_ErrorCode) Enum() *LogServiceError_ErrorCode { + p := new(LogServiceError_ErrorCode) + *p = x + return p +} +func (x LogServiceError_ErrorCode) String() string { + return proto.EnumName(LogServiceError_ErrorCode_name, int32(x)) +} +func (x *LogServiceError_ErrorCode) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(LogServiceError_ErrorCode_value, data, "LogServiceError_ErrorCode") + if err != nil { + return err + } + *x = LogServiceError_ErrorCode(value) + return nil +} + +type LogServiceError struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *LogServiceError) Reset() { *m = LogServiceError{} } +func (m *LogServiceError) String() string { return proto.CompactTextString(m) } +func (*LogServiceError) ProtoMessage() {} + +type UserAppLogLine struct { + TimestampUsec *int64 `protobuf:"varint,1,req,name=timestamp_usec" json:"timestamp_usec,omitempty"` + Level *int64 `protobuf:"varint,2,req,name=level" json:"level,omitempty"` + Message *string `protobuf:"bytes,3,req,name=message" json:"message,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *UserAppLogLine) Reset() { *m = UserAppLogLine{} } +func (m *UserAppLogLine) String() string { return proto.CompactTextString(m) } +func (*UserAppLogLine) ProtoMessage() {} + +func (m *UserAppLogLine) GetTimestampUsec() int64 { + if m != nil && m.TimestampUsec != nil { + return *m.TimestampUsec + } + return 0 +} + +func (m *UserAppLogLine) GetLevel() int64 { + if m != nil && m.Level != nil { + return *m.Level + } + return 0 +} + +func (m *UserAppLogLine) GetMessage() string { + if m != nil && m.Message != nil { + return *m.Message + } + return "" +} + +type UserAppLogGroup struct { + LogLine []*UserAppLogLine `protobuf:"bytes,2,rep,name=log_line" json:"log_line,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *UserAppLogGroup) Reset() { *m = UserAppLogGroup{} } +func (m *UserAppLogGroup) String() string { return proto.CompactTextString(m) } +func (*UserAppLogGroup) ProtoMessage() {} + +func (m *UserAppLogGroup) GetLogLine() []*UserAppLogLine { + if m != nil { + return m.LogLine + } + return nil +} + +type FlushRequest struct { + Logs []byte `protobuf:"bytes,1,opt,name=logs" json:"logs,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *FlushRequest) Reset() { *m = FlushRequest{} } +func (m *FlushRequest) String() string { return proto.CompactTextString(m) } +func (*FlushRequest) ProtoMessage() {} + +func (m *FlushRequest) GetLogs() []byte { + if m != nil { + return m.Logs + } + return nil +} + +type SetStatusRequest struct { + Status *string `protobuf:"bytes,1,req,name=status" json:"status,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *SetStatusRequest) Reset() { *m = SetStatusRequest{} } +func (m *SetStatusRequest) String() string { return proto.CompactTextString(m) } +func (*SetStatusRequest) ProtoMessage() {} + +func (m *SetStatusRequest) GetStatus() string { + if m != nil && m.Status != nil { + return *m.Status + } + return "" +} + +type LogOffset struct { + RequestId []byte `protobuf:"bytes,1,opt,name=request_id" json:"request_id,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *LogOffset) Reset() { *m = LogOffset{} } +func (m *LogOffset) String() string { return proto.CompactTextString(m) } +func (*LogOffset) ProtoMessage() {} + +func (m *LogOffset) GetRequestId() []byte { + if m != nil { + return m.RequestId + } + return nil +} + +type LogLine struct { + Time *int64 `protobuf:"varint,1,req,name=time" json:"time,omitempty"` + Level *int32 `protobuf:"varint,2,req,name=level" json:"level,omitempty"` + LogMessage *string `protobuf:"bytes,3,req,name=log_message" json:"log_message,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *LogLine) Reset() { *m = LogLine{} } +func (m *LogLine) String() string { return proto.CompactTextString(m) } +func (*LogLine) ProtoMessage() {} + +func (m *LogLine) GetTime() int64 { + if m != nil && m.Time != nil { + return *m.Time + } + return 0 +} + +func (m *LogLine) GetLevel() int32 { + if m != nil && m.Level != nil { + return *m.Level + } + return 0 +} + +func (m *LogLine) GetLogMessage() string { + if m != nil && m.LogMessage != nil { + return *m.LogMessage + } + return "" +} + +type RequestLog struct { + AppId *string `protobuf:"bytes,1,req,name=app_id" json:"app_id,omitempty"` + ModuleId *string `protobuf:"bytes,37,opt,name=module_id,def=default" json:"module_id,omitempty"` + VersionId *string `protobuf:"bytes,2,req,name=version_id" json:"version_id,omitempty"` + RequestId []byte `protobuf:"bytes,3,req,name=request_id" json:"request_id,omitempty"` + Offset *LogOffset `protobuf:"bytes,35,opt,name=offset" json:"offset,omitempty"` + Ip *string `protobuf:"bytes,4,req,name=ip" json:"ip,omitempty"` + Nickname *string `protobuf:"bytes,5,opt,name=nickname" json:"nickname,omitempty"` + StartTime *int64 `protobuf:"varint,6,req,name=start_time" json:"start_time,omitempty"` + EndTime *int64 `protobuf:"varint,7,req,name=end_time" json:"end_time,omitempty"` + Latency *int64 `protobuf:"varint,8,req,name=latency" json:"latency,omitempty"` + Mcycles *int64 `protobuf:"varint,9,req,name=mcycles" json:"mcycles,omitempty"` + Method *string `protobuf:"bytes,10,req,name=method" json:"method,omitempty"` + Resource *string `protobuf:"bytes,11,req,name=resource" json:"resource,omitempty"` + HttpVersion *string `protobuf:"bytes,12,req,name=http_version" json:"http_version,omitempty"` + Status *int32 `protobuf:"varint,13,req,name=status" json:"status,omitempty"` + ResponseSize *int64 `protobuf:"varint,14,req,name=response_size" json:"response_size,omitempty"` + Referrer *string `protobuf:"bytes,15,opt,name=referrer" json:"referrer,omitempty"` + UserAgent *string `protobuf:"bytes,16,opt,name=user_agent" json:"user_agent,omitempty"` + UrlMapEntry *string `protobuf:"bytes,17,req,name=url_map_entry" json:"url_map_entry,omitempty"` + Combined *string `protobuf:"bytes,18,req,name=combined" json:"combined,omitempty"` + ApiMcycles *int64 `protobuf:"varint,19,opt,name=api_mcycles" json:"api_mcycles,omitempty"` + Host *string `protobuf:"bytes,20,opt,name=host" json:"host,omitempty"` + Cost *float64 `protobuf:"fixed64,21,opt,name=cost" json:"cost,omitempty"` + TaskQueueName *string `protobuf:"bytes,22,opt,name=task_queue_name" json:"task_queue_name,omitempty"` + TaskName *string `protobuf:"bytes,23,opt,name=task_name" json:"task_name,omitempty"` + WasLoadingRequest *bool `protobuf:"varint,24,opt,name=was_loading_request" json:"was_loading_request,omitempty"` + PendingTime *int64 `protobuf:"varint,25,opt,name=pending_time" json:"pending_time,omitempty"` + ReplicaIndex *int32 `protobuf:"varint,26,opt,name=replica_index,def=-1" json:"replica_index,omitempty"` + Finished *bool `protobuf:"varint,27,opt,name=finished,def=1" json:"finished,omitempty"` + CloneKey []byte `protobuf:"bytes,28,opt,name=clone_key" json:"clone_key,omitempty"` + Line []*LogLine `protobuf:"bytes,29,rep,name=line" json:"line,omitempty"` + LinesIncomplete *bool `protobuf:"varint,36,opt,name=lines_incomplete" json:"lines_incomplete,omitempty"` + AppEngineRelease []byte `protobuf:"bytes,38,opt,name=app_engine_release" json:"app_engine_release,omitempty"` + ExitReason *int32 `protobuf:"varint,30,opt,name=exit_reason" json:"exit_reason,omitempty"` + WasThrottledForTime *bool `protobuf:"varint,31,opt,name=was_throttled_for_time" json:"was_throttled_for_time,omitempty"` + WasThrottledForRequests *bool `protobuf:"varint,32,opt,name=was_throttled_for_requests" json:"was_throttled_for_requests,omitempty"` + ThrottledTime *int64 `protobuf:"varint,33,opt,name=throttled_time" json:"throttled_time,omitempty"` + ServerName []byte `protobuf:"bytes,34,opt,name=server_name" json:"server_name,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *RequestLog) Reset() { *m = RequestLog{} } +func (m *RequestLog) String() string { return proto.CompactTextString(m) } +func (*RequestLog) ProtoMessage() {} + +const Default_RequestLog_ModuleId string = "default" +const Default_RequestLog_ReplicaIndex int32 = -1 +const Default_RequestLog_Finished bool = true + +func (m *RequestLog) GetAppId() string { + if m != nil && m.AppId != nil { + return *m.AppId + } + return "" +} + +func (m *RequestLog) GetModuleId() string { + if m != nil && m.ModuleId != nil { + return *m.ModuleId + } + return Default_RequestLog_ModuleId +} + +func (m *RequestLog) GetVersionId() string { + if m != nil && m.VersionId != nil { + return *m.VersionId + } + return "" +} + +func (m *RequestLog) GetRequestId() []byte { + if m != nil { + return m.RequestId + } + return nil +} + +func (m *RequestLog) GetOffset() *LogOffset { + if m != nil { + return m.Offset + } + return nil +} + +func (m *RequestLog) GetIp() string { + if m != nil && m.Ip != nil { + return *m.Ip + } + return "" +} + +func (m *RequestLog) GetNickname() string { + if m != nil && m.Nickname != nil { + return *m.Nickname + } + return "" +} + +func (m *RequestLog) GetStartTime() int64 { + if m != nil && m.StartTime != nil { + return *m.StartTime + } + return 0 +} + +func (m *RequestLog) GetEndTime() int64 { + if m != nil && m.EndTime != nil { + return *m.EndTime + } + return 0 +} + +func (m *RequestLog) GetLatency() int64 { + if m != nil && m.Latency != nil { + return *m.Latency + } + return 0 +} + +func (m *RequestLog) GetMcycles() int64 { + if m != nil && m.Mcycles != nil { + return *m.Mcycles + } + return 0 +} + +func (m *RequestLog) GetMethod() string { + if m != nil && m.Method != nil { + return *m.Method + } + return "" +} + +func (m *RequestLog) GetResource() string { + if m != nil && m.Resource != nil { + return *m.Resource + } + return "" +} + +func (m *RequestLog) GetHttpVersion() string { + if m != nil && m.HttpVersion != nil { + return *m.HttpVersion + } + return "" +} + +func (m *RequestLog) GetStatus() int32 { + if m != nil && m.Status != nil { + return *m.Status + } + return 0 +} + +func (m *RequestLog) GetResponseSize() int64 { + if m != nil && m.ResponseSize != nil { + return *m.ResponseSize + } + return 0 +} + +func (m *RequestLog) GetReferrer() string { + if m != nil && m.Referrer != nil { + return *m.Referrer + } + return "" +} + +func (m *RequestLog) GetUserAgent() string { + if m != nil && m.UserAgent != nil { + return *m.UserAgent + } + return "" +} + +func (m *RequestLog) GetUrlMapEntry() string { + if m != nil && m.UrlMapEntry != nil { + return *m.UrlMapEntry + } + return "" +} + +func (m *RequestLog) GetCombined() string { + if m != nil && m.Combined != nil { + return *m.Combined + } + return "" +} + +func (m *RequestLog) GetApiMcycles() int64 { + if m != nil && m.ApiMcycles != nil { + return *m.ApiMcycles + } + return 0 +} + +func (m *RequestLog) GetHost() string { + if m != nil && m.Host != nil { + return *m.Host + } + return "" +} + +func (m *RequestLog) GetCost() float64 { + if m != nil && m.Cost != nil { + return *m.Cost + } + return 0 +} + +func (m *RequestLog) GetTaskQueueName() string { + if m != nil && m.TaskQueueName != nil { + return *m.TaskQueueName + } + return "" +} + +func (m *RequestLog) GetTaskName() string { + if m != nil && m.TaskName != nil { + return *m.TaskName + } + return "" +} + +func (m *RequestLog) GetWasLoadingRequest() bool { + if m != nil && m.WasLoadingRequest != nil { + return *m.WasLoadingRequest + } + return false +} + +func (m *RequestLog) GetPendingTime() int64 { + if m != nil && m.PendingTime != nil { + return *m.PendingTime + } + return 0 +} + +func (m *RequestLog) GetReplicaIndex() int32 { + if m != nil && m.ReplicaIndex != nil { + return *m.ReplicaIndex + } + return Default_RequestLog_ReplicaIndex +} + +func (m *RequestLog) GetFinished() bool { + if m != nil && m.Finished != nil { + return *m.Finished + } + return Default_RequestLog_Finished +} + +func (m *RequestLog) GetCloneKey() []byte { + if m != nil { + return m.CloneKey + } + return nil +} + +func (m *RequestLog) GetLine() []*LogLine { + if m != nil { + return m.Line + } + return nil +} + +func (m *RequestLog) GetLinesIncomplete() bool { + if m != nil && m.LinesIncomplete != nil { + return *m.LinesIncomplete + } + return false +} + +func (m *RequestLog) GetAppEngineRelease() []byte { + if m != nil { + return m.AppEngineRelease + } + return nil +} + +func (m *RequestLog) GetExitReason() int32 { + if m != nil && m.ExitReason != nil { + return *m.ExitReason + } + return 0 +} + +func (m *RequestLog) GetWasThrottledForTime() bool { + if m != nil && m.WasThrottledForTime != nil { + return *m.WasThrottledForTime + } + return false +} + +func (m *RequestLog) GetWasThrottledForRequests() bool { + if m != nil && m.WasThrottledForRequests != nil { + return *m.WasThrottledForRequests + } + return false +} + +func (m *RequestLog) GetThrottledTime() int64 { + if m != nil && m.ThrottledTime != nil { + return *m.ThrottledTime + } + return 0 +} + +func (m *RequestLog) GetServerName() []byte { + if m != nil { + return m.ServerName + } + return nil +} + +type LogModuleVersion struct { + ModuleId *string `protobuf:"bytes,1,opt,name=module_id,def=default" json:"module_id,omitempty"` + VersionId *string `protobuf:"bytes,2,opt,name=version_id" json:"version_id,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *LogModuleVersion) Reset() { *m = LogModuleVersion{} } +func (m *LogModuleVersion) String() string { return proto.CompactTextString(m) } +func (*LogModuleVersion) ProtoMessage() {} + +const Default_LogModuleVersion_ModuleId string = "default" + +func (m *LogModuleVersion) GetModuleId() string { + if m != nil && m.ModuleId != nil { + return *m.ModuleId + } + return Default_LogModuleVersion_ModuleId +} + +func (m *LogModuleVersion) GetVersionId() string { + if m != nil && m.VersionId != nil { + return *m.VersionId + } + return "" +} + +type LogReadRequest struct { + AppId *string `protobuf:"bytes,1,req,name=app_id" json:"app_id,omitempty"` + VersionId []string `protobuf:"bytes,2,rep,name=version_id" json:"version_id,omitempty"` + ModuleVersion []*LogModuleVersion `protobuf:"bytes,19,rep,name=module_version" json:"module_version,omitempty"` + StartTime *int64 `protobuf:"varint,3,opt,name=start_time" json:"start_time,omitempty"` + EndTime *int64 `protobuf:"varint,4,opt,name=end_time" json:"end_time,omitempty"` + Offset *LogOffset `protobuf:"bytes,5,opt,name=offset" json:"offset,omitempty"` + RequestId [][]byte `protobuf:"bytes,6,rep,name=request_id" json:"request_id,omitempty"` + MinimumLogLevel *int32 `protobuf:"varint,7,opt,name=minimum_log_level" json:"minimum_log_level,omitempty"` + IncludeIncomplete *bool `protobuf:"varint,8,opt,name=include_incomplete" json:"include_incomplete,omitempty"` + Count *int64 `protobuf:"varint,9,opt,name=count" json:"count,omitempty"` + CombinedLogRegex *string `protobuf:"bytes,14,opt,name=combined_log_regex" json:"combined_log_regex,omitempty"` + HostRegex *string `protobuf:"bytes,15,opt,name=host_regex" json:"host_regex,omitempty"` + ReplicaIndex *int32 `protobuf:"varint,16,opt,name=replica_index" json:"replica_index,omitempty"` + IncludeAppLogs *bool `protobuf:"varint,10,opt,name=include_app_logs" json:"include_app_logs,omitempty"` + AppLogsPerRequest *int32 `protobuf:"varint,17,opt,name=app_logs_per_request" json:"app_logs_per_request,omitempty"` + IncludeHost *bool `protobuf:"varint,11,opt,name=include_host" json:"include_host,omitempty"` + IncludeAll *bool `protobuf:"varint,12,opt,name=include_all" json:"include_all,omitempty"` + CacheIterator *bool `protobuf:"varint,13,opt,name=cache_iterator" json:"cache_iterator,omitempty"` + NumShards *int32 `protobuf:"varint,18,opt,name=num_shards" json:"num_shards,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *LogReadRequest) Reset() { *m = LogReadRequest{} } +func (m *LogReadRequest) String() string { return proto.CompactTextString(m) } +func (*LogReadRequest) ProtoMessage() {} + +func (m *LogReadRequest) GetAppId() string { + if m != nil && m.AppId != nil { + return *m.AppId + } + return "" +} + +func (m *LogReadRequest) GetVersionId() []string { + if m != nil { + return m.VersionId + } + return nil +} + +func (m *LogReadRequest) GetModuleVersion() []*LogModuleVersion { + if m != nil { + return m.ModuleVersion + } + return nil +} + +func (m *LogReadRequest) GetStartTime() int64 { + if m != nil && m.StartTime != nil { + return *m.StartTime + } + return 0 +} + +func (m *LogReadRequest) GetEndTime() int64 { + if m != nil && m.EndTime != nil { + return *m.EndTime + } + return 0 +} + +func (m *LogReadRequest) GetOffset() *LogOffset { + if m != nil { + return m.Offset + } + return nil +} + +func (m *LogReadRequest) GetRequestId() [][]byte { + if m != nil { + return m.RequestId + } + return nil +} + +func (m *LogReadRequest) GetMinimumLogLevel() int32 { + if m != nil && m.MinimumLogLevel != nil { + return *m.MinimumLogLevel + } + return 0 +} + +func (m *LogReadRequest) GetIncludeIncomplete() bool { + if m != nil && m.IncludeIncomplete != nil { + return *m.IncludeIncomplete + } + return false +} + +func (m *LogReadRequest) GetCount() int64 { + if m != nil && m.Count != nil { + return *m.Count + } + return 0 +} + +func (m *LogReadRequest) GetCombinedLogRegex() string { + if m != nil && m.CombinedLogRegex != nil { + return *m.CombinedLogRegex + } + return "" +} + +func (m *LogReadRequest) GetHostRegex() string { + if m != nil && m.HostRegex != nil { + return *m.HostRegex + } + return "" +} + +func (m *LogReadRequest) GetReplicaIndex() int32 { + if m != nil && m.ReplicaIndex != nil { + return *m.ReplicaIndex + } + return 0 +} + +func (m *LogReadRequest) GetIncludeAppLogs() bool { + if m != nil && m.IncludeAppLogs != nil { + return *m.IncludeAppLogs + } + return false +} + +func (m *LogReadRequest) GetAppLogsPerRequest() int32 { + if m != nil && m.AppLogsPerRequest != nil { + return *m.AppLogsPerRequest + } + return 0 +} + +func (m *LogReadRequest) GetIncludeHost() bool { + if m != nil && m.IncludeHost != nil { + return *m.IncludeHost + } + return false +} + +func (m *LogReadRequest) GetIncludeAll() bool { + if m != nil && m.IncludeAll != nil { + return *m.IncludeAll + } + return false +} + +func (m *LogReadRequest) GetCacheIterator() bool { + if m != nil && m.CacheIterator != nil { + return *m.CacheIterator + } + return false +} + +func (m *LogReadRequest) GetNumShards() int32 { + if m != nil && m.NumShards != nil { + return *m.NumShards + } + return 0 +} + +type LogReadResponse struct { + Log []*RequestLog `protobuf:"bytes,1,rep,name=log" json:"log,omitempty"` + Offset *LogOffset `protobuf:"bytes,2,opt,name=offset" json:"offset,omitempty"` + LastEndTime *int64 `protobuf:"varint,3,opt,name=last_end_time" json:"last_end_time,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *LogReadResponse) Reset() { *m = LogReadResponse{} } +func (m *LogReadResponse) String() string { return proto.CompactTextString(m) } +func (*LogReadResponse) ProtoMessage() {} + +func (m *LogReadResponse) GetLog() []*RequestLog { + if m != nil { + return m.Log + } + return nil +} + +func (m *LogReadResponse) GetOffset() *LogOffset { + if m != nil { + return m.Offset + } + return nil +} + +func (m *LogReadResponse) GetLastEndTime() int64 { + if m != nil && m.LastEndTime != nil { + return *m.LastEndTime + } + return 0 +} + +type LogUsageRecord struct { + VersionId *string `protobuf:"bytes,1,opt,name=version_id" json:"version_id,omitempty"` + StartTime *int32 `protobuf:"varint,2,opt,name=start_time" json:"start_time,omitempty"` + EndTime *int32 `protobuf:"varint,3,opt,name=end_time" json:"end_time,omitempty"` + Count *int64 `protobuf:"varint,4,opt,name=count" json:"count,omitempty"` + TotalSize *int64 `protobuf:"varint,5,opt,name=total_size" json:"total_size,omitempty"` + Records *int32 `protobuf:"varint,6,opt,name=records" json:"records,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *LogUsageRecord) Reset() { *m = LogUsageRecord{} } +func (m *LogUsageRecord) String() string { return proto.CompactTextString(m) } +func (*LogUsageRecord) ProtoMessage() {} + +func (m *LogUsageRecord) GetVersionId() string { + if m != nil && m.VersionId != nil { + return *m.VersionId + } + return "" +} + +func (m *LogUsageRecord) GetStartTime() int32 { + if m != nil && m.StartTime != nil { + return *m.StartTime + } + return 0 +} + +func (m *LogUsageRecord) GetEndTime() int32 { + if m != nil && m.EndTime != nil { + return *m.EndTime + } + return 0 +} + +func (m *LogUsageRecord) GetCount() int64 { + if m != nil && m.Count != nil { + return *m.Count + } + return 0 +} + +func (m *LogUsageRecord) GetTotalSize() int64 { + if m != nil && m.TotalSize != nil { + return *m.TotalSize + } + return 0 +} + +func (m *LogUsageRecord) GetRecords() int32 { + if m != nil && m.Records != nil { + return *m.Records + } + return 0 +} + +type LogUsageRequest struct { + AppId *string `protobuf:"bytes,1,req,name=app_id" json:"app_id,omitempty"` + VersionId []string `protobuf:"bytes,2,rep,name=version_id" json:"version_id,omitempty"` + StartTime *int32 `protobuf:"varint,3,opt,name=start_time" json:"start_time,omitempty"` + EndTime *int32 `protobuf:"varint,4,opt,name=end_time" json:"end_time,omitempty"` + ResolutionHours *uint32 `protobuf:"varint,5,opt,name=resolution_hours,def=1" json:"resolution_hours,omitempty"` + CombineVersions *bool `protobuf:"varint,6,opt,name=combine_versions" json:"combine_versions,omitempty"` + UsageVersion *int32 `protobuf:"varint,7,opt,name=usage_version" json:"usage_version,omitempty"` + VersionsOnly *bool `protobuf:"varint,8,opt,name=versions_only" json:"versions_only,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *LogUsageRequest) Reset() { *m = LogUsageRequest{} } +func (m *LogUsageRequest) String() string { return proto.CompactTextString(m) } +func (*LogUsageRequest) ProtoMessage() {} + +const Default_LogUsageRequest_ResolutionHours uint32 = 1 + +func (m *LogUsageRequest) GetAppId() string { + if m != nil && m.AppId != nil { + return *m.AppId + } + return "" +} + +func (m *LogUsageRequest) GetVersionId() []string { + if m != nil { + return m.VersionId + } + return nil +} + +func (m *LogUsageRequest) GetStartTime() int32 { + if m != nil && m.StartTime != nil { + return *m.StartTime + } + return 0 +} + +func (m *LogUsageRequest) GetEndTime() int32 { + if m != nil && m.EndTime != nil { + return *m.EndTime + } + return 0 +} + +func (m *LogUsageRequest) GetResolutionHours() uint32 { + if m != nil && m.ResolutionHours != nil { + return *m.ResolutionHours + } + return Default_LogUsageRequest_ResolutionHours +} + +func (m *LogUsageRequest) GetCombineVersions() bool { + if m != nil && m.CombineVersions != nil { + return *m.CombineVersions + } + return false +} + +func (m *LogUsageRequest) GetUsageVersion() int32 { + if m != nil && m.UsageVersion != nil { + return *m.UsageVersion + } + return 0 +} + +func (m *LogUsageRequest) GetVersionsOnly() bool { + if m != nil && m.VersionsOnly != nil { + return *m.VersionsOnly + } + return false +} + +type LogUsageResponse struct { + Usage []*LogUsageRecord `protobuf:"bytes,1,rep,name=usage" json:"usage,omitempty"` + Summary *LogUsageRecord `protobuf:"bytes,2,opt,name=summary" json:"summary,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *LogUsageResponse) Reset() { *m = LogUsageResponse{} } +func (m *LogUsageResponse) String() string { return proto.CompactTextString(m) } +func (*LogUsageResponse) ProtoMessage() {} + +func (m *LogUsageResponse) GetUsage() []*LogUsageRecord { + if m != nil { + return m.Usage + } + return nil +} + +func (m *LogUsageResponse) GetSummary() *LogUsageRecord { + if m != nil { + return m.Summary + } + return nil +} + +func init() { +} diff --git a/vendor/google.golang.org/appengine/internal/main.go b/vendor/google.golang.org/appengine/internal/main.go new file mode 100644 index 00000000000..49036163c2b --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/main.go @@ -0,0 +1,15 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// +build appengine + +package internal + +import ( + "appengine_internal" +) + +func Main() { + appengine_internal.Main() +} diff --git a/vendor/google.golang.org/appengine/internal/main_vm.go b/vendor/google.golang.org/appengine/internal/main_vm.go new file mode 100644 index 00000000000..57331ad171c --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/main_vm.go @@ -0,0 +1,44 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// +build !appengine + +package internal + +import ( + "io" + "log" + "net/http" + "net/url" + "os" +) + +func Main() { + installHealthChecker(http.DefaultServeMux) + + port := "8080" + if s := os.Getenv("PORT"); s != "" { + port = s + } + + if err := http.ListenAndServe(":"+port, http.HandlerFunc(handleHTTP)); err != nil { + log.Fatalf("http.ListenAndServe: %v", err) + } +} + +func installHealthChecker(mux *http.ServeMux) { + // If no health check handler has been installed by this point, add a trivial one. + const healthPath = "/_ah/health" + hreq := &http.Request{ + Method: "GET", + URL: &url.URL{ + Path: healthPath, + }, + } + if _, pat := mux.Handler(hreq); pat != healthPath { + mux.HandleFunc(healthPath, func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "ok") + }) + } +} diff --git a/vendor/google.golang.org/appengine/internal/metadata.go b/vendor/google.golang.org/appengine/internal/metadata.go new file mode 100644 index 00000000000..9cc1f71d104 --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/metadata.go @@ -0,0 +1,61 @@ +// Copyright 2014 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package internal + +// This file has code for accessing metadata. +// +// References: +// https://cloud.google.com/compute/docs/metadata + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" +) + +const ( + metadataHost = "metadata" + metadataPath = "/computeMetadata/v1/" +) + +var ( + metadataRequestHeaders = http.Header{ + "Metadata-Flavor": []string{"Google"}, + } +) + +// TODO(dsymonds): Do we need to support default values, like Python? +func mustGetMetadata(key string) []byte { + b, err := getMetadata(key) + if err != nil { + log.Fatalf("Metadata fetch failed: %v", err) + } + return b +} + +func getMetadata(key string) ([]byte, error) { + // TODO(dsymonds): May need to use url.Parse to support keys with query args. + req := &http.Request{ + Method: "GET", + URL: &url.URL{ + Scheme: "http", + Host: metadataHost, + Path: metadataPath + key, + }, + Header: metadataRequestHeaders, + Host: metadataHost, + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, fmt.Errorf("metadata server returned HTTP %d", resp.StatusCode) + } + return ioutil.ReadAll(resp.Body) +} diff --git a/vendor/google.golang.org/appengine/internal/modules/modules_service.pb.go b/vendor/google.golang.org/appengine/internal/modules/modules_service.pb.go new file mode 100644 index 00000000000..a0145ed317c --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/modules/modules_service.pb.go @@ -0,0 +1,375 @@ +// Code generated by protoc-gen-go. +// source: google.golang.org/appengine/internal/modules/modules_service.proto +// DO NOT EDIT! + +/* +Package modules is a generated protocol buffer package. + +It is generated from these files: + google.golang.org/appengine/internal/modules/modules_service.proto + +It has these top-level messages: + ModulesServiceError + GetModulesRequest + GetModulesResponse + GetVersionsRequest + GetVersionsResponse + GetDefaultVersionRequest + GetDefaultVersionResponse + GetNumInstancesRequest + GetNumInstancesResponse + SetNumInstancesRequest + SetNumInstancesResponse + StartModuleRequest + StartModuleResponse + StopModuleRequest + StopModuleResponse + GetHostnameRequest + GetHostnameResponse +*/ +package modules + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type ModulesServiceError_ErrorCode int32 + +const ( + ModulesServiceError_OK ModulesServiceError_ErrorCode = 0 + ModulesServiceError_INVALID_MODULE ModulesServiceError_ErrorCode = 1 + ModulesServiceError_INVALID_VERSION ModulesServiceError_ErrorCode = 2 + ModulesServiceError_INVALID_INSTANCES ModulesServiceError_ErrorCode = 3 + ModulesServiceError_TRANSIENT_ERROR ModulesServiceError_ErrorCode = 4 + ModulesServiceError_UNEXPECTED_STATE ModulesServiceError_ErrorCode = 5 +) + +var ModulesServiceError_ErrorCode_name = map[int32]string{ + 0: "OK", + 1: "INVALID_MODULE", + 2: "INVALID_VERSION", + 3: "INVALID_INSTANCES", + 4: "TRANSIENT_ERROR", + 5: "UNEXPECTED_STATE", +} +var ModulesServiceError_ErrorCode_value = map[string]int32{ + "OK": 0, + "INVALID_MODULE": 1, + "INVALID_VERSION": 2, + "INVALID_INSTANCES": 3, + "TRANSIENT_ERROR": 4, + "UNEXPECTED_STATE": 5, +} + +func (x ModulesServiceError_ErrorCode) Enum() *ModulesServiceError_ErrorCode { + p := new(ModulesServiceError_ErrorCode) + *p = x + return p +} +func (x ModulesServiceError_ErrorCode) String() string { + return proto.EnumName(ModulesServiceError_ErrorCode_name, int32(x)) +} +func (x *ModulesServiceError_ErrorCode) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(ModulesServiceError_ErrorCode_value, data, "ModulesServiceError_ErrorCode") + if err != nil { + return err + } + *x = ModulesServiceError_ErrorCode(value) + return nil +} + +type ModulesServiceError struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *ModulesServiceError) Reset() { *m = ModulesServiceError{} } +func (m *ModulesServiceError) String() string { return proto.CompactTextString(m) } +func (*ModulesServiceError) ProtoMessage() {} + +type GetModulesRequest struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetModulesRequest) Reset() { *m = GetModulesRequest{} } +func (m *GetModulesRequest) String() string { return proto.CompactTextString(m) } +func (*GetModulesRequest) ProtoMessage() {} + +type GetModulesResponse struct { + Module []string `protobuf:"bytes,1,rep,name=module" json:"module,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetModulesResponse) Reset() { *m = GetModulesResponse{} } +func (m *GetModulesResponse) String() string { return proto.CompactTextString(m) } +func (*GetModulesResponse) ProtoMessage() {} + +func (m *GetModulesResponse) GetModule() []string { + if m != nil { + return m.Module + } + return nil +} + +type GetVersionsRequest struct { + Module *string `protobuf:"bytes,1,opt,name=module" json:"module,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetVersionsRequest) Reset() { *m = GetVersionsRequest{} } +func (m *GetVersionsRequest) String() string { return proto.CompactTextString(m) } +func (*GetVersionsRequest) ProtoMessage() {} + +func (m *GetVersionsRequest) GetModule() string { + if m != nil && m.Module != nil { + return *m.Module + } + return "" +} + +type GetVersionsResponse struct { + Version []string `protobuf:"bytes,1,rep,name=version" json:"version,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetVersionsResponse) Reset() { *m = GetVersionsResponse{} } +func (m *GetVersionsResponse) String() string { return proto.CompactTextString(m) } +func (*GetVersionsResponse) ProtoMessage() {} + +func (m *GetVersionsResponse) GetVersion() []string { + if m != nil { + return m.Version + } + return nil +} + +type GetDefaultVersionRequest struct { + Module *string `protobuf:"bytes,1,opt,name=module" json:"module,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetDefaultVersionRequest) Reset() { *m = GetDefaultVersionRequest{} } +func (m *GetDefaultVersionRequest) String() string { return proto.CompactTextString(m) } +func (*GetDefaultVersionRequest) ProtoMessage() {} + +func (m *GetDefaultVersionRequest) GetModule() string { + if m != nil && m.Module != nil { + return *m.Module + } + return "" +} + +type GetDefaultVersionResponse struct { + Version *string `protobuf:"bytes,1,req,name=version" json:"version,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetDefaultVersionResponse) Reset() { *m = GetDefaultVersionResponse{} } +func (m *GetDefaultVersionResponse) String() string { return proto.CompactTextString(m) } +func (*GetDefaultVersionResponse) ProtoMessage() {} + +func (m *GetDefaultVersionResponse) GetVersion() string { + if m != nil && m.Version != nil { + return *m.Version + } + return "" +} + +type GetNumInstancesRequest struct { + Module *string `protobuf:"bytes,1,opt,name=module" json:"module,omitempty"` + Version *string `protobuf:"bytes,2,opt,name=version" json:"version,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetNumInstancesRequest) Reset() { *m = GetNumInstancesRequest{} } +func (m *GetNumInstancesRequest) String() string { return proto.CompactTextString(m) } +func (*GetNumInstancesRequest) ProtoMessage() {} + +func (m *GetNumInstancesRequest) GetModule() string { + if m != nil && m.Module != nil { + return *m.Module + } + return "" +} + +func (m *GetNumInstancesRequest) GetVersion() string { + if m != nil && m.Version != nil { + return *m.Version + } + return "" +} + +type GetNumInstancesResponse struct { + Instances *int64 `protobuf:"varint,1,req,name=instances" json:"instances,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetNumInstancesResponse) Reset() { *m = GetNumInstancesResponse{} } +func (m *GetNumInstancesResponse) String() string { return proto.CompactTextString(m) } +func (*GetNumInstancesResponse) ProtoMessage() {} + +func (m *GetNumInstancesResponse) GetInstances() int64 { + if m != nil && m.Instances != nil { + return *m.Instances + } + return 0 +} + +type SetNumInstancesRequest struct { + Module *string `protobuf:"bytes,1,opt,name=module" json:"module,omitempty"` + Version *string `protobuf:"bytes,2,opt,name=version" json:"version,omitempty"` + Instances *int64 `protobuf:"varint,3,req,name=instances" json:"instances,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *SetNumInstancesRequest) Reset() { *m = SetNumInstancesRequest{} } +func (m *SetNumInstancesRequest) String() string { return proto.CompactTextString(m) } +func (*SetNumInstancesRequest) ProtoMessage() {} + +func (m *SetNumInstancesRequest) GetModule() string { + if m != nil && m.Module != nil { + return *m.Module + } + return "" +} + +func (m *SetNumInstancesRequest) GetVersion() string { + if m != nil && m.Version != nil { + return *m.Version + } + return "" +} + +func (m *SetNumInstancesRequest) GetInstances() int64 { + if m != nil && m.Instances != nil { + return *m.Instances + } + return 0 +} + +type SetNumInstancesResponse struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *SetNumInstancesResponse) Reset() { *m = SetNumInstancesResponse{} } +func (m *SetNumInstancesResponse) String() string { return proto.CompactTextString(m) } +func (*SetNumInstancesResponse) ProtoMessage() {} + +type StartModuleRequest struct { + Module *string `protobuf:"bytes,1,req,name=module" json:"module,omitempty"` + Version *string `protobuf:"bytes,2,req,name=version" json:"version,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *StartModuleRequest) Reset() { *m = StartModuleRequest{} } +func (m *StartModuleRequest) String() string { return proto.CompactTextString(m) } +func (*StartModuleRequest) ProtoMessage() {} + +func (m *StartModuleRequest) GetModule() string { + if m != nil && m.Module != nil { + return *m.Module + } + return "" +} + +func (m *StartModuleRequest) GetVersion() string { + if m != nil && m.Version != nil { + return *m.Version + } + return "" +} + +type StartModuleResponse struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *StartModuleResponse) Reset() { *m = StartModuleResponse{} } +func (m *StartModuleResponse) String() string { return proto.CompactTextString(m) } +func (*StartModuleResponse) ProtoMessage() {} + +type StopModuleRequest struct { + Module *string `protobuf:"bytes,1,opt,name=module" json:"module,omitempty"` + Version *string `protobuf:"bytes,2,opt,name=version" json:"version,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *StopModuleRequest) Reset() { *m = StopModuleRequest{} } +func (m *StopModuleRequest) String() string { return proto.CompactTextString(m) } +func (*StopModuleRequest) ProtoMessage() {} + +func (m *StopModuleRequest) GetModule() string { + if m != nil && m.Module != nil { + return *m.Module + } + return "" +} + +func (m *StopModuleRequest) GetVersion() string { + if m != nil && m.Version != nil { + return *m.Version + } + return "" +} + +type StopModuleResponse struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *StopModuleResponse) Reset() { *m = StopModuleResponse{} } +func (m *StopModuleResponse) String() string { return proto.CompactTextString(m) } +func (*StopModuleResponse) ProtoMessage() {} + +type GetHostnameRequest struct { + Module *string `protobuf:"bytes,1,opt,name=module" json:"module,omitempty"` + Version *string `protobuf:"bytes,2,opt,name=version" json:"version,omitempty"` + Instance *string `protobuf:"bytes,3,opt,name=instance" json:"instance,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetHostnameRequest) Reset() { *m = GetHostnameRequest{} } +func (m *GetHostnameRequest) String() string { return proto.CompactTextString(m) } +func (*GetHostnameRequest) ProtoMessage() {} + +func (m *GetHostnameRequest) GetModule() string { + if m != nil && m.Module != nil { + return *m.Module + } + return "" +} + +func (m *GetHostnameRequest) GetVersion() string { + if m != nil && m.Version != nil { + return *m.Version + } + return "" +} + +func (m *GetHostnameRequest) GetInstance() string { + if m != nil && m.Instance != nil { + return *m.Instance + } + return "" +} + +type GetHostnameResponse struct { + Hostname *string `protobuf:"bytes,1,req,name=hostname" json:"hostname,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *GetHostnameResponse) Reset() { *m = GetHostnameResponse{} } +func (m *GetHostnameResponse) String() string { return proto.CompactTextString(m) } +func (*GetHostnameResponse) ProtoMessage() {} + +func (m *GetHostnameResponse) GetHostname() string { + if m != nil && m.Hostname != nil { + return *m.Hostname + } + return "" +} + +func init() { +} diff --git a/vendor/google.golang.org/appengine/internal/net.go b/vendor/google.golang.org/appengine/internal/net.go new file mode 100644 index 00000000000..3b94cf0c6a8 --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/net.go @@ -0,0 +1,56 @@ +// Copyright 2014 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package internal + +// This file implements a network dialer that limits the number of concurrent connections. +// It is only used for API calls. + +import ( + "log" + "net" + "runtime" + "sync" + "time" +) + +var limitSem = make(chan int, 100) // TODO(dsymonds): Use environment variable. + +func limitRelease() { + // non-blocking + select { + case <-limitSem: + default: + // This should not normally happen. + log.Print("appengine: unbalanced limitSem release!") + } +} + +func limitDial(network, addr string) (net.Conn, error) { + limitSem <- 1 + + // Dial with a timeout in case the API host is MIA. + // The connection should normally be very fast. + conn, err := net.DialTimeout(network, addr, 500*time.Millisecond) + if err != nil { + limitRelease() + return nil, err + } + lc := &limitConn{Conn: conn} + runtime.SetFinalizer(lc, (*limitConn).Close) // shouldn't usually be required + return lc, nil +} + +type limitConn struct { + close sync.Once + net.Conn +} + +func (lc *limitConn) Close() error { + defer lc.close.Do(func() { + limitRelease() + runtime.SetFinalizer(lc, nil) + }) + return lc.Conn.Close() +} diff --git a/vendor/google.golang.org/appengine/internal/remote_api/remote_api.pb.go b/vendor/google.golang.org/appengine/internal/remote_api/remote_api.pb.go new file mode 100644 index 00000000000..526bd39e6d1 --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/remote_api/remote_api.pb.go @@ -0,0 +1,231 @@ +// Code generated by protoc-gen-go. +// source: google.golang.org/appengine/internal/remote_api/remote_api.proto +// DO NOT EDIT! + +/* +Package remote_api is a generated protocol buffer package. + +It is generated from these files: + google.golang.org/appengine/internal/remote_api/remote_api.proto + +It has these top-level messages: + Request + ApplicationError + RpcError + Response +*/ +package remote_api + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type RpcError_ErrorCode int32 + +const ( + RpcError_UNKNOWN RpcError_ErrorCode = 0 + RpcError_CALL_NOT_FOUND RpcError_ErrorCode = 1 + RpcError_PARSE_ERROR RpcError_ErrorCode = 2 + RpcError_SECURITY_VIOLATION RpcError_ErrorCode = 3 + RpcError_OVER_QUOTA RpcError_ErrorCode = 4 + RpcError_REQUEST_TOO_LARGE RpcError_ErrorCode = 5 + RpcError_CAPABILITY_DISABLED RpcError_ErrorCode = 6 + RpcError_FEATURE_DISABLED RpcError_ErrorCode = 7 + RpcError_BAD_REQUEST RpcError_ErrorCode = 8 + RpcError_RESPONSE_TOO_LARGE RpcError_ErrorCode = 9 + RpcError_CANCELLED RpcError_ErrorCode = 10 + RpcError_REPLAY_ERROR RpcError_ErrorCode = 11 + RpcError_DEADLINE_EXCEEDED RpcError_ErrorCode = 12 +) + +var RpcError_ErrorCode_name = map[int32]string{ + 0: "UNKNOWN", + 1: "CALL_NOT_FOUND", + 2: "PARSE_ERROR", + 3: "SECURITY_VIOLATION", + 4: "OVER_QUOTA", + 5: "REQUEST_TOO_LARGE", + 6: "CAPABILITY_DISABLED", + 7: "FEATURE_DISABLED", + 8: "BAD_REQUEST", + 9: "RESPONSE_TOO_LARGE", + 10: "CANCELLED", + 11: "REPLAY_ERROR", + 12: "DEADLINE_EXCEEDED", +} +var RpcError_ErrorCode_value = map[string]int32{ + "UNKNOWN": 0, + "CALL_NOT_FOUND": 1, + "PARSE_ERROR": 2, + "SECURITY_VIOLATION": 3, + "OVER_QUOTA": 4, + "REQUEST_TOO_LARGE": 5, + "CAPABILITY_DISABLED": 6, + "FEATURE_DISABLED": 7, + "BAD_REQUEST": 8, + "RESPONSE_TOO_LARGE": 9, + "CANCELLED": 10, + "REPLAY_ERROR": 11, + "DEADLINE_EXCEEDED": 12, +} + +func (x RpcError_ErrorCode) Enum() *RpcError_ErrorCode { + p := new(RpcError_ErrorCode) + *p = x + return p +} +func (x RpcError_ErrorCode) String() string { + return proto.EnumName(RpcError_ErrorCode_name, int32(x)) +} +func (x *RpcError_ErrorCode) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(RpcError_ErrorCode_value, data, "RpcError_ErrorCode") + if err != nil { + return err + } + *x = RpcError_ErrorCode(value) + return nil +} + +type Request struct { + ServiceName *string `protobuf:"bytes,2,req,name=service_name" json:"service_name,omitempty"` + Method *string `protobuf:"bytes,3,req,name=method" json:"method,omitempty"` + Request []byte `protobuf:"bytes,4,req,name=request" json:"request,omitempty"` + RequestId *string `protobuf:"bytes,5,opt,name=request_id" json:"request_id,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Request) Reset() { *m = Request{} } +func (m *Request) String() string { return proto.CompactTextString(m) } +func (*Request) ProtoMessage() {} + +func (m *Request) GetServiceName() string { + if m != nil && m.ServiceName != nil { + return *m.ServiceName + } + return "" +} + +func (m *Request) GetMethod() string { + if m != nil && m.Method != nil { + return *m.Method + } + return "" +} + +func (m *Request) GetRequest() []byte { + if m != nil { + return m.Request + } + return nil +} + +func (m *Request) GetRequestId() string { + if m != nil && m.RequestId != nil { + return *m.RequestId + } + return "" +} + +type ApplicationError struct { + Code *int32 `protobuf:"varint,1,req,name=code" json:"code,omitempty"` + Detail *string `protobuf:"bytes,2,req,name=detail" json:"detail,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *ApplicationError) Reset() { *m = ApplicationError{} } +func (m *ApplicationError) String() string { return proto.CompactTextString(m) } +func (*ApplicationError) ProtoMessage() {} + +func (m *ApplicationError) GetCode() int32 { + if m != nil && m.Code != nil { + return *m.Code + } + return 0 +} + +func (m *ApplicationError) GetDetail() string { + if m != nil && m.Detail != nil { + return *m.Detail + } + return "" +} + +type RpcError struct { + Code *int32 `protobuf:"varint,1,req,name=code" json:"code,omitempty"` + Detail *string `protobuf:"bytes,2,opt,name=detail" json:"detail,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *RpcError) Reset() { *m = RpcError{} } +func (m *RpcError) String() string { return proto.CompactTextString(m) } +func (*RpcError) ProtoMessage() {} + +func (m *RpcError) GetCode() int32 { + if m != nil && m.Code != nil { + return *m.Code + } + return 0 +} + +func (m *RpcError) GetDetail() string { + if m != nil && m.Detail != nil { + return *m.Detail + } + return "" +} + +type Response struct { + Response []byte `protobuf:"bytes,1,opt,name=response" json:"response,omitempty"` + Exception []byte `protobuf:"bytes,2,opt,name=exception" json:"exception,omitempty"` + ApplicationError *ApplicationError `protobuf:"bytes,3,opt,name=application_error" json:"application_error,omitempty"` + JavaException []byte `protobuf:"bytes,4,opt,name=java_exception" json:"java_exception,omitempty"` + RpcError *RpcError `protobuf:"bytes,5,opt,name=rpc_error" json:"rpc_error,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Response) Reset() { *m = Response{} } +func (m *Response) String() string { return proto.CompactTextString(m) } +func (*Response) ProtoMessage() {} + +func (m *Response) GetResponse() []byte { + if m != nil { + return m.Response + } + return nil +} + +func (m *Response) GetException() []byte { + if m != nil { + return m.Exception + } + return nil +} + +func (m *Response) GetApplicationError() *ApplicationError { + if m != nil { + return m.ApplicationError + } + return nil +} + +func (m *Response) GetJavaException() []byte { + if m != nil { + return m.JavaException + } + return nil +} + +func (m *Response) GetRpcError() *RpcError { + if m != nil { + return m.RpcError + } + return nil +} + +func init() { +} diff --git a/vendor/google.golang.org/appengine/internal/transaction.go b/vendor/google.golang.org/appengine/internal/transaction.go new file mode 100644 index 00000000000..28a6d181206 --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/transaction.go @@ -0,0 +1,107 @@ +// Copyright 2014 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package internal + +// This file implements hooks for applying datastore transactions. + +import ( + "errors" + "reflect" + + "github.com/golang/protobuf/proto" + netcontext "golang.org/x/net/context" + + basepb "google.golang.org/appengine/internal/base" + pb "google.golang.org/appengine/internal/datastore" +) + +var transactionSetters = make(map[reflect.Type]reflect.Value) + +// RegisterTransactionSetter registers a function that sets transaction information +// in a protocol buffer message. f should be a function with two arguments, +// the first being a protocol buffer type, and the second being *datastore.Transaction. +func RegisterTransactionSetter(f interface{}) { + v := reflect.ValueOf(f) + transactionSetters[v.Type().In(0)] = v +} + +// applyTransaction applies the transaction t to message pb +// by using the relevant setter passed to RegisterTransactionSetter. +func applyTransaction(pb proto.Message, t *pb.Transaction) { + v := reflect.ValueOf(pb) + if f, ok := transactionSetters[v.Type()]; ok { + f.Call([]reflect.Value{v, reflect.ValueOf(t)}) + } +} + +var transactionKey = "used for *Transaction" + +func transactionFromContext(ctx netcontext.Context) *transaction { + t, _ := ctx.Value(&transactionKey).(*transaction) + return t +} + +func withTransaction(ctx netcontext.Context, t *transaction) netcontext.Context { + return netcontext.WithValue(ctx, &transactionKey, t) +} + +type transaction struct { + transaction pb.Transaction + finished bool +} + +var ErrConcurrentTransaction = errors.New("internal: concurrent transaction") + +func RunTransactionOnce(c netcontext.Context, f func(netcontext.Context) error, xg bool) error { + if transactionFromContext(c) != nil { + return errors.New("nested transactions are not supported") + } + + // Begin the transaction. + t := &transaction{} + req := &pb.BeginTransactionRequest{ + App: proto.String(FullyQualifiedAppID(c)), + } + if xg { + req.AllowMultipleEg = proto.Bool(true) + } + if err := Call(c, "datastore_v3", "BeginTransaction", req, &t.transaction); err != nil { + return err + } + + // Call f, rolling back the transaction if f returns a non-nil error, or panics. + // The panic is not recovered. + defer func() { + if t.finished { + return + } + t.finished = true + // Ignore the error return value, since we are already returning a non-nil + // error (or we're panicking). + Call(c, "datastore_v3", "Rollback", &t.transaction, &basepb.VoidProto{}) + }() + if err := f(withTransaction(c, t)); err != nil { + return err + } + t.finished = true + + // Commit the transaction. + res := &pb.CommitResponse{} + err := Call(c, "datastore_v3", "Commit", &t.transaction, res) + if ae, ok := err.(*APIError); ok { + /* TODO: restore this conditional + if appengine.IsDevAppServer() { + */ + // The Python Dev AppServer raises an ApplicationError with error code 2 (which is + // Error.CONCURRENT_TRANSACTION) and message "Concurrency exception.". + if ae.Code == int32(pb.Error_BAD_REQUEST) && ae.Detail == "ApplicationError: 2 Concurrency exception." { + return ErrConcurrentTransaction + } + if ae.Code == int32(pb.Error_CONCURRENT_TRANSACTION) { + return ErrConcurrentTransaction + } + } + return err +} diff --git a/vendor/google.golang.org/appengine/internal/urlfetch/urlfetch_service.pb.go b/vendor/google.golang.org/appengine/internal/urlfetch/urlfetch_service.pb.go new file mode 100644 index 00000000000..af463fbb265 --- /dev/null +++ b/vendor/google.golang.org/appengine/internal/urlfetch/urlfetch_service.pb.go @@ -0,0 +1,355 @@ +// Code generated by protoc-gen-go. +// source: google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +// DO NOT EDIT! + +/* +Package urlfetch is a generated protocol buffer package. + +It is generated from these files: + google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto + +It has these top-level messages: + URLFetchServiceError + URLFetchRequest + URLFetchResponse +*/ +package urlfetch + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type URLFetchServiceError_ErrorCode int32 + +const ( + URLFetchServiceError_OK URLFetchServiceError_ErrorCode = 0 + URLFetchServiceError_INVALID_URL URLFetchServiceError_ErrorCode = 1 + URLFetchServiceError_FETCH_ERROR URLFetchServiceError_ErrorCode = 2 + URLFetchServiceError_UNSPECIFIED_ERROR URLFetchServiceError_ErrorCode = 3 + URLFetchServiceError_RESPONSE_TOO_LARGE URLFetchServiceError_ErrorCode = 4 + URLFetchServiceError_DEADLINE_EXCEEDED URLFetchServiceError_ErrorCode = 5 + URLFetchServiceError_SSL_CERTIFICATE_ERROR URLFetchServiceError_ErrorCode = 6 + URLFetchServiceError_DNS_ERROR URLFetchServiceError_ErrorCode = 7 + URLFetchServiceError_CLOSED URLFetchServiceError_ErrorCode = 8 + URLFetchServiceError_INTERNAL_TRANSIENT_ERROR URLFetchServiceError_ErrorCode = 9 + URLFetchServiceError_TOO_MANY_REDIRECTS URLFetchServiceError_ErrorCode = 10 + URLFetchServiceError_MALFORMED_REPLY URLFetchServiceError_ErrorCode = 11 + URLFetchServiceError_CONNECTION_ERROR URLFetchServiceError_ErrorCode = 12 +) + +var URLFetchServiceError_ErrorCode_name = map[int32]string{ + 0: "OK", + 1: "INVALID_URL", + 2: "FETCH_ERROR", + 3: "UNSPECIFIED_ERROR", + 4: "RESPONSE_TOO_LARGE", + 5: "DEADLINE_EXCEEDED", + 6: "SSL_CERTIFICATE_ERROR", + 7: "DNS_ERROR", + 8: "CLOSED", + 9: "INTERNAL_TRANSIENT_ERROR", + 10: "TOO_MANY_REDIRECTS", + 11: "MALFORMED_REPLY", + 12: "CONNECTION_ERROR", +} +var URLFetchServiceError_ErrorCode_value = map[string]int32{ + "OK": 0, + "INVALID_URL": 1, + "FETCH_ERROR": 2, + "UNSPECIFIED_ERROR": 3, + "RESPONSE_TOO_LARGE": 4, + "DEADLINE_EXCEEDED": 5, + "SSL_CERTIFICATE_ERROR": 6, + "DNS_ERROR": 7, + "CLOSED": 8, + "INTERNAL_TRANSIENT_ERROR": 9, + "TOO_MANY_REDIRECTS": 10, + "MALFORMED_REPLY": 11, + "CONNECTION_ERROR": 12, +} + +func (x URLFetchServiceError_ErrorCode) Enum() *URLFetchServiceError_ErrorCode { + p := new(URLFetchServiceError_ErrorCode) + *p = x + return p +} +func (x URLFetchServiceError_ErrorCode) String() string { + return proto.EnumName(URLFetchServiceError_ErrorCode_name, int32(x)) +} +func (x *URLFetchServiceError_ErrorCode) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(URLFetchServiceError_ErrorCode_value, data, "URLFetchServiceError_ErrorCode") + if err != nil { + return err + } + *x = URLFetchServiceError_ErrorCode(value) + return nil +} + +type URLFetchRequest_RequestMethod int32 + +const ( + URLFetchRequest_GET URLFetchRequest_RequestMethod = 1 + URLFetchRequest_POST URLFetchRequest_RequestMethod = 2 + URLFetchRequest_HEAD URLFetchRequest_RequestMethod = 3 + URLFetchRequest_PUT URLFetchRequest_RequestMethod = 4 + URLFetchRequest_DELETE URLFetchRequest_RequestMethod = 5 + URLFetchRequest_PATCH URLFetchRequest_RequestMethod = 6 +) + +var URLFetchRequest_RequestMethod_name = map[int32]string{ + 1: "GET", + 2: "POST", + 3: "HEAD", + 4: "PUT", + 5: "DELETE", + 6: "PATCH", +} +var URLFetchRequest_RequestMethod_value = map[string]int32{ + "GET": 1, + "POST": 2, + "HEAD": 3, + "PUT": 4, + "DELETE": 5, + "PATCH": 6, +} + +func (x URLFetchRequest_RequestMethod) Enum() *URLFetchRequest_RequestMethod { + p := new(URLFetchRequest_RequestMethod) + *p = x + return p +} +func (x URLFetchRequest_RequestMethod) String() string { + return proto.EnumName(URLFetchRequest_RequestMethod_name, int32(x)) +} +func (x *URLFetchRequest_RequestMethod) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(URLFetchRequest_RequestMethod_value, data, "URLFetchRequest_RequestMethod") + if err != nil { + return err + } + *x = URLFetchRequest_RequestMethod(value) + return nil +} + +type URLFetchServiceError struct { + XXX_unrecognized []byte `json:"-"` +} + +func (m *URLFetchServiceError) Reset() { *m = URLFetchServiceError{} } +func (m *URLFetchServiceError) String() string { return proto.CompactTextString(m) } +func (*URLFetchServiceError) ProtoMessage() {} + +type URLFetchRequest struct { + Method *URLFetchRequest_RequestMethod `protobuf:"varint,1,req,name=Method,enum=appengine.URLFetchRequest_RequestMethod" json:"Method,omitempty"` + Url *string `protobuf:"bytes,2,req,name=Url" json:"Url,omitempty"` + Header []*URLFetchRequest_Header `protobuf:"group,3,rep,name=Header" json:"header,omitempty"` + Payload []byte `protobuf:"bytes,6,opt,name=Payload" json:"Payload,omitempty"` + FollowRedirects *bool `protobuf:"varint,7,opt,name=FollowRedirects,def=1" json:"FollowRedirects,omitempty"` + Deadline *float64 `protobuf:"fixed64,8,opt,name=Deadline" json:"Deadline,omitempty"` + MustValidateServerCertificate *bool `protobuf:"varint,9,opt,name=MustValidateServerCertificate,def=1" json:"MustValidateServerCertificate,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *URLFetchRequest) Reset() { *m = URLFetchRequest{} } +func (m *URLFetchRequest) String() string { return proto.CompactTextString(m) } +func (*URLFetchRequest) ProtoMessage() {} + +const Default_URLFetchRequest_FollowRedirects bool = true +const Default_URLFetchRequest_MustValidateServerCertificate bool = true + +func (m *URLFetchRequest) GetMethod() URLFetchRequest_RequestMethod { + if m != nil && m.Method != nil { + return *m.Method + } + return URLFetchRequest_GET +} + +func (m *URLFetchRequest) GetUrl() string { + if m != nil && m.Url != nil { + return *m.Url + } + return "" +} + +func (m *URLFetchRequest) GetHeader() []*URLFetchRequest_Header { + if m != nil { + return m.Header + } + return nil +} + +func (m *URLFetchRequest) GetPayload() []byte { + if m != nil { + return m.Payload + } + return nil +} + +func (m *URLFetchRequest) GetFollowRedirects() bool { + if m != nil && m.FollowRedirects != nil { + return *m.FollowRedirects + } + return Default_URLFetchRequest_FollowRedirects +} + +func (m *URLFetchRequest) GetDeadline() float64 { + if m != nil && m.Deadline != nil { + return *m.Deadline + } + return 0 +} + +func (m *URLFetchRequest) GetMustValidateServerCertificate() bool { + if m != nil && m.MustValidateServerCertificate != nil { + return *m.MustValidateServerCertificate + } + return Default_URLFetchRequest_MustValidateServerCertificate +} + +type URLFetchRequest_Header struct { + Key *string `protobuf:"bytes,4,req,name=Key" json:"Key,omitempty"` + Value *string `protobuf:"bytes,5,req,name=Value" json:"Value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *URLFetchRequest_Header) Reset() { *m = URLFetchRequest_Header{} } +func (m *URLFetchRequest_Header) String() string { return proto.CompactTextString(m) } +func (*URLFetchRequest_Header) ProtoMessage() {} + +func (m *URLFetchRequest_Header) GetKey() string { + if m != nil && m.Key != nil { + return *m.Key + } + return "" +} + +func (m *URLFetchRequest_Header) GetValue() string { + if m != nil && m.Value != nil { + return *m.Value + } + return "" +} + +type URLFetchResponse struct { + Content []byte `protobuf:"bytes,1,opt,name=Content" json:"Content,omitempty"` + StatusCode *int32 `protobuf:"varint,2,req,name=StatusCode" json:"StatusCode,omitempty"` + Header []*URLFetchResponse_Header `protobuf:"group,3,rep,name=Header" json:"header,omitempty"` + ContentWasTruncated *bool `protobuf:"varint,6,opt,name=ContentWasTruncated,def=0" json:"ContentWasTruncated,omitempty"` + ExternalBytesSent *int64 `protobuf:"varint,7,opt,name=ExternalBytesSent" json:"ExternalBytesSent,omitempty"` + ExternalBytesReceived *int64 `protobuf:"varint,8,opt,name=ExternalBytesReceived" json:"ExternalBytesReceived,omitempty"` + FinalUrl *string `protobuf:"bytes,9,opt,name=FinalUrl" json:"FinalUrl,omitempty"` + ApiCpuMilliseconds *int64 `protobuf:"varint,10,opt,name=ApiCpuMilliseconds,def=0" json:"ApiCpuMilliseconds,omitempty"` + ApiBytesSent *int64 `protobuf:"varint,11,opt,name=ApiBytesSent,def=0" json:"ApiBytesSent,omitempty"` + ApiBytesReceived *int64 `protobuf:"varint,12,opt,name=ApiBytesReceived,def=0" json:"ApiBytesReceived,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *URLFetchResponse) Reset() { *m = URLFetchResponse{} } +func (m *URLFetchResponse) String() string { return proto.CompactTextString(m) } +func (*URLFetchResponse) ProtoMessage() {} + +const Default_URLFetchResponse_ContentWasTruncated bool = false +const Default_URLFetchResponse_ApiCpuMilliseconds int64 = 0 +const Default_URLFetchResponse_ApiBytesSent int64 = 0 +const Default_URLFetchResponse_ApiBytesReceived int64 = 0 + +func (m *URLFetchResponse) GetContent() []byte { + if m != nil { + return m.Content + } + return nil +} + +func (m *URLFetchResponse) GetStatusCode() int32 { + if m != nil && m.StatusCode != nil { + return *m.StatusCode + } + return 0 +} + +func (m *URLFetchResponse) GetHeader() []*URLFetchResponse_Header { + if m != nil { + return m.Header + } + return nil +} + +func (m *URLFetchResponse) GetContentWasTruncated() bool { + if m != nil && m.ContentWasTruncated != nil { + return *m.ContentWasTruncated + } + return Default_URLFetchResponse_ContentWasTruncated +} + +func (m *URLFetchResponse) GetExternalBytesSent() int64 { + if m != nil && m.ExternalBytesSent != nil { + return *m.ExternalBytesSent + } + return 0 +} + +func (m *URLFetchResponse) GetExternalBytesReceived() int64 { + if m != nil && m.ExternalBytesReceived != nil { + return *m.ExternalBytesReceived + } + return 0 +} + +func (m *URLFetchResponse) GetFinalUrl() string { + if m != nil && m.FinalUrl != nil { + return *m.FinalUrl + } + return "" +} + +func (m *URLFetchResponse) GetApiCpuMilliseconds() int64 { + if m != nil && m.ApiCpuMilliseconds != nil { + return *m.ApiCpuMilliseconds + } + return Default_URLFetchResponse_ApiCpuMilliseconds +} + +func (m *URLFetchResponse) GetApiBytesSent() int64 { + if m != nil && m.ApiBytesSent != nil { + return *m.ApiBytesSent + } + return Default_URLFetchResponse_ApiBytesSent +} + +func (m *URLFetchResponse) GetApiBytesReceived() int64 { + if m != nil && m.ApiBytesReceived != nil { + return *m.ApiBytesReceived + } + return Default_URLFetchResponse_ApiBytesReceived +} + +type URLFetchResponse_Header struct { + Key *string `protobuf:"bytes,4,req,name=Key" json:"Key,omitempty"` + Value *string `protobuf:"bytes,5,req,name=Value" json:"Value,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *URLFetchResponse_Header) Reset() { *m = URLFetchResponse_Header{} } +func (m *URLFetchResponse_Header) String() string { return proto.CompactTextString(m) } +func (*URLFetchResponse_Header) ProtoMessage() {} + +func (m *URLFetchResponse_Header) GetKey() string { + if m != nil && m.Key != nil { + return *m.Key + } + return "" +} + +func (m *URLFetchResponse_Header) GetValue() string { + if m != nil && m.Value != nil { + return *m.Value + } + return "" +} + +func init() { +} diff --git a/vendor/google.golang.org/appengine/namespace.go b/vendor/google.golang.org/appengine/namespace.go new file mode 100644 index 00000000000..21860ca0822 --- /dev/null +++ b/vendor/google.golang.org/appengine/namespace.go @@ -0,0 +1,25 @@ +// Copyright 2012 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package appengine + +import ( + "fmt" + "regexp" + + "golang.org/x/net/context" + + "google.golang.org/appengine/internal" +) + +// Namespace returns a replacement context that operates within the given namespace. +func Namespace(c context.Context, namespace string) (context.Context, error) { + if !validNamespace.MatchString(namespace) { + return nil, fmt.Errorf("appengine: namespace %q does not match /%s/", namespace, validNamespace) + } + return internal.NamespacedContext(c, namespace), nil +} + +// validNamespace matches valid namespace names. +var validNamespace = regexp.MustCompile(`^[0-9A-Za-z._-]{0,100}$`) diff --git a/vendor/google.golang.org/appengine/timeout.go b/vendor/google.golang.org/appengine/timeout.go new file mode 100644 index 00000000000..05642a992a3 --- /dev/null +++ b/vendor/google.golang.org/appengine/timeout.go @@ -0,0 +1,20 @@ +// Copyright 2013 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package appengine + +import "golang.org/x/net/context" + +// IsTimeoutError reports whether err is a timeout error. +func IsTimeoutError(err error) bool { + if err == context.DeadlineExceeded { + return true + } + if t, ok := err.(interface { + IsTimeout() bool + }); ok { + return t.IsTimeout() + } + return false +} diff --git a/vendor/google.golang.org/appengine/urlfetch/urlfetch.go b/vendor/google.golang.org/appengine/urlfetch/urlfetch.go new file mode 100644 index 00000000000..6ffe1e6d901 --- /dev/null +++ b/vendor/google.golang.org/appengine/urlfetch/urlfetch.go @@ -0,0 +1,210 @@ +// Copyright 2011 Google Inc. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +// Package urlfetch provides an http.RoundTripper implementation +// for fetching URLs via App Engine's urlfetch service. +package urlfetch // import "google.golang.org/appengine/urlfetch" + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/golang/protobuf/proto" + "golang.org/x/net/context" + + "google.golang.org/appengine/internal" + pb "google.golang.org/appengine/internal/urlfetch" +) + +// Transport is an implementation of http.RoundTripper for +// App Engine. Users should generally create an http.Client using +// this transport and use the Client rather than using this transport +// directly. +type Transport struct { + Context context.Context + + // Controls whether the application checks the validity of SSL certificates + // over HTTPS connections. A value of false (the default) instructs the + // application to send a request to the server only if the certificate is + // valid and signed by a trusted certificate authority (CA), and also + // includes a hostname that matches the certificate. A value of true + // instructs the application to perform no certificate validation. + AllowInvalidServerCertificate bool +} + +// Verify statically that *Transport implements http.RoundTripper. +var _ http.RoundTripper = (*Transport)(nil) + +// Client returns an *http.Client using a default urlfetch Transport. This +// client will have the default deadline of 5 seconds, and will check the +// validity of SSL certificates. +// +// Any deadline of the provided context will be used for requests through this client; +// if the client does not have a deadline then a 5 second default is used. +func Client(ctx context.Context) *http.Client { + return &http.Client{ + Transport: &Transport{ + Context: ctx, + }, + } +} + +type bodyReader struct { + content []byte + truncated bool + closed bool +} + +// ErrTruncatedBody is the error returned after the final Read() from a +// response's Body if the body has been truncated by App Engine's proxy. +var ErrTruncatedBody = errors.New("urlfetch: truncated body") + +func statusCodeToText(code int) string { + if t := http.StatusText(code); t != "" { + return t + } + return strconv.Itoa(code) +} + +func (br *bodyReader) Read(p []byte) (n int, err error) { + if br.closed { + if br.truncated { + return 0, ErrTruncatedBody + } + return 0, io.EOF + } + n = copy(p, br.content) + if n > 0 { + br.content = br.content[n:] + return + } + if br.truncated { + br.closed = true + return 0, ErrTruncatedBody + } + return 0, io.EOF +} + +func (br *bodyReader) Close() error { + br.closed = true + br.content = nil + return nil +} + +// A map of the URL Fetch-accepted methods that take a request body. +var methodAcceptsRequestBody = map[string]bool{ + "POST": true, + "PUT": true, + "PATCH": true, +} + +// urlString returns a valid string given a URL. This function is necessary because +// the String method of URL doesn't correctly handle URLs with non-empty Opaque values. +// See http://code.google.com/p/go/issues/detail?id=4860. +func urlString(u *url.URL) string { + if u.Opaque == "" || strings.HasPrefix(u.Opaque, "//") { + return u.String() + } + aux := *u + aux.Opaque = "//" + aux.Host + aux.Opaque + return aux.String() +} + +// RoundTrip issues a single HTTP request and returns its response. Per the +// http.RoundTripper interface, RoundTrip only returns an error if there +// was an unsupported request or the URL Fetch proxy fails. +// Note that HTTP response codes such as 5xx, 403, 404, etc are not +// errors as far as the transport is concerned and will be returned +// with err set to nil. +func (t *Transport) RoundTrip(req *http.Request) (res *http.Response, err error) { + methNum, ok := pb.URLFetchRequest_RequestMethod_value[req.Method] + if !ok { + return nil, fmt.Errorf("urlfetch: unsupported HTTP method %q", req.Method) + } + + method := pb.URLFetchRequest_RequestMethod(methNum) + + freq := &pb.URLFetchRequest{ + Method: &method, + Url: proto.String(urlString(req.URL)), + FollowRedirects: proto.Bool(false), // http.Client's responsibility + MustValidateServerCertificate: proto.Bool(!t.AllowInvalidServerCertificate), + } + if deadline, ok := t.Context.Deadline(); ok { + freq.Deadline = proto.Float64(deadline.Sub(time.Now()).Seconds()) + } + + for k, vals := range req.Header { + for _, val := range vals { + freq.Header = append(freq.Header, &pb.URLFetchRequest_Header{ + Key: proto.String(k), + Value: proto.String(val), + }) + } + } + if methodAcceptsRequestBody[req.Method] && req.Body != nil { + // Avoid a []byte copy if req.Body has a Bytes method. + switch b := req.Body.(type) { + case interface { + Bytes() []byte + }: + freq.Payload = b.Bytes() + default: + freq.Payload, err = ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + } + } + + fres := &pb.URLFetchResponse{} + if err := internal.Call(t.Context, "urlfetch", "Fetch", freq, fres); err != nil { + return nil, err + } + + res = &http.Response{} + res.StatusCode = int(*fres.StatusCode) + res.Status = fmt.Sprintf("%d %s", res.StatusCode, statusCodeToText(res.StatusCode)) + res.Header = make(http.Header) + res.Request = req + + // Faked: + res.ProtoMajor = 1 + res.ProtoMinor = 1 + res.Proto = "HTTP/1.1" + res.Close = true + + for _, h := range fres.Header { + hkey := http.CanonicalHeaderKey(*h.Key) + hval := *h.Value + if hkey == "Content-Length" { + // Will get filled in below for all but HEAD requests. + if req.Method == "HEAD" { + res.ContentLength, _ = strconv.ParseInt(hval, 10, 64) + } + continue + } + res.Header.Add(hkey, hval) + } + + if req.Method != "HEAD" { + res.ContentLength = int64(len(fres.Content)) + } + + truncated := fres.GetContentWasTruncated() + res.Body = &bodyReader{content: fres.Content, truncated: truncated} + return +} + +func init() { + internal.RegisterErrorCodeMap("urlfetch", pb.URLFetchServiceError_ErrorCode_name) + internal.RegisterTimeoutErrorCode("urlfetch", int32(pb.URLFetchServiceError_DEADLINE_EXCEEDED)) +}
    - {{variable.query}} + {{variable.definition ? variable.definition : variable.query}}