mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/master' into develop
This commit is contained in:
commit
beb9f8ee74
2
.gitignore
vendored
2
.gitignore
vendored
@ -39,6 +39,8 @@ conf/custom.ini
|
|||||||
fig.yml
|
fig.yml
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
docker-compose.yaml
|
docker-compose.yaml
|
||||||
|
/conf/dashboards/custom.yaml
|
||||||
|
/conf/datasources/custom.yaml
|
||||||
profile.cov
|
profile.cov
|
||||||
/grafana
|
/grafana
|
||||||
.notouch
|
.notouch
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
* **Cloudwatch**: Fixes broken query inspector for cloudwatch [#9661](https://github.com/grafana/grafana/issues/9661), thx [@mtanda](https://github.com/mtanda)
|
* **Cloudwatch**: Fixes broken query inspector for cloudwatch [#9661](https://github.com/grafana/grafana/issues/9661), thx [@mtanda](https://github.com/mtanda)
|
||||||
* **Dashboard**: Make it possible to start dashboards from search and dashboard list panel [#1871](https://github.com/grafana/grafana/issues/1871)
|
* **Dashboard**: Make it possible to start dashboards from search and dashboard list panel [#1871](https://github.com/grafana/grafana/issues/1871)
|
||||||
* **Annotations**: Posting annotations now return the id of the annotation [#9798](https://github.com/grafana/grafana/issues/9798)
|
* **Annotations**: Posting annotations now return the id of the annotation [#9798](https://github.com/grafana/grafana/issues/9798)
|
||||||
|
* **Systemd**: Use systemd notification ready flag [#10024](https://github.com/grafana/grafana/issues/10024), thx [@jgrassler](https://github.com/jgrassler)
|
||||||
## Tech
|
## Tech
|
||||||
* **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
|
* **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
|
||||||
|
|
||||||
|
208
LICENSE.md
208
LICENSE.md
@ -1,14 +1,202 @@
|
|||||||
Copyright 2014-2017 Torkel Ödegaard, Raintank Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License"); you
|
Apache License
|
||||||
may not use this file except in compliance with the License. You may
|
Version 2.0, January 2004
|
||||||
obtain a copy of the License at
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
1. Definitions.
|
||||||
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.
|
|
||||||
|
|
||||||
|
"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.
|
||||||
|
16
NOTICE.md
16
NOTICE.md
@ -1,16 +1,6 @@
|
|||||||
|
|
||||||
This software is based on Kibana:
|
Copyright 2014-2017 Grafana Labs
|
||||||
========================================
|
|
||||||
|
This software is based on Kibana:
|
||||||
Copyright 2012-2013 Elasticsearch BV
|
Copyright 2012-2013 Elasticsearch BV
|
||||||
|
|
||||||
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.
|
|
||||||
|
6
build.go
6
build.go
@ -95,9 +95,9 @@ func main() {
|
|||||||
|
|
||||||
case "package":
|
case "package":
|
||||||
grunt(gruntBuildArg("release")...)
|
grunt(gruntBuildArg("release")...)
|
||||||
if runtime.GOOS != "windows" {
|
if runtime.GOOS != "windows" {
|
||||||
createLinuxPackages()
|
createLinuxPackages()
|
||||||
}
|
}
|
||||||
|
|
||||||
case "pkg-rpm":
|
case "pkg-rpm":
|
||||||
grunt(gruntBuildArg("release")...)
|
grunt(gruntBuildArg("release")...)
|
||||||
|
@ -7,3 +7,4 @@
|
|||||||
MYSQL_PASSWORD: password
|
MYSQL_PASSWORD: password
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "3306:3306"
|
||||||
|
tmpfs: /var/lib/mysql:rw
|
||||||
|
@ -5,3 +5,4 @@
|
|||||||
POSTGRES_PASSWORD: grafanatest
|
POSTGRES_PASSWORD: grafanatest
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
tmpfs: /var/lib/postgresql/data:rw
|
@ -65,13 +65,14 @@ Currently we do not provide any scripts/manifests for configuring Grafana. Rathe
|
|||||||
Tool | Project
|
Tool | Project
|
||||||
-----|------------
|
-----|------------
|
||||||
Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana)
|
Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana)
|
||||||
|
Ansible | [https://github.com/cloudalchemy/ansible-grafana](https://github.com/cloudalchemy/ansible-grafana)
|
||||||
Ansible | [https://github.com/picotrading/ansible-grafana](https://github.com/picotrading/ansible-grafana)
|
Ansible | [https://github.com/picotrading/ansible-grafana](https://github.com/picotrading/ansible-grafana)
|
||||||
Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana)
|
Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana)
|
||||||
Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana)
|
Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana)
|
||||||
|
|
||||||
## Datasources
|
## Datasources
|
||||||
|
|
||||||
> This feature is available from v4.7
|
> This feature is available from v5.0
|
||||||
|
|
||||||
It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`conf/datasources`](/installation/configuration/#datasources) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list.
|
It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`conf/datasources`](/installation/configuration/#datasources) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list.
|
||||||
|
|
||||||
|
@ -127,6 +127,12 @@ A query can returns multiple columns and Grafana will automatically create a lis
|
|||||||
SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
|
SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
|
||||||
|
```
|
||||||
|
|
||||||
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
|
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
@ -139,6 +139,12 @@ A query can return multiple columns and Grafana will automatically create a list
|
|||||||
SELECT host.hostname, other_host.hostname2 FROM host JOIN other_host ON host.city = other_host.city
|
SELECT host.hostname, other_host.hostname2 FROM host JOIN other_host ON host.city = other_host.city
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
|
||||||
|
```
|
||||||
|
|
||||||
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
|
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
@ -93,7 +93,10 @@ Directory where grafana will automatically scan and look for plugins
|
|||||||
|
|
||||||
### datasources
|
### datasources
|
||||||
|
|
||||||
Config files containing datasources that will be configured at startup
|
> This feature is available in 5.0+
|
||||||
|
|
||||||
|
Config files containing datasources that will be configured at startup.
|
||||||
|
You can read more about the config files at the [provisioning page](/administration/provisioning/#datasources).
|
||||||
|
|
||||||
## [server]
|
## [server]
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ After=postgresql.service mariadb.service mysql.service
|
|||||||
EnvironmentFile=/etc/sysconfig/grafana-server
|
EnvironmentFile=/etc/sysconfig/grafana-server
|
||||||
User=grafana
|
User=grafana
|
||||||
Group=grafana
|
Group=grafana
|
||||||
Type=simple
|
Type=notify
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
WorkingDirectory=/usr/share/grafana
|
WorkingDirectory=/usr/share/grafana
|
||||||
RuntimeDirectory=grafana
|
RuntimeDirectory=grafana
|
||||||
|
@ -10,7 +10,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func RenderToPng(c *middleware.Context) {
|
func RenderToPng(c *middleware.Context) {
|
||||||
queryReader := util.NewUrlQueryReader(c.Req.URL)
|
queryReader, err := util.NewUrlQueryReader(c.Req.URL)
|
||||||
|
if err != nil {
|
||||||
|
c.Handle(400, "Render parameters error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
|
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
|
||||||
|
|
||||||
renderOpts := &renderer.RenderOpts{
|
renderOpts := &renderer.RenderOpts{
|
||||||
|
@ -3,7 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -96,6 +98,7 @@ func (g *GrafanaServerImpl) Start() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SendSystemdNotification("READY=1")
|
||||||
g.startHttpServer()
|
g.startHttpServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,3 +172,28 @@ func (g *GrafanaServerImpl) writePIDFile() {
|
|||||||
|
|
||||||
g.log.Info("Writing PID file", "path", *pidFile, "pid", pid)
|
g.log.Info("Writing PID file", "path", *pidFile, "pid", pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SendSystemdNotification(state string) error {
|
||||||
|
notifySocket := os.Getenv("NOTIFY_SOCKET")
|
||||||
|
|
||||||
|
if notifySocket == "" {
|
||||||
|
return fmt.Errorf("NOTIFY_SOCKET environment variable empty or unset.")
|
||||||
|
}
|
||||||
|
|
||||||
|
socketAddr := &net.UnixAddr{
|
||||||
|
Name: notifySocket,
|
||||||
|
Net: "unixgram",
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.Write([]byte(state))
|
||||||
|
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
@ -38,6 +38,7 @@ func TestLogFile(t *testing.T) {
|
|||||||
So(fileLogWrite.maxlines_curlines, ShouldEqual, 3)
|
So(fileLogWrite.maxlines_curlines, ShouldEqual, 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fileLogWrite.Close()
|
||||||
err = os.Remove(fileLogWrite.Filename)
|
err = os.Remove(fileLogWrite.Filename)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
})
|
})
|
||||||
|
@ -366,7 +366,6 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
"steppedLine": false,
|
"steppedLine": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"dsType": "influxdb",
|
|
||||||
"groupBy": [
|
"groupBy": [
|
||||||
{
|
{
|
||||||
"params": [
|
"params": [
|
||||||
@ -411,7 +410,6 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
"tags": []
|
"tags": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dsType": "influxdb",
|
|
||||||
"groupBy": [
|
"groupBy": [
|
||||||
{
|
{
|
||||||
"params": [
|
"params": [
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
@ -176,7 +177,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
|||||||
|
|
||||||
func SlackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error {
|
func SlackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error {
|
||||||
if evalContext.ImageOnDiskPath == "" {
|
if evalContext.ImageOnDiskPath == "" {
|
||||||
evalContext.ImageOnDiskPath = "public/img/mixed_styles.png"
|
evalContext.ImageOnDiskPath = filepath.Join(setting.HomePath, "public/img/mixed_styles.png")
|
||||||
}
|
}
|
||||||
log.Info("Uploading to slack via file.upload API")
|
log.Info("Uploading to slack via file.upload API")
|
||||||
headers, uploadBody, err := GenerateSlackBody(evalContext.ImageOnDiskPath, token, recipient)
|
headers, uploadBody, err := GenerateSlackBody(evalContext.ImageOnDiskPath, token, recipient)
|
||||||
|
@ -401,7 +401,7 @@ func SearchUsers(query *m.SearchUsersQuery) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if query.Query != "" {
|
if query.Query != "" {
|
||||||
whereConditions = append(whereConditions, "(email LIKE ? OR name LIKE ? OR login like ?)")
|
whereConditions = append(whereConditions, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)")
|
||||||
whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards)
|
whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/tsdb"
|
"github.com/grafana/grafana/pkg/tsdb"
|
||||||
opentracing "github.com/opentracing/opentracing-go"
|
"github.com/opentracing/opentracing-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GraphiteExecutor struct {
|
type GraphiteExecutor struct {
|
||||||
@ -158,7 +158,7 @@ func formatTimeRange(input string) string {
|
|||||||
if input == "now" {
|
if input == "now" {
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1)
|
return strings.Replace(strings.Replace(strings.Replace(input, "now", "", -1), "m", "min", -1), "M", "mon", -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fixIntervalFormat(target string) string {
|
func fixIntervalFormat(target string) string {
|
||||||
|
@ -18,14 +18,14 @@ func TestGraphiteFunctions(t *testing.T) {
|
|||||||
Convey("formatting time range for now-1m", func() {
|
Convey("formatting time range for now-1m", func() {
|
||||||
|
|
||||||
timeRange := formatTimeRange("now-1m")
|
timeRange := formatTimeRange("now-1m")
|
||||||
So(timeRange, ShouldEqual, "now-1min")
|
So(timeRange, ShouldEqual, "-1min")
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("formatting time range for now-1M", func() {
|
Convey("formatting time range for now-1M", func() {
|
||||||
|
|
||||||
timeRange := formatTimeRange("now-1M")
|
timeRange := formatTimeRange("now-1M")
|
||||||
So(timeRange, ShouldEqual, "now-1mon")
|
So(timeRange, ShouldEqual, "-1mon")
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ func TestInfluxdbQueryParser(t *testing.T) {
|
|||||||
Convey("can parse influxdb json model", func() {
|
Convey("can parse influxdb json model", func() {
|
||||||
json := `
|
json := `
|
||||||
{
|
{
|
||||||
"dsType": "influxdb",
|
|
||||||
"groupBy": [
|
"groupBy": [
|
||||||
{
|
{
|
||||||
"params": [
|
"params": [
|
||||||
@ -123,7 +122,6 @@ func TestInfluxdbQueryParser(t *testing.T) {
|
|||||||
Convey("can part raw query json model", func() {
|
Convey("can part raw query json model", func() {
|
||||||
json := `
|
json := `
|
||||||
{
|
{
|
||||||
"dsType": "influxdb",
|
|
||||||
"groupBy": [
|
"groupBy": [
|
||||||
{
|
{
|
||||||
"params": [
|
"params": [
|
||||||
|
@ -50,6 +50,7 @@ func (rp *ResponseParser) transformRows(rows []Row, queryResult *tsdb.QueryResul
|
|||||||
result = append(result, &tsdb.TimeSeries{
|
result = append(result, &tsdb.TimeSeries{
|
||||||
Name: rp.formatSerieName(row, column, query),
|
Name: rp.formatSerieName(row, column, query),
|
||||||
Points: points,
|
Points: points,
|
||||||
|
Tags: row.Tags,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,15 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro
|
|||||||
|
|
||||||
rowLimit := 1000000
|
rowLimit := 1000000
|
||||||
rowCount := 0
|
rowCount := 0
|
||||||
|
timeIndex := -1
|
||||||
|
|
||||||
|
// check if there is a column named time
|
||||||
|
for i, col := range columnNames {
|
||||||
|
switch col {
|
||||||
|
case "time":
|
||||||
|
timeIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for ; rows.Next(); rowCount++ {
|
for ; rows.Next(); rowCount++ {
|
||||||
if rowCount > rowLimit {
|
if rowCount > rowLimit {
|
||||||
@ -89,6 +98,15 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convert column named time to unix timestamp to make
|
||||||
|
// native datetime postgres types work in annotation queries
|
||||||
|
if timeIndex != -1 {
|
||||||
|
switch value := values[timeIndex].(type) {
|
||||||
|
case time.Time:
|
||||||
|
values[timeIndex] = float64(value.UnixNano() / 1e9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table.Rows = append(table.Rows, values)
|
table.Rows = append(table.Rows, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,8 +160,13 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
|
|||||||
func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
|
func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
|
||||||
pointsBySeries := make(map[string]*tsdb.TimeSeries)
|
pointsBySeries := make(map[string]*tsdb.TimeSeries)
|
||||||
seriesByQueryOrder := list.New()
|
seriesByQueryOrder := list.New()
|
||||||
columnNames, err := rows.Columns()
|
|
||||||
|
|
||||||
|
columnNames, err := rows.Columns()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
columnTypes, err := rows.ColumnTypes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -153,13 +176,21 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
|
|||||||
timeIndex := -1
|
timeIndex := -1
|
||||||
metricIndex := -1
|
metricIndex := -1
|
||||||
|
|
||||||
// check columns of resultset
|
// check columns of resultset: a column named time is mandatory
|
||||||
|
// the first text column is treated as metric name unless a column named metric is present
|
||||||
for i, col := range columnNames {
|
for i, col := range columnNames {
|
||||||
switch col {
|
switch col {
|
||||||
case "time":
|
case "time":
|
||||||
timeIndex = i
|
timeIndex = i
|
||||||
case "metric":
|
case "metric":
|
||||||
metricIndex = i
|
metricIndex = i
|
||||||
|
default:
|
||||||
|
if metricIndex == -1 {
|
||||||
|
switch columnTypes[i].DatabaseTypeName() {
|
||||||
|
case "UNKNOWN", "TEXT", "VARCHAR", "CHAR":
|
||||||
|
metricIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,10 +9,15 @@ type UrlQueryReader struct {
|
|||||||
values url.Values
|
values url.Values
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUrlQueryReader(url *url.URL) *UrlQueryReader {
|
func NewUrlQueryReader(urlInfo *url.URL) (*UrlQueryReader, error) {
|
||||||
return &UrlQueryReader{
|
u, err := url.ParseQuery(urlInfo.String())
|
||||||
values: url.Query(),
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &UrlQueryReader{
|
||||||
|
values: u,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UrlQueryReader) Get(name string, def string) string {
|
func (r *UrlQueryReader) Get(name string, def string) string {
|
||||||
|
@ -17,6 +17,10 @@ class Settings {
|
|||||||
alertingEnabled: boolean;
|
alertingEnabled: boolean;
|
||||||
authProxyEnabled: boolean;
|
authProxyEnabled: boolean;
|
||||||
ldapEnabled: boolean;
|
ldapEnabled: boolean;
|
||||||
|
oauth: any;
|
||||||
|
disableUserSignUp: boolean;
|
||||||
|
loginHint: any;
|
||||||
|
loginError: any;
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
var defaults = {
|
var defaults = {
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
define([
|
|
||||||
'./inspect_ctrl',
|
|
||||||
'./json_editor_ctrl',
|
|
||||||
'./login_ctrl',
|
|
||||||
'./invited_ctrl',
|
|
||||||
'./signup_ctrl',
|
|
||||||
'./reset_password_ctrl',
|
|
||||||
'./error_ctrl',
|
|
||||||
], function () {});
|
|
7
public/app/core/controllers/all.ts
Normal file
7
public/app/core/controllers/all.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import './inspect_ctrl';
|
||||||
|
import './json_editor_ctrl';
|
||||||
|
import './login_ctrl';
|
||||||
|
import './invited_ctrl';
|
||||||
|
import './signup_ctrl';
|
||||||
|
import './reset_password_ctrl';
|
||||||
|
import './error_ctrl';
|
@ -1,13 +1,10 @@
|
|||||||
define([
|
import config from 'app/core/config';
|
||||||
'angular',
|
import coreModule from '../core_module';
|
||||||
'app/core/config',
|
|
||||||
'../core_module',
|
|
||||||
],
|
|
||||||
function (angular, config, coreModule) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
coreModule.default.controller('ErrorCtrl', function($scope, contextSrv, navModelSrv) {
|
export class ErrorCtrl {
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor($scope, contextSrv, navModelSrv) {
|
||||||
$scope.navModel = navModelSrv.getNotFoundNav();
|
$scope.navModel = navModelSrv.getNotFoundNav();
|
||||||
$scope.appSubUrl = config.appSubUrl;
|
$scope.appSubUrl = config.appSubUrl;
|
||||||
|
|
||||||
@ -17,7 +14,7 @@ function (angular, config, coreModule) {
|
|||||||
$scope.$on('$destroy', function() {
|
$scope.$on('$destroy', function() {
|
||||||
contextSrv.sidemenu = showSideMenu;
|
contextSrv.sidemenu = showSideMenu;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
coreModule.controller('ErrorCtrl', ErrorCtrl);
|
||||||
|
|
||||||
});
|
|
@ -1,14 +1,10 @@
|
|||||||
define([
|
import coreModule from '../core_module';
|
||||||
'angular',
|
import config from 'app/core/config';
|
||||||
'../core_module',
|
|
||||||
'app/core/config',
|
|
||||||
],
|
|
||||||
function (angular, coreModule, config) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
config = config.default;
|
export class InvitedCtrl {
|
||||||
|
|
||||||
coreModule.default.controller('InvitedCtrl', function($scope, $routeParams, contextSrv, backendSrv) {
|
/** @ngInject */
|
||||||
|
constructor($scope, $routeParams, contextSrv, backendSrv) {
|
||||||
contextSrv.sidemenu = false;
|
contextSrv.sidemenu = false;
|
||||||
$scope.formModel = {};
|
$scope.formModel = {};
|
||||||
|
|
||||||
@ -35,6 +31,7 @@ function (angular, coreModule, config) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.init();
|
$scope.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
coreModule.controller('InvitedCtrl', InvitedCtrl);
|
||||||
});
|
|
@ -1,12 +1,10 @@
|
|||||||
define([
|
import angular from 'angular';
|
||||||
'angular',
|
import coreModule from '../core_module';
|
||||||
'../core_module',
|
|
||||||
],
|
|
||||||
function (angular, coreModule) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
coreModule.default.controller('JsonEditorCtrl', function($scope) {
|
export class JsonEditorCtrl {
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor($scope) {
|
||||||
$scope.json = angular.toJson($scope.object, true);
|
$scope.json = angular.toJson($scope.object, true);
|
||||||
$scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor;
|
$scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor;
|
||||||
|
|
||||||
@ -14,7 +12,7 @@ function (angular, coreModule) {
|
|||||||
var newObject = angular.fromJson($scope.json);
|
var newObject = angular.fromJson($scope.json);
|
||||||
$scope.updateHandler(newObject, $scope.object);
|
$scope.updateHandler(newObject, $scope.object);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
coreModule.controller('JsonEditorCtrl', JsonEditorCtrl);
|
||||||
|
|
||||||
});
|
|
@ -1,15 +1,11 @@
|
|||||||
define([
|
import _ from 'lodash';
|
||||||
'angular',
|
import coreModule from '../core_module';
|
||||||
'lodash',
|
import config from 'app/core/config';
|
||||||
'../core_module',
|
|
||||||
'app/core/config',
|
|
||||||
],
|
|
||||||
function (angular, _, coreModule, config) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
config = config.default;
|
export class LoginCtrl {
|
||||||
|
|
||||||
coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) {
|
/** @ngInject */
|
||||||
|
constructor($scope, backendSrv, contextSrv, $location) {
|
||||||
$scope.formModel = {
|
$scope.formModel = {
|
||||||
user: '',
|
user: '',
|
||||||
email: '',
|
email: '',
|
||||||
@ -74,8 +70,7 @@ function (angular, _, coreModule, config) {
|
|||||||
|
|
||||||
if (params.redirect && params.redirect[0] === '/') {
|
if (params.redirect && params.redirect[0] === '/') {
|
||||||
window.location.href = config.appSubUrl + params.redirect;
|
window.location.href = config.appSubUrl + params.redirect;
|
||||||
}
|
} else if (result.redirectUrl) {
|
||||||
else if (result.redirectUrl) {
|
|
||||||
window.location.href = result.redirectUrl;
|
window.location.href = result.redirectUrl;
|
||||||
} else {
|
} else {
|
||||||
window.location.href = config.appSubUrl + '/';
|
window.location.href = config.appSubUrl + '/';
|
||||||
@ -84,5 +79,7 @@ function (angular, _, coreModule, config) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.init();
|
$scope.init();
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
coreModule.controller('LoginCtrl', LoginCtrl);
|
@ -1,11 +1,9 @@
|
|||||||
define([
|
import coreModule from '../core_module';
|
||||||
'angular',
|
|
||||||
'../core_module',
|
|
||||||
],
|
|
||||||
function (angular, coreModule) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
coreModule.default.controller('ResetPasswordCtrl', function($scope, contextSrv, backendSrv, $location) {
|
export class ResetPasswordCtrl {
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor($scope, contextSrv, backendSrv, $location) {
|
||||||
contextSrv.sidemenu = false;
|
contextSrv.sidemenu = false;
|
||||||
$scope.formModel = {};
|
$scope.formModel = {};
|
||||||
$scope.mode = 'send';
|
$scope.mode = 'send';
|
||||||
@ -37,7 +35,7 @@ function (angular, coreModule) {
|
|||||||
$location.path('login');
|
$location.path('login');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
coreModule.controller('ResetPasswordCtrl', ResetPasswordCtrl);
|
||||||
|
|
||||||
});
|
|
@ -26,7 +26,7 @@ export class DashExportCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveJson() {
|
saveJson() {
|
||||||
var clone = this.dashboardSrv.getCurrent().getSaveModelClone();
|
var clone = this.dash;
|
||||||
|
|
||||||
this.$scope.$root.appEvent('show-json-editor', {
|
this.$scope.$root.appEvent('show-json-editor', {
|
||||||
object: clone,
|
object: clone,
|
||||||
|
@ -1,18 +1,11 @@
|
|||||||
define(['angular',
|
import angular from 'angular';
|
||||||
'lodash',
|
import moment from 'moment';
|
||||||
'jquery',
|
import config from 'app/core/config';
|
||||||
'moment',
|
|
||||||
'app/core/config',
|
|
||||||
],
|
|
||||||
function (angular, _, $, moment, config) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
config = config.default;
|
export class ShareModalCtrl {
|
||||||
|
|
||||||
var module = angular.module('grafana.controllers');
|
|
||||||
|
|
||||||
module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) {
|
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) {
|
||||||
$scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' };
|
$scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' };
|
||||||
$scope.editor = { index: $scope.tabIndex || 0};
|
$scope.editor = { index: $scope.tabIndex || 0};
|
||||||
|
|
||||||
@ -93,7 +86,7 @@ function (angular, _, $, moment, config) {
|
|||||||
$scope.getShareUrl = function() {
|
$scope.getShareUrl = function() {
|
||||||
return $scope.shareUrl;
|
return $scope.shareUrl;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
angular.module('grafana.controllers').controller('ShareModalCtrl', ShareModalCtrl);
|
||||||
|
|
||||||
});
|
|
@ -172,7 +172,6 @@ export class ElasticQueryBuilder {
|
|||||||
build(target, adhocFilters?, queryString?) {
|
build(target, adhocFilters?, queryString?) {
|
||||||
// make sure query has defaults;
|
// make sure query has defaults;
|
||||||
target.metrics = target.metrics || [{ type: 'count', id: '1' }];
|
target.metrics = target.metrics || [{ type: 'count', id: '1' }];
|
||||||
target.dsType = 'elasticsearch';
|
|
||||||
target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}];
|
target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}];
|
||||||
target.timeField = this.timeField;
|
target.timeField = this.timeField;
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ export default class InfluxQuery {
|
|||||||
this.scopedVars = scopedVars;
|
this.scopedVars = scopedVars;
|
||||||
|
|
||||||
target.policy = target.policy || 'default';
|
target.policy = target.policy || 'default';
|
||||||
target.dsType = 'influxdb';
|
|
||||||
target.resultFormat = target.resultFormat || 'time_series';
|
target.resultFormat = target.resultFormat || 'time_series';
|
||||||
target.orderByTime = target.orderByTime || 'ASC';
|
target.orderByTime = target.orderByTime || 'ASC';
|
||||||
target.tags = target.tags || [];
|
target.tags = target.tags || [];
|
||||||
|
@ -103,12 +103,21 @@ export class MysqlDatasource {
|
|||||||
format: 'table',
|
format: 'table',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
queries: [interpolatedQuery],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (optionalOptions && optionalOptions.range && optionalOptions.range.from) {
|
||||||
|
data['from'] = optionalOptions.range.from.valueOf().toString();
|
||||||
|
}
|
||||||
|
if (optionalOptions && optionalOptions.range && optionalOptions.range.to) {
|
||||||
|
data['to'] = optionalOptions.range.to.valueOf().toString();
|
||||||
|
}
|
||||||
|
|
||||||
return this.backendSrv.datasourceRequest({
|
return this.backendSrv.datasourceRequest({
|
||||||
url: '/api/tsdb/query',
|
url: '/api/tsdb/query',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: data
|
||||||
queries: [interpolatedQuery],
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
|
.then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
|
||||||
}
|
}
|
||||||
|
@ -99,12 +99,21 @@ export class PostgresDatasource {
|
|||||||
format: 'table',
|
format: 'table',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
queries: [interpolatedQuery],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (optionalOptions && optionalOptions.range && optionalOptions.range.from) {
|
||||||
|
data['from'] = optionalOptions.range.from.valueOf().toString();
|
||||||
|
}
|
||||||
|
if (optionalOptions && optionalOptions.range && optionalOptions.range.to) {
|
||||||
|
data['to'] = optionalOptions.range.to.valueOf().toString();
|
||||||
|
}
|
||||||
|
|
||||||
return this.backendSrv.datasourceRequest({
|
return this.backendSrv.datasourceRequest({
|
||||||
url: '/api/tsdb/query',
|
url: '/api/tsdb/query',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: data
|
||||||
queries: [interpolatedQuery],
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
|
.then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
|
||||||
}
|
}
|
||||||
|
@ -67,8 +67,8 @@ export default class PrometheusMetricFindQuery {
|
|||||||
|
|
||||||
return this.datasource._request("GET", url).then(function(result) {
|
return this.datasource._request("GET", url).then(function(result) {
|
||||||
var _labels = _.map(result.data.data, function(metric) {
|
var _labels = _.map(result.data.data, function(metric) {
|
||||||
return metric[label];
|
return metric[label] || '';
|
||||||
});
|
}).filter(function(label) { return label !== ''; });
|
||||||
|
|
||||||
return _.uniq(_labels).map(function(metric) {
|
return _.uniq(_labels).map(function(metric) {
|
||||||
return {
|
return {
|
||||||
|
@ -1,63 +1,60 @@
|
|||||||
<query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="false">
|
<query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="false">
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
<code-editor content="ctrl.target.expr" datasource="ctrl.datasource" on-change="ctrl.refreshMetricData()"
|
<code-editor content="ctrl.target.expr" datasource="ctrl.datasource" on-change="ctrl.refreshMetricData()" get-completer="ctrl.getCompleter()"
|
||||||
get-completer="ctrl.getCompleter()" data-mode="prometheus" code-editor-focus="ctrl.isLastQuery">
|
data-mode="prometheus" code-editor-focus="ctrl.isLastQuery">
|
||||||
</code-editor>
|
</code-editor>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form max-width-26">
|
<div class="gf-form max-width-26">
|
||||||
<label class="gf-form-label width-8">Legend format</label>
|
<label class="gf-form-label width-8">Legend format</label>
|
||||||
<input type="text" class="gf-form-input" ng-model="ctrl.target.legendFormat"
|
<input type="text" class="gf-form-input" ng-model="ctrl.target.legendFormat" spellcheck='false' placeholder="legend format"
|
||||||
spellcheck='false' placeholder="legend format" data-min-length=0 data-items=1000
|
data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
|
||||||
ng-model-onblur ng-change="ctrl.refreshMetricData()">
|
</input>
|
||||||
</input>
|
<info-popover mode="right-absolute">
|
||||||
</div>
|
Controls the name of the time series, using name or pattern. For example {{hostname}} will be replaced with label value for
|
||||||
|
the label hostname.
|
||||||
|
</info-popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-6">Min step</label>
|
<label class="gf-form-label width-6">Min step</label>
|
||||||
<input type="text" class="gf-form-input width-8" ng-model="ctrl.target.interval"
|
<input type="text" class="gf-form-input width-8" ng-model="ctrl.target.interval" data-placement="right" spellcheck='false'
|
||||||
data-placement="right"
|
placeholder="{{ctrl.panelCtrl.interval}}" data-min-length=0 data-items=100 ng-model-onblur ng-change="ctrl.refreshMetricData()"
|
||||||
spellcheck='false'
|
/>
|
||||||
placeholder="{{ctrl.panelCtrl.interval}}"
|
<info-popover mode="right-absolute">
|
||||||
data-min-length=0 data-items=100
|
Leave blank for auto handling based on time range and panel width
|
||||||
ng-model-onblur
|
</info-popover>
|
||||||
ng-change="ctrl.refreshMetricData()"/>
|
</div>
|
||||||
<info-popover mode="right-absolute">
|
|
||||||
Leave blank for auto handling based on time range and panel width
|
|
||||||
</info-popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label">Resolution</label>
|
<label class="gf-form-label">Resolution</label>
|
||||||
<div class="gf-form-select-wrapper max-width-15">
|
<div class="gf-form-select-wrapper max-width-15">
|
||||||
<select ng-model="ctrl.target.intervalFactor" class="gf-form-input"
|
<select ng-model="ctrl.target.intervalFactor" class="gf-form-input" ng-options="r.factor as r.label for r in ctrl.resolutions"
|
||||||
ng-options="r.factor as r.label for r in ctrl.resolutions"
|
ng-change="ctrl.refreshMetricData()">
|
||||||
ng-change="ctrl.refreshMetricData()">
|
</select>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-6">Format as</label>
|
|
||||||
<div class="gf-form-select-wrapper width-8">
|
|
||||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats" ng-change="ctrl.refresh()"></select>
|
|
||||||
</div>
|
|
||||||
<gf-form-switch class="gf-form" label="Instant" label-class="width-5" checked="ctrl.target.instant" on-change="ctrl.refresh()">
|
|
||||||
</gf-form-switch>
|
|
||||||
<label class="gf-form-label">
|
|
||||||
<a href="{{ctrl.linkToPrometheus}}" target="_blank" bs-tooltip="'Link to Graph in Prometheus'">
|
|
||||||
<i class="fa fa-share-square-o"></i>
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-6">Format as</label>
|
||||||
|
<div class="gf-form-select-wrapper width-8">
|
||||||
|
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.format" ng-options="f.value as f.text for f in ctrl.formats"
|
||||||
|
ng-change="ctrl.refresh()"></select>
|
||||||
|
</div>
|
||||||
|
<gf-form-switch class="gf-form" label="Instant" label-class="width-5" checked="ctrl.target.instant" on-change="ctrl.refresh()">
|
||||||
|
</gf-form-switch>
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a href="{{ctrl.linkToPrometheus}}" target="_blank" bs-tooltip="'Link to Graph in Prometheus'">
|
||||||
|
<i class="fa fa-share-square-o"></i>
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form gf-form--grow">
|
||||||
|
<div class="gf-form-label gf-form-label--grow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</query-editor-row>
|
</query-editor-row>
|
||||||
|
@ -13,6 +13,10 @@
|
|||||||
"alerting": true,
|
"alerting": true,
|
||||||
"annotations": true,
|
"annotations": true,
|
||||||
|
|
||||||
|
"queryOptions": {
|
||||||
|
"minInterval": true
|
||||||
|
},
|
||||||
|
|
||||||
"info": {
|
"info": {
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Grafana Project",
|
"name": "Grafana Project",
|
||||||
|
@ -76,6 +76,24 @@ describe('PrometheusMetricFindQuery', function() {
|
|||||||
ctx.$rootScope.$apply();
|
ctx.$rootScope.$apply();
|
||||||
expect(results.length).to.be(3);
|
expect(results.length).to.be(3);
|
||||||
});
|
});
|
||||||
|
it('label_values(metric, resource) result should not contain empty string', function() {
|
||||||
|
response = {
|
||||||
|
status: "success",
|
||||||
|
data: [
|
||||||
|
{__name__: "metric", resource: "value1"},
|
||||||
|
{__name__: "metric", resource: "value2"},
|
||||||
|
{__name__: "metric", resource: ""}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response);
|
||||||
|
var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv);
|
||||||
|
pm.process().then(function(data) { results = data; });
|
||||||
|
ctx.$httpBackend.flush();
|
||||||
|
ctx.$rootScope.$apply();
|
||||||
|
expect(results.length).to.be(2);
|
||||||
|
expect(results[0].text).to.be("value1");
|
||||||
|
expect(results[1].text).to.be("value2");
|
||||||
|
});
|
||||||
it('metrics(metric.*) should generate metric name query', function() {
|
it('metrics(metric.*) should generate metric name query', function() {
|
||||||
response = {
|
response = {
|
||||||
status: "success",
|
status: "success",
|
||||||
|
Loading…
Reference in New Issue
Block a user