Merge remote-tracking branch 'upstream/master' into postgres-query-builder
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
.awcache
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
data*
|
||||
dist
|
||||
docker
|
||||
docs
|
||||
dump.rdb
|
||||
node_modules
|
||||
/local
|
||||
/tmp
|
||||
/vendor
|
||||
*.yml
|
||||
*.md
|
||||
/vendor
|
||||
/tmp
|
||||
@@ -44,7 +44,9 @@ docker-compose.yaml
|
||||
/conf/provisioning/**/custom.yaml
|
||||
profile.cov
|
||||
/grafana
|
||||
/local
|
||||
.notouch
|
||||
/Makefile.local
|
||||
/pkg/cmd/grafana-cli/grafana-cli
|
||||
/pkg/cmd/grafana-server/grafana-server
|
||||
/pkg/cmd/grafana-server/debug
|
||||
|
||||
+16
-1
@@ -7,12 +7,27 @@
|
||||
* **Prometheus**: Table columns order now changes when rearrange queries [#11690](https://github.com/grafana/grafana/issues/11690), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Variables**: Fix variable interpolation when using multiple formatting types [#11800](https://github.com/grafana/grafana/issues/11800), thx [@svenklemm](https://github.com/svenklemm)
|
||||
* **Dashboard**: Fix date selector styling for dark/light theme in time picker control [#11616](https://github.com/grafana/grafana/issues/11616)
|
||||
* **Discord**: Alert notification channel type for Discord, [#7964](https://github.com/grafana/grafana/issues/7964) thx [@jereksel](https://github.com/jereksel),
|
||||
* **InfluxDB**: Support SELECT queries in templating query, [#5013](https://github.com/grafana/grafana/issues/5013)
|
||||
* **Dashboard**: JSON Model under dashboard settings can now be updated & changes saved, [#1429](https://github.com/grafana/grafana/issues/1429), thx [@jereksel](https://github.com/jereksel)
|
||||
* **Security**: Fix XSS vulnerabilities in dashboard links [#11813](https://github.com/grafana/grafana/pull/11813)
|
||||
* **Singlestat**: Fix "time of last point" shows local time when dashboard timezone set to UTC [#10338](https://github.com/grafana/grafana/issues/10338)
|
||||
|
||||
# 5.1.1 (unreleased)
|
||||
# 5.1.3 (2018-05-16)
|
||||
|
||||
* **Scroll**: Graph panel / legend texts shifts on the left each time we move scrollbar on firefox [#11830](https://github.com/grafana/grafana/issues/11830)
|
||||
|
||||
# 5.1.2 (2018-05-09)
|
||||
|
||||
* **Database**: Fix MySql migration issue [#11862](https://github.com/grafana/grafana/issues/11862)
|
||||
* **Google Analytics**: Enable Google Analytics anonymizeIP setting for GDPR [#11656](https://github.com/grafana/grafana/pull/11656)
|
||||
|
||||
# 5.1.1 (2018-05-07)
|
||||
|
||||
* **LDAP**: LDAP login with MariaDB/MySQL database and dn>100 chars not possible [#11754](https://github.com/grafana/grafana/issues/11754)
|
||||
* **Build**: AppVeyor Windows build missing version and commit info [#11758](https://github.com/grafana/grafana/issues/11758)
|
||||
* **Scroll**: Scroll can't start in graphs on Chrome mobile [#11710](https://github.com/grafana/grafana/issues/11710)
|
||||
* **Units**: Revert renaming of unit key ppm [#11743](https://github.com/grafana/grafana/issues/11743)
|
||||
|
||||
# 5.1.0 (2018-04-26)
|
||||
|
||||
|
||||
Generated
+13
-1
@@ -111,6 +111,18 @@
|
||||
]
|
||||
revision = "270bc3860bb94dd3a3ffd047377d746c5e276726"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/facebookgo/inject"
|
||||
packages = ["."]
|
||||
revision = "cc1aa653e50f6a9893bcaef89e673e5b24e1e97b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/facebookgo/structtag"
|
||||
packages = ["."]
|
||||
revision = "217e25fb96916cc60332e399c9aa63f5c422ceed"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fatih/color"
|
||||
packages = ["."]
|
||||
@@ -649,6 +661,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "2bd5b309496d57e2189a1cc28f5c1c41398c19729ba0cf53c8cbb17ea3f706b5"
|
||||
inputs-digest = "bd54a1a836599d90b36d4ac1af56d716ef9ca5be4865e217bddd49e3d32a1997"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+10
-16
@@ -1,26 +1,20 @@
|
||||
# Roadmap (2018-02-22)
|
||||
# Roadmap (2018-05-06)
|
||||
|
||||
This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change.
|
||||
But it will give you an idea of our current vision and plan.
|
||||
|
||||
|
||||
### Short term (1-2 months)
|
||||
|
||||
- v5.1
|
||||
- Build speed improvements & integration test execution
|
||||
- Kubernetes friendly docker container
|
||||
- Enterprise LDAP
|
||||
- Provisioning workflow
|
||||
- MSSQL datasource
|
||||
- Elasticsearch alerting
|
||||
- Crossplatform builds
|
||||
- Backend service refactorings
|
||||
- Explore UI
|
||||
- First login registration view
|
||||
|
||||
### Mid term (2-4 months)
|
||||
|
||||
- v5.2
|
||||
- Azure monitor backend rewrite
|
||||
- Elasticsearch alerting
|
||||
- First login registration view
|
||||
- Backend plugins? (alert notifiers, auth)
|
||||
- Crossplatform builds
|
||||
- IFQL Initial support
|
||||
- Multi-Stat panel
|
||||
- React Panels
|
||||
- Templating Query Editor UI Plugin hook
|
||||
|
||||
### Long term (4 - 8 months)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# http://localhost:3000 (Grafana running locally)
|
||||
#
|
||||
# Please note that you'll need to change the root_url in the Grafana configuration:
|
||||
# root_url = %(protocol)s://%(domain)s:/grafana/
|
||||
# root_url = %(protocol)s://%(domain)s:10081/grafana/
|
||||
|
||||
apacheproxy:
|
||||
build: blocks/apache_proxy
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
CREATE LOGIN %%USER%% WITH PASSWORD = '%%PWD%%'
|
||||
GO
|
||||
|
||||
CREATE DATABASE %%DB%%;
|
||||
CREATE DATABASE %%DB%%
|
||||
ON
|
||||
( NAME = %%DB%%,
|
||||
FILENAME = '/var/opt/mssql/data/%%DB%%.mdf',
|
||||
SIZE = 500MB,
|
||||
MAXSIZE = 1000MB,
|
||||
FILEGROWTH = 100MB )
|
||||
LOG ON
|
||||
( NAME = %%DB%%_log,
|
||||
FILENAME = '/var/opt/mssql/data/%%DB%%_log.ldf',
|
||||
SIZE = 500MB,
|
||||
MAXSIZE = 1000MB,
|
||||
FILEGROWTH = 100MB );
|
||||
GO
|
||||
|
||||
USE %%DB%%;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
environment:
|
||||
ACCEPT_EULA: Y
|
||||
MSSQL_SA_PASSWORD: Password!
|
||||
MSSQL_PID: Express
|
||||
MSSQL_PID: Developer
|
||||
MSSQL_DATABASE: grafana
|
||||
MSSQL_USER: grafana
|
||||
MSSQL_PASSWORD: Password!
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# http://localhost:3000 (Grafana running locally)
|
||||
#
|
||||
# Please note that you'll need to change the root_url in the Grafana configuration:
|
||||
# root_url = %(protocol)s://%(domain)s:/grafana/
|
||||
# root_url = %(protocol)s://%(domain)s:10080/grafana/
|
||||
|
||||
nginxproxy:
|
||||
build: blocks/nginx_proxy
|
||||
|
||||
@@ -94,7 +94,7 @@ deleteDatasources:
|
||||
orgId: 1
|
||||
|
||||
# list of datasources to insert/update depending
|
||||
# whats available in the database
|
||||
# what's available in the database
|
||||
datasources:
|
||||
# <string, required> name of the datasource. Required
|
||||
- name: Graphite
|
||||
|
||||
@@ -11,7 +11,7 @@ weight = 3
|
||||
+++
|
||||
|
||||
|
||||
## Whats new in Grafana v4.1
|
||||
## What's new in Grafana v4.1
|
||||
- **Graph**: Support for shared tooltip on all graphs as you hover over one graph. [#1578](https://github.com/grafana/grafana/pull/1578), [#6274](https://github.com/grafana/grafana/pull/6274)
|
||||
- **Victorops**: Add VictorOps notification integration [#6411](https://github.com/grafana/grafana/issues/6411), thx [@ichekrygin](https://github.com/ichekrygin)
|
||||
- **Opsgenie**: Add OpsGenie notification integratiion [#6687](https://github.com/grafana/grafana/issues/6687), thx [@kylemcc](https://github.com/kylemcc)
|
||||
@@ -24,7 +24,7 @@ weight = 3
|
||||
|
||||
{{< imgbox max-width="60%" img="/img/docs/v41/shared_tooltip.gif" caption="Shared tooltip" >}}
|
||||
|
||||
Showing the tooltip on all panels at the same time has been a long standing request in Grafana and we are really happy to finally be able to release it.
|
||||
Showing the tooltip on all panels at the same time has been a long standing request in Grafana and we are really happy to finally be able to release it.
|
||||
You can enable/disable the shared tooltip from the dashboard settings menu or cycle between default, shared tooltip and shared crosshair by pressing `CTRL + O` or `CMD + O`.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
@@ -50,7 +50,7 @@ Panels with a help text available have a little indicator in the top left corner
|
||||
In Grafana 4.1.0 you can configure your Cloudwatch data source with `access key` and `secret key` directly in the data source configuration page.
|
||||
This enables people to use the Cloudwatch data source without having access to the filesystem where Grafana is running.
|
||||
|
||||
Once the `access key` and `secret key` have been saved the user will no longer be able to view them.
|
||||
Once the `access key` and `secret key` have been saved the user will no longer be able to view them.
|
||||
<div class="clearfix"></div>
|
||||
|
||||
## Upgrade & Breaking changes
|
||||
|
||||
@@ -10,7 +10,7 @@ parent = "whatsnew"
|
||||
weight = -1
|
||||
+++
|
||||
|
||||
## Whats new in Grafana v4.2
|
||||
## What's new in Grafana v4.2
|
||||
|
||||
Grafana v4.2 Beta is now [available for download](https://grafana.com/grafana/download/4.2.0).
|
||||
Just like the last release this one contains lots bug fixes and minor improvements.
|
||||
|
||||
@@ -64,7 +64,7 @@ This makes exploring and filtering Prometheus data much easier.
|
||||
* **Dataproxy**: Allow grafan to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250)
|
||||
* **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367)
|
||||
* **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739)
|
||||
* **Slack**: Allow images to be uploaded to slack when Token is precent [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8)
|
||||
* **Slack**: Allow images to be uploaded to slack when Token is present [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8)
|
||||
* **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn)
|
||||
* **Table**: Add support for displaying the timestamp with milliseconds [#9429](https://github.com/grafana/grafana/pull/9429), thx [@s1061123](https://github.com/s1061123)
|
||||
* **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo)
|
||||
|
||||
@@ -100,8 +100,8 @@ In the table below you can see some examples and you can find all different opti
|
||||
Filter Option | Example | Raw | Interpolated | Description
|
||||
------------ | ------------- | ------------- | ------------- | -------------
|
||||
`glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | Formats multi-value variable into a glob
|
||||
`regex` | ${servers:regex} | `'test.', 'test2'` | `(test\\.|test2)` | Formats multi-value variable into a regex string
|
||||
`pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.|test2` | Formats multi-value variable into a pipe-separated string
|
||||
`regex` | ${servers:regex} | `'test.', 'test2'` | <code>(test\.|test2)</code> | Formats multi-value variable into a regex string
|
||||
`pipe` | ${servers:pipe} | `'test.', 'test2'` | <code>test.|test2</code> | Formats multi-value variable into a pipe-separated string
|
||||
`csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string
|
||||
|
||||
## Improved workflow for provisioned dashboards
|
||||
@@ -122,4 +122,4 @@ More information in the [Provisioning documentation](/features/datasources/prome
|
||||
## 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.
|
||||
of new features, changes, and bug fixes.
|
||||
|
||||
@@ -106,6 +106,7 @@ Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"role": "Viewer",
|
||||
|
||||
@@ -380,6 +380,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
"role":"Viewer"
|
||||
}
|
||||
```
|
||||
Note: The api will only work when you pass the admin name and password
|
||||
to the request http url, like http://admin:admin@localhost:3000/api/orgs/1/users
|
||||
|
||||
**Example Response**:
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ server {
|
||||
```bash
|
||||
[server]
|
||||
domain = foo.bar
|
||||
root_url = %(protocol)s://%(domain)s:/grafana
|
||||
root_url = %(protocol)s://%(domain)s/grafana/
|
||||
```
|
||||
|
||||
#### Nginx configuration with sub path
|
||||
@@ -98,7 +98,7 @@ Given:
|
||||
```bash
|
||||
[server]
|
||||
domain = localhost:8080
|
||||
root_url = %(protocol)s://%(domain)s:/grafana
|
||||
root_url = %(protocol)s://%(domain)s/grafana/
|
||||
```
|
||||
|
||||
Create an Inbound Rule for the parent website (localhost:8080 in this example) in IIS Manager with the following settings:
|
||||
|
||||
@@ -93,8 +93,6 @@ Directory where grafana will automatically scan and look for plugins
|
||||
|
||||
### provisioning
|
||||
|
||||
> This feature is available in 5.0+
|
||||
|
||||
Folder that contains [provisioning](/administration/provisioning) config files that grafana will apply on startup. Dashboards will be reloaded when the json files changes
|
||||
|
||||
## [server]
|
||||
@@ -659,6 +657,10 @@ Set to `true` to enable auto sign up of users who do not exist in Grafana DB. De
|
||||
|
||||
Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header.
|
||||
|
||||
### headers
|
||||
|
||||
Used to define additional headers for `Name`, `Email` and/or `Login`, for example if the user's name is sent in the X-WEBAUTH-NAME header and their email address in the X-WEBAUTH-EMAIL header, set `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`.
|
||||
|
||||
<hr>
|
||||
|
||||
## [session]
|
||||
@@ -713,7 +715,7 @@ Analytics ID here. By default this feature is disabled.
|
||||
|
||||
## [dashboards]
|
||||
|
||||
### versions_to_keep (introduced in v5.0)
|
||||
### versions_to_keep
|
||||
|
||||
Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ weight = 1
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Stable for Debian-based Linux | [grafana_5.1.0_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0_amd64.deb)
|
||||
Stable for Debian-based Linux | [grafana_5.1.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.3_amd64.deb)
|
||||
<!--
|
||||
Beta for Debian-based Linux | [grafana_5.1.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0-beta1_amd64.deb)
|
||||
-->
|
||||
@@ -27,9 +27,9 @@ installation.
|
||||
|
||||
|
||||
```bash
|
||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0_amd64.deb
|
||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.3_amd64.deb
|
||||
sudo apt-get install -y adduser libfontconfig
|
||||
sudo dpkg -i grafana_5.1.0_amd64.deb
|
||||
sudo dpkg -i grafana_5.1.3_amd64.deb
|
||||
```
|
||||
|
||||
<!-- ## Install Latest Beta
|
||||
|
||||
@@ -15,7 +15,7 @@ weight = 2
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.0 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-1.x86_64.rpm)
|
||||
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.3-1.x86_64.rpm)
|
||||
<!--
|
||||
Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-beta1.x86_64.rpm)
|
||||
-->
|
||||
@@ -28,7 +28,7 @@ installation.
|
||||
You can install Grafana using Yum directly.
|
||||
|
||||
```bash
|
||||
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-1.x86_64.rpm
|
||||
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.3-1.x86_64.rpm
|
||||
```
|
||||
|
||||
<!-- ## Install Beta
|
||||
@@ -42,15 +42,15 @@ Or install manually using `rpm`.
|
||||
#### On CentOS / Fedora / Redhat:
|
||||
|
||||
```bash
|
||||
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-1.x86_64.rpm
|
||||
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.3-1.x86_64.rpm
|
||||
$ sudo yum install initscripts fontconfig
|
||||
$ sudo rpm -Uvh grafana-5.1.0-1.x86_64.rpm
|
||||
$ sudo rpm -Uvh grafana-5.1.3-1.x86_64.rpm
|
||||
```
|
||||
|
||||
#### On OpenSuse:
|
||||
|
||||
```bash
|
||||
$ sudo rpm -i --nodeps grafana-5.1.0-1.x86_64.rpm
|
||||
$ sudo rpm -i --nodeps grafana-5.1.3-1.x86_64.rpm
|
||||
```
|
||||
|
||||
## Install via YUM Repository
|
||||
|
||||
@@ -12,7 +12,7 @@ weight = 3
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Latest stable package for Windows | [grafana-5.1.0.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0.windows-x64.zip)
|
||||
Latest stable package for Windows | [grafana-5.1.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.3.windows-x64.zip)
|
||||
|
||||
<!--
|
||||
Latest beta package for Windows | [grafana.5.1.0-beta1.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.windows-x64.zip)
|
||||
|
||||
@@ -5,7 +5,7 @@ type = "docs"
|
||||
[menu.docs]
|
||||
name = "Developing App Plugins"
|
||||
parent = "developing"
|
||||
weight = 6
|
||||
weight = 4
|
||||
+++
|
||||
|
||||
# Grafana Apps
|
||||
|
||||
@@ -5,7 +5,7 @@ type = "docs"
|
||||
[menu.docs]
|
||||
name = "Developing Datasource Plugins"
|
||||
parent = "developing"
|
||||
weight = 6
|
||||
weight = 5
|
||||
+++
|
||||
|
||||
# Datasources
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
---
|
||||
page_title: Plugin panel
|
||||
page_description: Panel plugins for Grafana
|
||||
page_keywords: grafana, plugins, documentation
|
||||
---
|
||||
|
||||
|
||||
+++
|
||||
title = "Installing Plugins"
|
||||
title = "Developing Panel Plugins"
|
||||
keywords = ["grafana", "plugins", "panel", "documentation"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Developing Panel Plugins"
|
||||
parent = "developing"
|
||||
weight = 1
|
||||
weight = 4
|
||||
+++
|
||||
|
||||
|
||||
@@ -20,7 +15,21 @@ Panels are the main building blocks of dashboards.
|
||||
|
||||
## Panel development
|
||||
|
||||
Examples
|
||||
|
||||
### Scrolling
|
||||
The grafana dashboard framework controls the panel height. To enable a scrollbar within the panel the PanelCtrl needs to set the scrollable static variable:
|
||||
|
||||
```javascript
|
||||
export class MyPanelCtrl extends PanelCtrl {
|
||||
static scrollable = true;
|
||||
...
|
||||
```
|
||||
|
||||
In this case, make sure the template has a single `<div>...</div>` root. The plugin loader will modify that element adding a scrollbar.
|
||||
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
- [clock-panel](https://github.com/grafana/clock-panel)
|
||||
- [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts)
|
||||
|
||||
@@ -5,7 +5,7 @@ type = "docs"
|
||||
[menu.docs]
|
||||
name = "plugin.json Schema"
|
||||
parent = "developing"
|
||||
weight = 6
|
||||
weight = 8
|
||||
+++
|
||||
|
||||
# Plugin.json
|
||||
|
||||
@@ -50,6 +50,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 16,
|
||||
"version": 0,
|
||||
"links": []
|
||||
@@ -71,6 +72,7 @@ Each field in the dashboard JSON is explained below with its usage:
|
||||
| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
|
||||
| **templating** | templating metadata, see [templating section](#templating) for details |
|
||||
| **annotations** | annotations metadata, see [annotations section](#annotations) for details |
|
||||
| **refresh** | auto-refresh interval
|
||||
| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to said schema |
|
||||
| **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
|
||||
| **panels** | panels array, see below for detail. |
|
||||
|
||||
@@ -16,7 +16,7 @@ Example:
|
||||
- Parent site: http://localhost:8080
|
||||
- Grafana: http://localhost:3000
|
||||
|
||||
Grafana as a subpath: http://localhost:8080/grafana
|
||||
Grafana as a subpath: http://localhost:8080/grafana
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -33,7 +33,7 @@ Given that the subpath should be `grafana` and the parent site is `localhost:808
|
||||
```bash
|
||||
[server]
|
||||
domain = localhost:8080
|
||||
root_url = %(protocol)s://%(domain)s:/grafana
|
||||
root_url = %(protocol)s://%(domain)s/grafana/
|
||||
```
|
||||
|
||||
Restart the Grafana server after changing the config file.
|
||||
@@ -74,11 +74,11 @@ When navigating to the grafana url (`http://localhost:8080/grafana` in the examp
|
||||
|
||||
1. The `root_url` setting in the Grafana config file does not match the parent url with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files):
|
||||
|
||||
`; root_url = %(protocol)s://%(domain)s:/grafana`
|
||||
`; root_url = %(protocol)s://%(domain)s/grafana/`
|
||||
|
||||
2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS:
|
||||
|
||||
`root_url = %(protocol)s://%(domain)s:/grafana`
|
||||
`root_url = %(protocol)s://%(domain)s/grafana/`
|
||||
|
||||
pattern in Inbound Rule: `wrongsubpath(/)?(.*)`
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ weight = 10
|
||||
</a>
|
||||
<figcaption>
|
||||
<a href="https://youtu.be/FC13uhFRsVw?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" target="_blank" rel="noopener noreferrer">
|
||||
#3 Whats New In Grafana 2.0
|
||||
#3 What's New In Grafana 2.0
|
||||
</a>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
@@ -179,5 +179,8 @@
|
||||
"tether": "^1.4.0",
|
||||
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||
"tinycolor2": "^1.4.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"caniuse-db": "1.0.30000772"
|
||||
}
|
||||
}
|
||||
|
||||
+19
-10
@@ -26,9 +26,14 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&HTTPServer{})
|
||||
}
|
||||
|
||||
type HTTPServer struct {
|
||||
log log.Logger
|
||||
macaron *macaron.Macaron
|
||||
@@ -41,12 +46,14 @@ type HTTPServer struct {
|
||||
Bus bus.Bus `inject:""`
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Init() {
|
||||
func (hs *HTTPServer) Init() error {
|
||||
hs.log = log.New("http.server")
|
||||
hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Start(ctx context.Context) error {
|
||||
func (hs *HTTPServer) Run(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
hs.context = ctx
|
||||
@@ -57,17 +64,18 @@ func (hs *HTTPServer) Start(ctx context.Context) error {
|
||||
hs.streamManager.Run(ctx)
|
||||
|
||||
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
|
||||
hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
|
||||
hs.log.Info("HTTP Server Listen", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
|
||||
|
||||
hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron}
|
||||
|
||||
// handle http shutdown on server context done
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
// Hacky fix for race condition between ListenAndServe and Shutdown
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
if err := hs.httpSrv.Shutdown(context.Background()); err != nil {
|
||||
hs.log.Error("Failed to shutdown server", "error", err)
|
||||
}
|
||||
hs.log.Info("Stopped HTTP Server")
|
||||
}()
|
||||
|
||||
switch setting.Protocol {
|
||||
@@ -106,12 +114,6 @@ func (hs *HTTPServer) Start(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Shutdown(ctx context.Context) error {
|
||||
err := hs.httpSrv.Shutdown(ctx)
|
||||
hs.log.Info("Stopped HTTP server")
|
||||
return err
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
|
||||
if certfile == "" {
|
||||
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
|
||||
@@ -172,6 +174,7 @@ func (hs *HTTPServer) newMacaron() *macaron.Macaron {
|
||||
hs.mapStatic(m, route.Directory, "", pluginRoute)
|
||||
}
|
||||
|
||||
hs.mapStatic(m, setting.StaticRootPath, "build", "public/build")
|
||||
hs.mapStatic(m, setting.StaticRootPath, "", "public")
|
||||
hs.mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
|
||||
|
||||
@@ -239,6 +242,12 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string,
|
||||
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
}
|
||||
|
||||
if prefix == "public/build" {
|
||||
headers = func(c *macaron.Context) {
|
||||
c.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
}
|
||||
}
|
||||
|
||||
if setting.Env == setting.DEV {
|
||||
headers = func(c *macaron.Context) {
|
||||
c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache")
|
||||
|
||||
@@ -22,7 +22,9 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli
|
||||
Args: flag.Args(),
|
||||
})
|
||||
|
||||
sqlstore.NewEngine()
|
||||
engine := &sqlstore.SqlStore{}
|
||||
engine.Cfg = cfg
|
||||
engine.Init()
|
||||
|
||||
if err := command(cmd); err != nil {
|
||||
logger.Errorf("\n%s: ", color.RedString("Error"))
|
||||
|
||||
@@ -39,7 +39,6 @@ var enterprise 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 exitChan = make(chan int)
|
||||
|
||||
func main() {
|
||||
v := flag.Bool("v", false, "prints current version and exits")
|
||||
@@ -81,29 +80,20 @@ func main() {
|
||||
setting.Enterprise, _ = strconv.ParseBool(enterprise)
|
||||
|
||||
metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
|
||||
shutdownCompleted := make(chan int)
|
||||
|
||||
server := NewGrafanaServer()
|
||||
|
||||
go listenToSystemSignals(server, shutdownCompleted)
|
||||
go listenToSystemSignals(server)
|
||||
|
||||
go func() {
|
||||
code := 0
|
||||
if err := server.Start(); err != nil {
|
||||
log.Error2("Startup failed", "error", err)
|
||||
code = 1
|
||||
}
|
||||
err := server.Run()
|
||||
|
||||
exitChan <- code
|
||||
}()
|
||||
|
||||
code := <-shutdownCompleted
|
||||
log.Info2("Grafana shutdown completed.", "code", code)
|
||||
trace.Stop()
|
||||
log.Close()
|
||||
os.Exit(code)
|
||||
|
||||
server.Exit(err)
|
||||
}
|
||||
|
||||
func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) {
|
||||
var code int
|
||||
func listenToSystemSignals(server *GrafanaServerImpl) {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
ignoreChan := make(chan os.Signal, 1)
|
||||
|
||||
@@ -112,12 +102,6 @@ func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int
|
||||
|
||||
select {
|
||||
case sig := <-signalChan:
|
||||
trace.Stop() // Stops trace if profiling has been enabled
|
||||
server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
|
||||
shutdownCompleted <- 0
|
||||
case code = <-exitChan:
|
||||
trace.Stop() // Stops trace if profiling has been enabled
|
||||
server.Shutdown(code, "startup error")
|
||||
shutdownCompleted <- code
|
||||
server.Shutdown(fmt.Sprintf("System signal: %s", sig))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -16,19 +15,15 @@ import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
"github.com/grafana/grafana/pkg/tracing"
|
||||
|
||||
// self registering services
|
||||
_ "github.com/grafana/grafana/pkg/extensions"
|
||||
@@ -37,7 +32,10 @@ import (
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting"
|
||||
_ "github.com/grafana/grafana/pkg/services/cleanup"
|
||||
_ "github.com/grafana/grafana/pkg/services/notifications"
|
||||
_ "github.com/grafana/grafana/pkg/services/provisioning"
|
||||
_ "github.com/grafana/grafana/pkg/services/search"
|
||||
_ "github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
_ "github.com/grafana/grafana/pkg/tracing"
|
||||
)
|
||||
|
||||
func NewGrafanaServer() *GrafanaServerImpl {
|
||||
@@ -54,50 +52,36 @@ func NewGrafanaServer() *GrafanaServerImpl {
|
||||
}
|
||||
|
||||
type GrafanaServerImpl struct {
|
||||
context context.Context
|
||||
shutdownFn context.CancelFunc
|
||||
childRoutines *errgroup.Group
|
||||
log log.Logger
|
||||
cfg *setting.Cfg
|
||||
context context.Context
|
||||
shutdownFn context.CancelFunc
|
||||
childRoutines *errgroup.Group
|
||||
log log.Logger
|
||||
cfg *setting.Cfg
|
||||
shutdownReason string
|
||||
shutdownInProgress bool
|
||||
|
||||
RouteRegister api.RouteRegister `inject:""`
|
||||
HttpServer *api.HTTPServer `inject:""`
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Start() error {
|
||||
func (g *GrafanaServerImpl) Run() error {
|
||||
g.loadConfiguration()
|
||||
g.writePIDFile()
|
||||
|
||||
// initSql
|
||||
sqlstore.NewEngine() // TODO: this should return an error
|
||||
sqlstore.EnsureAdminUser()
|
||||
|
||||
login.Init()
|
||||
social.NewOAuthService()
|
||||
|
||||
if err := provisioning.Init(g.context, setting.HomePath, g.cfg.Raw); err != nil {
|
||||
return fmt.Errorf("Failed to provision Grafana from config. error: %v", err)
|
||||
}
|
||||
|
||||
tracingCloser, err := tracing.Init(g.cfg.Raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Tracing settings is not valid. error: %v", err)
|
||||
}
|
||||
defer tracingCloser.Close()
|
||||
|
||||
serviceGraph := inject.Graph{}
|
||||
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
|
||||
serviceGraph.Provide(&inject.Object{Value: g.cfg})
|
||||
serviceGraph.Provide(&inject.Object{Value: dashboards.NewProvisioningService()})
|
||||
serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
|
||||
serviceGraph.Provide(&inject.Object{Value: api.HTTPServer{}})
|
||||
|
||||
// self registered services
|
||||
services := registry.GetServices()
|
||||
|
||||
// Add all services to dependency graph
|
||||
for _, service := range services {
|
||||
serviceGraph.Provide(&inject.Object{Value: service})
|
||||
serviceGraph.Provide(&inject.Object{Value: service.Instance})
|
||||
}
|
||||
|
||||
serviceGraph.Provide(&inject.Object{Value: g})
|
||||
@@ -109,37 +93,56 @@ func (g *GrafanaServerImpl) Start() error {
|
||||
|
||||
// Init & start services
|
||||
for _, service := range services {
|
||||
if registry.IsDisabled(service) {
|
||||
if registry.IsDisabled(service.Instance) {
|
||||
continue
|
||||
}
|
||||
|
||||
g.log.Info("Initializing " + reflect.TypeOf(service).Elem().Name())
|
||||
g.log.Info("Initializing " + service.Name)
|
||||
|
||||
if err := service.Init(); err != nil {
|
||||
return fmt.Errorf("Service init failed %v", err)
|
||||
if err := service.Instance.Init(); err != nil {
|
||||
return fmt.Errorf("Service init failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start background services
|
||||
for index := range services {
|
||||
service, ok := services[index].(registry.BackgroundService)
|
||||
for _, srv := range services {
|
||||
// variable needed for accessing loop variable in function callback
|
||||
descriptor := srv
|
||||
service, ok := srv.Instance.(registry.BackgroundService)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if registry.IsDisabled(services[index]) {
|
||||
if registry.IsDisabled(descriptor.Instance) {
|
||||
continue
|
||||
}
|
||||
|
||||
g.childRoutines.Go(func() error {
|
||||
// Skip starting new service when shutting down
|
||||
// Can happen when service stop/return during startup
|
||||
if g.shutdownInProgress {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := service.Run(g.context)
|
||||
g.log.Info("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err)
|
||||
|
||||
// If error is not canceled then the service crashed
|
||||
if err != context.Canceled && err != nil {
|
||||
g.log.Error("Stopped "+descriptor.Name, "reason", err)
|
||||
} else {
|
||||
g.log.Info("Stopped "+descriptor.Name, "reason", err)
|
||||
}
|
||||
|
||||
// Mark that we are in shutdown mode
|
||||
// So more services are not started
|
||||
g.shutdownInProgress = true
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
sendSystemdNotification("READY=1")
|
||||
return g.startHttpServer()
|
||||
|
||||
return g.childRoutines.Wait()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) loadConfiguration() {
|
||||
@@ -158,28 +161,29 @@ func (g *GrafanaServerImpl) loadConfiguration() {
|
||||
g.cfg.LogConfigSources()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) startHttpServer() error {
|
||||
g.HttpServer.Init()
|
||||
|
||||
err := g.HttpServer.Start(g.context)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Fail to start server. error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
||||
g.log.Info("Shutdown started", "code", code, "reason", reason)
|
||||
func (g *GrafanaServerImpl) Shutdown(reason string) {
|
||||
g.log.Info("Shutdown started", "reason", reason)
|
||||
g.shutdownReason = reason
|
||||
g.shutdownInProgress = true
|
||||
|
||||
// call cancel func on root context
|
||||
g.shutdownFn()
|
||||
|
||||
// wait for child routines
|
||||
if err := g.childRoutines.Wait(); err != nil && err != context.Canceled {
|
||||
g.log.Error("Server shutdown completed", "error", err)
|
||||
g.childRoutines.Wait()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Exit(reason error) {
|
||||
// default exit code is 1
|
||||
code := 1
|
||||
|
||||
if reason == context.Canceled && g.shutdownReason != "" {
|
||||
reason = fmt.Errorf(g.shutdownReason)
|
||||
code = 0
|
||||
}
|
||||
|
||||
g.log.Error("Server shutdown", "reason", reason)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) writePIDFile() {
|
||||
|
||||
@@ -106,6 +106,15 @@ func (f Float) String() string {
|
||||
return fmt.Sprintf("%1.3f", f.Float64)
|
||||
}
|
||||
|
||||
// FullString returns float as string in full precision
|
||||
func (f Float) FullString() string {
|
||||
if !f.Valid {
|
||||
return "null"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%f", f.Float64)
|
||||
}
|
||||
|
||||
// SetValid changes this Float's value and also sets it to be non-null.
|
||||
func (f *Float) SetValid(n float64) {
|
||||
f.Float64 = n
|
||||
|
||||
+1
-1
@@ -349,7 +349,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
|
||||
|
||||
func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
|
||||
if name == "DN" {
|
||||
return result.Entries[0].DN
|
||||
return result.Entries[n].DN
|
||||
}
|
||||
for _, attr := range result.Entries[n].Attributes {
|
||||
if attr.Name == name {
|
||||
|
||||
@@ -53,6 +53,20 @@ func TestLdapAuther(t *testing.T) {
|
||||
So(result, ShouldEqual, user1)
|
||||
})
|
||||
|
||||
ldapAutherScenario("Given group match with different case", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
{GroupDN: "cn=users", OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userQueryReturns(user1)
|
||||
|
||||
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"CN=users"}})
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, user1)
|
||||
})
|
||||
|
||||
ldapAutherScenario("Given no existing grafana user", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LdapUserInfo struct {
|
||||
DN string
|
||||
FirstName string
|
||||
@@ -15,7 +19,7 @@ func (u *LdapUserInfo) isMemberOf(group string) bool {
|
||||
}
|
||||
|
||||
for _, member := range u.MemberOf {
|
||||
if member == group {
|
||||
if strings.EqualFold(member, group) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -111,6 +112,16 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, field := range []string{"Name", "Email", "Login"} {
|
||||
if setting.AuthProxyHeaders[field] == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if val := ctx.Req.Header.Get(setting.AuthProxyHeaders[field]); val != "" {
|
||||
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val)
|
||||
}
|
||||
}
|
||||
|
||||
// add/update user in grafana
|
||||
cmd := &m.UpsertUserCommand{
|
||||
ReqContext: ctx,
|
||||
|
||||
@@ -19,12 +19,13 @@ type SendEmailCommandSync struct {
|
||||
}
|
||||
|
||||
type SendWebhookSync struct {
|
||||
Url string
|
||||
User string
|
||||
Password string
|
||||
Body string
|
||||
HttpMethod string
|
||||
HttpHeader map[string]string
|
||||
Url string
|
||||
User string
|
||||
Password string
|
||||
Body string
|
||||
HttpMethod string
|
||||
HttpHeader map[string]string
|
||||
ContentType string
|
||||
}
|
||||
|
||||
type SendResetPasswordEmailCommand struct {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
@@ -79,6 +80,14 @@ func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSou
|
||||
qr.ErrorString = r.Error
|
||||
}
|
||||
|
||||
if r.MetaJson != "" {
|
||||
metaJson, err := simplejson.NewJson([]byte(r.MetaJson))
|
||||
if err != nil {
|
||||
tw.logger.Error("Error parsing JSON Meta field: " + err.Error())
|
||||
}
|
||||
qr.Meta = metaJson
|
||||
}
|
||||
|
||||
for _, s := range r.GetSeries() {
|
||||
points := tsdb.TimeSeriesPoints{}
|
||||
|
||||
|
||||
@@ -2,15 +2,35 @@ package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var services = []Service{}
|
||||
|
||||
func RegisterService(srv Service) {
|
||||
services = append(services, srv)
|
||||
type Descriptor struct {
|
||||
Name string
|
||||
Instance Service
|
||||
InitPriority Priority
|
||||
}
|
||||
|
||||
func GetServices() []Service {
|
||||
var services []*Descriptor
|
||||
|
||||
func RegisterService(instance Service) {
|
||||
services = append(services, &Descriptor{
|
||||
Name: reflect.TypeOf(instance).Elem().Name(),
|
||||
Instance: instance,
|
||||
InitPriority: Low,
|
||||
})
|
||||
}
|
||||
|
||||
func Register(descriptor *Descriptor) {
|
||||
services = append(services, descriptor)
|
||||
}
|
||||
|
||||
func GetServices() []*Descriptor {
|
||||
sort.Slice(services, func(i, j int) bool {
|
||||
return services[i].InitPriority > services[j].InitPriority
|
||||
})
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
@@ -27,7 +47,18 @@ type BackgroundService interface {
|
||||
Run(ctx context.Context) error
|
||||
}
|
||||
|
||||
type HasInitPriority interface {
|
||||
GetInitPriority() Priority
|
||||
}
|
||||
|
||||
func IsDisabled(srv Service) bool {
|
||||
canBeDisabled, ok := srv.(CanBeDisabled)
|
||||
return ok && canBeDisabled.IsDisabled()
|
||||
}
|
||||
|
||||
type Priority int
|
||||
|
||||
const (
|
||||
High Priority = 100
|
||||
Low Priority = 0
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
type AlertingService struct {
|
||||
execQueue chan *Job
|
||||
//clock clock.Clock
|
||||
ticker *Ticker
|
||||
@@ -28,20 +28,20 @@ type Engine struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&Engine{})
|
||||
registry.RegisterService(&AlertingService{})
|
||||
}
|
||||
|
||||
func NewEngine() *Engine {
|
||||
e := &Engine{}
|
||||
func NewEngine() *AlertingService {
|
||||
e := &AlertingService{}
|
||||
e.Init()
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Engine) IsDisabled() bool {
|
||||
func (e *AlertingService) IsDisabled() bool {
|
||||
return !setting.AlertingEnabled || !setting.ExecuteAlerts
|
||||
}
|
||||
|
||||
func (e *Engine) Init() error {
|
||||
func (e *AlertingService) Init() error {
|
||||
e.ticker = NewTicker(time.Now(), time.Second*0, clock.New())
|
||||
e.execQueue = make(chan *Job, 1000)
|
||||
e.scheduler = NewScheduler()
|
||||
@@ -52,7 +52,7 @@ func (e *Engine) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) Run(ctx context.Context) error {
|
||||
func (e *AlertingService) Run(ctx context.Context) error {
|
||||
alertGroup, ctx := errgroup.WithContext(ctx)
|
||||
alertGroup.Go(func() error { return e.alertingTicker(ctx) })
|
||||
alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
|
||||
@@ -61,7 +61,7 @@ func (e *Engine) Run(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
|
||||
func (e *AlertingService) alertingTicker(grafanaCtx context.Context) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Scheduler Panic: stopping alertingTicker", "error", err, "stack", log.Stack(1))
|
||||
@@ -86,7 +86,7 @@ func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
|
||||
func (e *AlertingService) runJobDispatcher(grafanaCtx context.Context) error {
|
||||
dispatcherGroup, alertCtx := errgroup.WithContext(grafanaCtx)
|
||||
|
||||
for {
|
||||
@@ -106,7 +106,7 @@ var (
|
||||
alertMaxAttempts = 3
|
||||
)
|
||||
|
||||
func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
|
||||
func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
@@ -141,7 +141,7 @@ func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
|
||||
func (e *AlertingService) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
|
||||
job.Running = false
|
||||
close(cancelChan)
|
||||
for cancelFn := range cancelChan {
|
||||
@@ -150,7 +150,7 @@ func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
|
||||
func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"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/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "discord",
|
||||
Name: "Discord",
|
||||
Description: "Sends notifications to Discord",
|
||||
Factory: NewDiscordNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Discord settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Webhook URL</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.url" placeholder="Discord webhook URL"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
url := model.Settings.Get("url").MustString()
|
||||
if url == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find webhook url property in settings"}
|
||||
}
|
||||
|
||||
return &DiscordNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
WebhookURL: url,
|
||||
log: log.New("alerting.notifier.discord"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type DiscordNotifier struct {
|
||||
NotifierBase
|
||||
WebhookURL string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending alert notification to", "webhook_url", this.WebhookURL)
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed get rule link", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("username", "Grafana")
|
||||
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
|
||||
for _, evt := range evalContext.EvalMatches {
|
||||
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"name": evt.Metric,
|
||||
"value": evt.Value.FullString(),
|
||||
"inline": true,
|
||||
})
|
||||
}
|
||||
|
||||
footer := map[string]interface{}{
|
||||
"text": "Grafana v" + setting.BuildVersion,
|
||||
"icon_url": "https://grafana.com/assets/img/fav32.png",
|
||||
}
|
||||
|
||||
color, _ := strconv.ParseInt(strings.TrimLeft(evalContext.GetStateModel().Color, "#"), 16, 0)
|
||||
|
||||
embed := simplejson.New()
|
||||
embed.Set("title", evalContext.GetNotificationTitle())
|
||||
//Discord takes integer for color
|
||||
embed.Set("color", color)
|
||||
embed.Set("url", ruleUrl)
|
||||
embed.Set("description", evalContext.Rule.Message)
|
||||
embed.Set("type", "rich")
|
||||
embed.Set("fields", fields)
|
||||
embed.Set("footer", footer)
|
||||
|
||||
var image map[string]interface{}
|
||||
var embeddedImage = false
|
||||
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
image = map[string]interface{}{
|
||||
"url": evalContext.ImagePublicUrl,
|
||||
}
|
||||
embed.Set("image", image)
|
||||
} else {
|
||||
image = map[string]interface{}{
|
||||
"url": "attachment://graph.png",
|
||||
}
|
||||
embed.Set("image", image)
|
||||
embeddedImage = true
|
||||
}
|
||||
|
||||
bodyJSON.Set("embeds", []interface{}{embed})
|
||||
|
||||
json, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
content_type := "application/json"
|
||||
|
||||
var body []byte
|
||||
|
||||
if embeddedImage {
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
w := multipart.NewWriter(&b)
|
||||
|
||||
f, err := os.Open(evalContext.ImageOnDiskPath)
|
||||
|
||||
if err != nil {
|
||||
this.log.Error("Can't open graph file", err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
fw, err := w.CreateFormField("payload_json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = fw.Write([]byte(string(json))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fw, err = w.CreateFormFile("file", "graph.png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = io.Copy(fw, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Close()
|
||||
|
||||
body = b.Bytes()
|
||||
content_type = w.FormDataContentType()
|
||||
|
||||
} else {
|
||||
body = json
|
||||
}
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: this.WebhookURL,
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
ContentType: content_type,
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send notification to Discord", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDiscordNotifier(t *testing.T) {
|
||||
Convey("Telegram notifier tests", t, func() {
|
||||
|
||||
Convey("Parsing alert notification from settings", func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "discord_testing",
|
||||
Type: "discord",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewDiscordNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("settings should trigger incident", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "https://web.hook/"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "discord_testing",
|
||||
Type: "discord",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewDiscordNotifier(model)
|
||||
discordNotifier := not.(*DiscordNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(discordNotifier.Name, ShouldEqual, "discord_testing")
|
||||
So(discordNotifier.Type, ShouldEqual, "discord")
|
||||
So(discordNotifier.WebhookURL, ShouldEqual, "https://web.hook/")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -104,12 +104,13 @@ func (ns *NotificationService) Run(ctx context.Context) error {
|
||||
|
||||
func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
|
||||
return ns.sendWebRequestSync(ctx, &Webhook{
|
||||
Url: cmd.Url,
|
||||
User: cmd.User,
|
||||
Password: cmd.Password,
|
||||
Body: cmd.Body,
|
||||
HttpMethod: cmd.HttpMethod,
|
||||
HttpHeader: cmd.HttpHeader,
|
||||
Url: cmd.Url,
|
||||
User: cmd.User,
|
||||
Password: cmd.Password,
|
||||
Body: cmd.Body,
|
||||
HttpMethod: cmd.HttpMethod,
|
||||
HttpHeader: cmd.HttpHeader,
|
||||
ContentType: cmd.ContentType,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,13 @@ import (
|
||||
)
|
||||
|
||||
type Webhook struct {
|
||||
Url string
|
||||
User string
|
||||
Password string
|
||||
Body string
|
||||
HttpMethod string
|
||||
HttpHeader map[string]string
|
||||
Url string
|
||||
User string
|
||||
Password string
|
||||
Body string
|
||||
HttpMethod string
|
||||
HttpHeader map[string]string
|
||||
ContentType string
|
||||
}
|
||||
|
||||
var netTransport = &http.Transport{
|
||||
@@ -48,8 +49,13 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *
|
||||
return err
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
if webhook.ContentType == "" {
|
||||
webhook.ContentType = "application/json"
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", webhook.ContentType)
|
||||
request.Header.Add("User-Agent", "Grafana")
|
||||
|
||||
if webhook.User != "" && webhook.Password != "" {
|
||||
request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
|
||||
|
||||
parsedDashboards, err := cr.parseConfigs(file)
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(parsedDashboards) > 0 {
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
simpleDashboardConfig = "./test-configs/dashboards-from-disk"
|
||||
oldVersion = "./test-configs/version-0"
|
||||
brokenConfigs = "./test-configs/broken-configs"
|
||||
simpleDashboardConfig = "./testdata/test-configs/dashboards-from-disk"
|
||||
oldVersion = "./testdata/test-configs/version-0"
|
||||
brokenConfigs = "./testdata/test-configs/broken-configs"
|
||||
)
|
||||
|
||||
func TestDashboardsAsConfig(t *testing.T) {
|
||||
|
||||
@@ -10,19 +10,16 @@ import (
|
||||
type DashboardProvisioner struct {
|
||||
cfgReader *configReader
|
||||
log log.Logger
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) {
|
||||
func NewDashboardProvisioner(configDirectory string) *DashboardProvisioner {
|
||||
log := log.New("provisioning.dashboard")
|
||||
d := &DashboardProvisioner{
|
||||
cfgReader: &configReader{path: configDirectory, log: log},
|
||||
log: log,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
err := d.Provision(ctx)
|
||||
return d, err
|
||||
return d
|
||||
}
|
||||
|
||||
func (provider *DashboardProvisioner) Provision(ctx context.Context) error {
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
defaultDashboards = "./test-dashboards/folder-one"
|
||||
brokenDashboards = "./test-dashboards/broken-dashboards"
|
||||
oneDashboard = "./test-dashboards/one-dashboard"
|
||||
containingId = "./test-dashboards/containing-id"
|
||||
defaultDashboards = "./testdata/test-dashboards/folder-one"
|
||||
brokenDashboards = "./testdata/test-dashboards/broken-dashboards"
|
||||
oneDashboard = "./testdata/test-dashboards/one-dashboard"
|
||||
containingId = "./testdata/test-dashboards/containing-id"
|
||||
|
||||
fakeService *fakeDashboardProvisioningService
|
||||
)
|
||||
|
||||
@@ -2,30 +2,40 @@ package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
|
||||
ini "gopkg.in/ini.v1"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func Init(ctx context.Context, homePath string, cfg *ini.File) error {
|
||||
provisioningPath := makeAbsolute(cfg.Section("paths").Key("provisioning").String(), homePath)
|
||||
func init() {
|
||||
registry.RegisterService(&ProvisioningService{})
|
||||
}
|
||||
|
||||
datasourcePath := path.Join(provisioningPath, "datasources")
|
||||
type ProvisioningService struct {
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
}
|
||||
|
||||
func (ps *ProvisioningService) Init() error {
|
||||
datasourcePath := path.Join(ps.Cfg.ProvisioningPath, "datasources")
|
||||
if err := datasources.Provision(datasourcePath); err != nil {
|
||||
return fmt.Errorf("Datasource provisioning error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *ProvisioningService) Run(ctx context.Context) error {
|
||||
dashboardPath := path.Join(ps.Cfg.ProvisioningPath, "dashboards")
|
||||
dashProvisioner := dashboards.NewDashboardProvisioner(dashboardPath)
|
||||
|
||||
if err := dashProvisioner.Provision(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dashboardPath := path.Join(provisioningPath, "dashboards")
|
||||
_, err := dashboards.Provision(ctx, dashboardPath)
|
||||
return err
|
||||
}
|
||||
|
||||
func makeAbsolute(path string, root string) string {
|
||||
if filepath.IsAbs(path) {
|
||||
return path
|
||||
}
|
||||
return filepath.Join(root, path)
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
|
||||
builder.Write(" ORDER BY name ASC")
|
||||
|
||||
if query.Limit != 0 {
|
||||
builder.Write(" LIMIT ?", query.Limit)
|
||||
builder.Write(dialect.Limit(query.Limit))
|
||||
}
|
||||
|
||||
alerts := make([]*m.AlertListItemDTO, 0)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -21,7 +20,6 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
}
|
||||
|
||||
err := GetAlertNotifications(cmd)
|
||||
fmt.Printf("error %v", err)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result, ShouldBeNil)
|
||||
})
|
||||
|
||||
@@ -50,7 +50,7 @@ func (r *SqlAnnotationRepo) ensureTagsExist(sess *DBSession, tags []*models.Tag)
|
||||
var existingTag models.Tag
|
||||
|
||||
// check if it exists
|
||||
if exists, err := sess.Table("tag").Where("`key`=? AND `value`=?", tag.Key, tag.Value).Get(&existingTag); err != nil {
|
||||
if exists, err := sess.Table("tag").Where(dialect.Quote("key")+"=? AND "+dialect.Quote("value")+"=?", tag.Key, tag.Value).Get(&existingTag); err != nil {
|
||||
return nil, err
|
||||
} else if exists {
|
||||
tag.Id = existingTag.Id
|
||||
@@ -146,7 +146,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
params = append(params, query.OrgId)
|
||||
|
||||
if query.AnnotationId != 0 {
|
||||
fmt.Print("annotation query")
|
||||
// fmt.Print("annotation query")
|
||||
sql.WriteString(` AND annotation.id = ?`)
|
||||
params = append(params, query.AnnotationId)
|
||||
}
|
||||
@@ -193,10 +193,10 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
tags := models.ParseTagPairs(query.Tags)
|
||||
for _, tag := range tags {
|
||||
if tag.Value == "" {
|
||||
keyValueFilters = append(keyValueFilters, "(tag.key = ?)")
|
||||
keyValueFilters = append(keyValueFilters, "(tag."+dialect.Quote("key")+" = ?)")
|
||||
params = append(params, tag.Key)
|
||||
} else {
|
||||
keyValueFilters = append(keyValueFilters, "(tag.key = ? AND tag.value = ?)")
|
||||
keyValueFilters = append(keyValueFilters, "(tag."+dialect.Quote("key")+" = ? AND tag."+dialect.Quote("value")+" = ?)")
|
||||
params = append(params, tag.Key, tag.Value)
|
||||
}
|
||||
}
|
||||
@@ -219,7 +219,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
query.Limit = 100
|
||||
}
|
||||
|
||||
sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
|
||||
sql.WriteString(" ORDER BY epoch DESC" + dialect.Limit(query.Limit))
|
||||
|
||||
items := make([]*annotations.ItemDTO, 0)
|
||||
|
||||
|
||||
@@ -10,12 +10,18 @@ import (
|
||||
)
|
||||
|
||||
func TestSavingTags(t *testing.T) {
|
||||
InitTestDB(t)
|
||||
|
||||
Convey("Testing annotation saving/loading", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
repo := SqlAnnotationRepo{}
|
||||
|
||||
Convey("Can save tags", func() {
|
||||
Reset(func() {
|
||||
_, err := x.Exec("DELETE FROM annotation_tag WHERE 1=1")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
tagPairs := []*models.Tag{
|
||||
{Key: "outage"},
|
||||
{Key: "type", Value: "outage"},
|
||||
@@ -31,12 +37,19 @@ func TestSavingTags(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnnotations(t *testing.T) {
|
||||
Convey("Testing annotation saving/loading", t, func() {
|
||||
InitTestDB(t)
|
||||
InitTestDB(t)
|
||||
|
||||
Convey("Testing annotation saving/loading", t, func() {
|
||||
repo := SqlAnnotationRepo{}
|
||||
|
||||
Convey("Can save annotation", func() {
|
||||
Reset(func() {
|
||||
_, err := x.Exec("DELETE FROM annotation WHERE 1=1")
|
||||
So(err, ShouldBeNil)
|
||||
_, err = x.Exec("DELETE FROM annotation_tag WHERE 1=1")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
annotation := &annotations.Item{
|
||||
OrgId: 1,
|
||||
UserId: 1,
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -110,46 +109,43 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteExpiredSnapshots(t *testing.T) {
|
||||
Convey("Testing dashboard snapshots clean up", t, func() {
|
||||
x := InitTestDB(t)
|
||||
sqlstore := InitTestDB(t)
|
||||
|
||||
Convey("Testing dashboard snapshots clean up", t, func() {
|
||||
setting.SnapShotRemoveExpired = true
|
||||
|
||||
notExpiredsnapshot := createTestSnapshot(x, "key1", 1000)
|
||||
createTestSnapshot(x, "key2", -1000)
|
||||
createTestSnapshot(x, "key3", -1000)
|
||||
notExpiredsnapshot := createTestSnapshot(sqlstore, "key1", 48000)
|
||||
createTestSnapshot(sqlstore, "key2", -1200)
|
||||
createTestSnapshot(sqlstore, "key3", -1200)
|
||||
|
||||
Convey("Clean up old dashboard snapshots", func() {
|
||||
err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{})
|
||||
So(err, ShouldBeNil)
|
||||
err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := m.GetDashboardSnapshotsQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN},
|
||||
}
|
||||
err = SearchDashboardSnapshots(&query)
|
||||
So(err, ShouldBeNil)
|
||||
query := m.GetDashboardSnapshotsQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN},
|
||||
}
|
||||
err = SearchDashboardSnapshots(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Key, ShouldEqual, notExpiredsnapshot.Key)
|
||||
})
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Key, ShouldEqual, notExpiredsnapshot.Key)
|
||||
|
||||
Convey("Don't delete anything if there are no expired snapshots", func() {
|
||||
err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{})
|
||||
So(err, ShouldBeNil)
|
||||
err = DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := m.GetDashboardSnapshotsQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN},
|
||||
}
|
||||
SearchDashboardSnapshots(&query)
|
||||
query = m.GetDashboardSnapshotsQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN},
|
||||
}
|
||||
SearchDashboardSnapshots(&query)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
})
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Key, ShouldEqual, notExpiredsnapshot.Key)
|
||||
})
|
||||
}
|
||||
|
||||
func createTestSnapshot(x *xorm.Engine, key string, expires int64) *m.DashboardSnapshot {
|
||||
func createTestSnapshot(sqlstore *SqlStore, key string, expires int64) *m.DashboardSnapshot {
|
||||
cmd := m.CreateDashboardSnapshotCommand{
|
||||
Key: key,
|
||||
DeleteKey: "delete" + key,
|
||||
@@ -164,9 +160,11 @@ func createTestSnapshot(x *xorm.Engine, key string, expires int64) *m.DashboardS
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Set expiry date manually - to be able to create expired snapshots
|
||||
expireDate := time.Now().Add(time.Second * time.Duration(expires))
|
||||
_, err = x.Exec("update dashboard_snapshot set expires = ? where "+dialect.Quote("key")+" = ?", expireDate, key)
|
||||
So(err, ShouldBeNil)
|
||||
if expires < 0 {
|
||||
expireDate := time.Now().Add(time.Second * time.Duration(expires))
|
||||
_, err = sqlstore.engine.Exec("UPDATE dashboard_snapshot SET expires = ? WHERE id = ?", expireDate, cmd.Result.Id)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
return cmd.Result
|
||||
}
|
||||
|
||||
@@ -86,10 +86,7 @@ func addAnnotationMig(mg *Migrator) {
|
||||
// clear alert text
|
||||
//
|
||||
updateTextFieldSql := "UPDATE annotation SET TEXT = '' WHERE alert_id > 0"
|
||||
mg.AddMigration("Update alert annotations and set TEXT to empty", new(RawSqlMigration).
|
||||
Sqlite(updateTextFieldSql).
|
||||
Postgres(updateTextFieldSql).
|
||||
Mysql(updateTextFieldSql))
|
||||
mg.AddMigration("Update alert annotations and set TEXT to empty", NewRawSqlMigration(updateTextFieldSql))
|
||||
|
||||
//
|
||||
// Add a 'created' & 'updated' column
|
||||
@@ -111,8 +108,5 @@ func addAnnotationMig(mg *Migrator) {
|
||||
// Convert epoch saved as seconds to miliseconds
|
||||
//
|
||||
updateEpochSql := "UPDATE annotation SET epoch = (epoch*1000) where epoch < 9999999999"
|
||||
mg.AddMigration("Convert existing annotations from seconds to milliseconds", new(RawSqlMigration).
|
||||
Sqlite(updateEpochSql).
|
||||
Postgres(updateEpochSql).
|
||||
Mysql(updateEpochSql))
|
||||
mg.AddMigration("Convert existing annotations from seconds to milliseconds", NewRawSqlMigration(updateEpochSql))
|
||||
}
|
||||
|
||||
@@ -45,8 +45,5 @@ INSERT INTO dashboard_acl
|
||||
(-1,-1, 2,'Editor','2017-06-20','2017-06-20')
|
||||
`
|
||||
|
||||
mg.AddMigration("save default acl rules in dashboard_acl table", new(RawSqlMigration).
|
||||
Sqlite(rawSQL).
|
||||
Postgres(rawSQL).
|
||||
Mysql(rawSQL))
|
||||
mg.AddMigration("save default acl rules in dashboard_acl table", NewRawSqlMigration(rawSQL))
|
||||
}
|
||||
|
||||
@@ -90,9 +90,7 @@ func addDashboardMigration(mg *Migrator) {
|
||||
mg.AddMigration("drop table dashboard_v1", NewDropTableMigration("dashboard_v1"))
|
||||
|
||||
// change column type of dashboard.data
|
||||
mg.AddMigration("alter dashboard.data to mediumtext v1", new(RawSqlMigration).
|
||||
Sqlite("SELECT 0 WHERE 0;").
|
||||
Postgres("SELECT 0;").
|
||||
mg.AddMigration("alter dashboard.data to mediumtext v1", NewRawSqlMigration("").
|
||||
Mysql("ALTER TABLE dashboard MODIFY data MEDIUMTEXT;"))
|
||||
|
||||
// add column to store updater of a dashboard
|
||||
@@ -157,7 +155,7 @@ func addDashboardMigration(mg *Migrator) {
|
||||
Name: "uid", Type: DB_NVarchar, Length: 40, Nullable: true,
|
||||
}))
|
||||
|
||||
mg.AddMigration("Update uid column values in dashboard", new(RawSqlMigration).
|
||||
mg.AddMigration("Update uid column values in dashboard", NewRawSqlMigration("").
|
||||
Sqlite("UPDATE dashboard SET uid=printf('%09d',id) WHERE uid IS NULL;").
|
||||
Postgres("UPDATE dashboard SET uid=lpad('' || id,9,'0') WHERE uid IS NULL;").
|
||||
Mysql("UPDATE dashboard SET uid=lpad(id,9,'0') WHERE uid IS NULL;"))
|
||||
|
||||
@@ -50,9 +50,7 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
|
||||
addTableIndicesMigrations(mg, "v5", snapshotV5)
|
||||
|
||||
// change column type of dashboard
|
||||
mg.AddMigration("alter dashboard_snapshot to mediumtext v2", new(RawSqlMigration).
|
||||
Sqlite("SELECT 0 WHERE 0;").
|
||||
Postgres("SELECT 0;").
|
||||
mg.AddMigration("alter dashboard_snapshot to mediumtext v2", NewRawSqlMigration("").
|
||||
Mysql("ALTER TABLE dashboard_snapshot MODIFY dashboard MEDIUMTEXT;"))
|
||||
|
||||
mg.AddMigration("Update dashboard_snapshot table charset", NewTableCharsetMigration("dashboard_snapshot", []*Column{
|
||||
|
||||
@@ -28,10 +28,7 @@ func addDashboardVersionMigration(mg *Migrator) {
|
||||
|
||||
// before new dashboards where created with version 0, now they are always inserted with version 1
|
||||
const setVersionTo1WhereZeroSQL = `UPDATE dashboard SET version = 1 WHERE version = 0`
|
||||
mg.AddMigration("Set dashboard version to 1 where 0", new(RawSqlMigration).
|
||||
Sqlite(setVersionTo1WhereZeroSQL).
|
||||
Postgres(setVersionTo1WhereZeroSQL).
|
||||
Mysql(setVersionTo1WhereZeroSQL))
|
||||
mg.AddMigration("Set dashboard version to 1 where 0", NewRawSqlMigration(setVersionTo1WhereZeroSQL))
|
||||
|
||||
const rawSQL = `INSERT INTO dashboard_version
|
||||
(
|
||||
@@ -54,14 +51,9 @@ SELECT
|
||||
'',
|
||||
dashboard.data
|
||||
FROM dashboard;`
|
||||
mg.AddMigration("save existing dashboard data in dashboard_version table v1", new(RawSqlMigration).
|
||||
Sqlite(rawSQL).
|
||||
Postgres(rawSQL).
|
||||
Mysql(rawSQL))
|
||||
mg.AddMigration("save existing dashboard data in dashboard_version table v1", NewRawSqlMigration(rawSQL))
|
||||
|
||||
// change column type of dashboard_version.data
|
||||
mg.AddMigration("alter dashboard_version.data to mediumtext v1", new(RawSqlMigration).
|
||||
Sqlite("SELECT 0 WHERE 0;").
|
||||
Postgres("SELECT 0;").
|
||||
mg.AddMigration("alter dashboard_version.data to mediumtext v1", NewRawSqlMigration("").
|
||||
Mysql("ALTER TABLE dashboard_version MODIFY data MEDIUMTEXT;"))
|
||||
}
|
||||
|
||||
@@ -122,10 +122,7 @@ func addDataSourceMigration(mg *Migrator) {
|
||||
}))
|
||||
|
||||
const setVersionToOneWhereZero = `UPDATE data_source SET version = 1 WHERE version = 0`
|
||||
mg.AddMigration("Update initial version to 1", new(RawSqlMigration).
|
||||
Sqlite(setVersionToOneWhereZero).
|
||||
Postgres(setVersionToOneWhereZero).
|
||||
Mysql(setVersionToOneWhereZero))
|
||||
mg.AddMigration("Update initial version to 1", NewRawSqlMigration(setVersionToOneWhereZero))
|
||||
|
||||
mg.AddMigration("Add read_only data column", NewAddColumnMigration(tableV2, &Column{
|
||||
Name: "read_only", Type: DB_Bool, Nullable: true,
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestMigrations(t *testing.T) {
|
||||
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
sqlutil.CleanDB(x)
|
||||
NewDialect(x).CleanDB()
|
||||
|
||||
_, err = x.SQL(sql).Get(&r)
|
||||
So(err, ShouldNotBeNil)
|
||||
@@ -39,7 +39,7 @@ func TestMigrations(t *testing.T) {
|
||||
has, err := x.SQL(sql).Get(&r)
|
||||
So(err, ShouldBeNil)
|
||||
So(has, ShouldBeTrue)
|
||||
expectedMigrations := mg.MigrationsCount() - 2 //we currently skip to migrations. We should rewrite skipped migrations to write in the log as well. until then we have to keep this
|
||||
expectedMigrations := mg.MigrationsCount() //we currently skip to migrations. We should rewrite skipped migrations to write in the log as well. until then we have to keep this
|
||||
So(r.Count, ShouldEqual, expectedMigrations)
|
||||
|
||||
mg = NewMigrator(x)
|
||||
|
||||
@@ -48,27 +48,6 @@ func addOrgMigrations(mg *Migrator) {
|
||||
mg.AddMigration("create org_user table v1", NewAddTableMigration(orgUserV1))
|
||||
addTableIndicesMigrations(mg, "v1", orgUserV1)
|
||||
|
||||
//------- copy data from old table-------------------
|
||||
mg.AddMigration("copy data account to org", NewCopyTableDataMigration("org", "account", map[string]string{
|
||||
"id": "id",
|
||||
"version": "version",
|
||||
"name": "name",
|
||||
"created": "created",
|
||||
"updated": "updated",
|
||||
}).IfTableExists("account"))
|
||||
|
||||
mg.AddMigration("copy data account_user to org_user", NewCopyTableDataMigration("org_user", "account_user", map[string]string{
|
||||
"id": "id",
|
||||
"org_id": "account_id",
|
||||
"user_id": "user_id",
|
||||
"role": "role",
|
||||
"created": "created",
|
||||
"updated": "updated",
|
||||
}).IfTableExists("account_user"))
|
||||
|
||||
mg.AddMigration("Drop old table account", NewDropTableMigration("account"))
|
||||
mg.AddMigration("Drop old table account_user", NewDropTableMigration("account_user"))
|
||||
|
||||
mg.AddMigration("Update org table charset", NewTableCharsetMigration("org", []*Column{
|
||||
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "address1", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
@@ -85,8 +64,5 @@ func addOrgMigrations(mg *Migrator) {
|
||||
}))
|
||||
|
||||
const migrateReadOnlyViewersToViewers = `UPDATE org_user SET role = 'Viewer' WHERE role = 'Read Only Editor'`
|
||||
mg.AddMigration("Migrate all Read Only Viewers to Viewers", new(RawSqlMigration).
|
||||
Sqlite(migrateReadOnlyViewersToViewers).
|
||||
Postgres(migrateReadOnlyViewersToViewers).
|
||||
Mysql(migrateReadOnlyViewersToViewers))
|
||||
mg.AddMigration("Migrate all Read Only Viewers to Viewers", NewRawSqlMigration(migrateReadOnlyViewersToViewers))
|
||||
}
|
||||
|
||||
@@ -22,8 +22,7 @@ func addUserAuthMigrations(mg *Migrator) {
|
||||
// add indices
|
||||
addTableIndicesMigrations(mg, "v1", userAuthV1)
|
||||
|
||||
mg.AddMigration("alter user_auth.auth_id to length 255", new(RawSqlMigration).
|
||||
Sqlite("SELECT 0 WHERE 0;").
|
||||
Postgres("ALTER TABLE user_auth ALTER COLUMN auth_id TYPE VARCHAR(255);").
|
||||
Mysql("ALTER TABLE user_auth MODIFY auth_id VARCHAR(255);"))
|
||||
mg.AddMigration("alter user_auth.auth_id to length 190", NewRawSqlMigration("").
|
||||
Postgres("ALTER TABLE user_auth ALTER COLUMN auth_id TYPE VARCHAR(190);").
|
||||
Mysql("ALTER TABLE user_auth MODIFY auth_id VARCHAR(190);"))
|
||||
}
|
||||
|
||||
@@ -15,48 +15,9 @@ type Column struct {
|
||||
}
|
||||
|
||||
func (col *Column) String(d Dialect) string {
|
||||
sql := d.QuoteStr() + col.Name + d.QuoteStr() + " "
|
||||
|
||||
sql += d.SqlType(col) + " "
|
||||
|
||||
if col.IsPrimaryKey {
|
||||
sql += "PRIMARY KEY "
|
||||
if col.IsAutoIncrement {
|
||||
sql += d.AutoIncrStr() + " "
|
||||
}
|
||||
}
|
||||
|
||||
if d.ShowCreateNull() {
|
||||
if col.Nullable {
|
||||
sql += "NULL "
|
||||
} else {
|
||||
sql += "NOT NULL "
|
||||
}
|
||||
}
|
||||
|
||||
if col.Default != "" {
|
||||
sql += "DEFAULT " + col.Default + " "
|
||||
}
|
||||
|
||||
return sql
|
||||
return d.ColString(col)
|
||||
}
|
||||
|
||||
func (col *Column) StringNoPk(d Dialect) string {
|
||||
sql := d.QuoteStr() + col.Name + d.QuoteStr() + " "
|
||||
|
||||
sql += d.SqlType(col) + " "
|
||||
|
||||
if d.ShowCreateNull() {
|
||||
if col.Nullable {
|
||||
sql += "NULL "
|
||||
} else {
|
||||
sql += "NOT NULL "
|
||||
}
|
||||
}
|
||||
|
||||
if col.Default != "" {
|
||||
sql += "DEFAULT " + d.Default(col) + " "
|
||||
}
|
||||
|
||||
return sql
|
||||
return d.ColStringNoPk(col)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ package migrator
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
type Dialect interface {
|
||||
DriverName() string
|
||||
QuoteStr() string
|
||||
Quote(string) string
|
||||
AndStr() string
|
||||
AutoIncrStr() string
|
||||
@@ -31,16 +32,29 @@ type Dialect interface {
|
||||
TableCheckSql(tableName string) (string, []interface{})
|
||||
RenameTable(oldName string, newName string) string
|
||||
UpdateTableSql(tableName string, columns []*Column) string
|
||||
|
||||
ColString(*Column) string
|
||||
ColStringNoPk(*Column) string
|
||||
|
||||
Limit(limit int64) string
|
||||
LimitOffset(limit int64, offset int64) string
|
||||
|
||||
PreInsertId(table string, sess *xorm.Session) error
|
||||
PostInsertId(table string, sess *xorm.Session) error
|
||||
|
||||
CleanDB() error
|
||||
NoOpSql() string
|
||||
}
|
||||
|
||||
func NewDialect(name string) Dialect {
|
||||
func NewDialect(engine *xorm.Engine) Dialect {
|
||||
name := engine.DriverName()
|
||||
switch name {
|
||||
case MYSQL:
|
||||
return NewMysqlDialect()
|
||||
return NewMysqlDialect(engine)
|
||||
case SQLITE:
|
||||
return NewSqlite3Dialect()
|
||||
return NewSqlite3Dialect(engine)
|
||||
case POSTGRES:
|
||||
return NewPostgresDialect()
|
||||
return NewPostgresDialect(engine)
|
||||
}
|
||||
|
||||
panic("Unsupported database type: " + name)
|
||||
@@ -48,6 +62,7 @@ func NewDialect(name string) Dialect {
|
||||
|
||||
type BaseDialect struct {
|
||||
dialect Dialect
|
||||
engine *xorm.Engine
|
||||
driverName string
|
||||
}
|
||||
|
||||
@@ -100,9 +115,12 @@ func (b *BaseDialect) CreateTableSql(table *Table) string {
|
||||
}
|
||||
|
||||
if len(pkList) > 1 {
|
||||
sql += "PRIMARY KEY ( "
|
||||
sql += b.dialect.Quote(strings.Join(pkList, b.dialect.Quote(",")))
|
||||
sql += " ), "
|
||||
quotedCols := []string{}
|
||||
for _, col := range pkList {
|
||||
quotedCols = append(quotedCols, b.dialect.Quote(col))
|
||||
}
|
||||
|
||||
sql += "PRIMARY KEY ( " + strings.Join(quotedCols, ",") + " ), "
|
||||
}
|
||||
|
||||
sql = sql[:len(sql)-2] + ")"
|
||||
@@ -127,9 +145,12 @@ func (db *BaseDialect) CreateIndexSql(tableName string, index *Index) string {
|
||||
|
||||
idxName := index.XName(tableName)
|
||||
|
||||
return fmt.Sprintf("CREATE%s INDEX %v ON %v (%v);", unique,
|
||||
quote(idxName), quote(tableName),
|
||||
quote(strings.Join(index.Cols, quote(","))))
|
||||
quotedCols := []string{}
|
||||
for _, col := range index.Cols {
|
||||
quotedCols = append(quotedCols, db.dialect.Quote(col))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("CREATE%s INDEX %v ON %v (%v);", unique, quote(idxName), quote(tableName), strings.Join(quotedCols, ","))
|
||||
}
|
||||
|
||||
func (db *BaseDialect) QuoteColList(cols []string) string {
|
||||
@@ -168,3 +189,74 @@ func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
|
||||
func (db *BaseDialect) UpdateTableSql(tableName string, columns []*Column) string {
|
||||
return "-- NOT REQUIRED"
|
||||
}
|
||||
|
||||
func (db *BaseDialect) ColString(col *Column) string {
|
||||
sql := db.dialect.Quote(col.Name) + " "
|
||||
|
||||
sql += db.dialect.SqlType(col) + " "
|
||||
|
||||
if col.IsPrimaryKey {
|
||||
sql += "PRIMARY KEY "
|
||||
if col.IsAutoIncrement {
|
||||
sql += db.dialect.AutoIncrStr() + " "
|
||||
}
|
||||
}
|
||||
|
||||
if db.dialect.ShowCreateNull() {
|
||||
if col.Nullable {
|
||||
sql += "NULL "
|
||||
} else {
|
||||
sql += "NOT NULL "
|
||||
}
|
||||
}
|
||||
|
||||
if col.Default != "" {
|
||||
sql += "DEFAULT " + db.dialect.Default(col) + " "
|
||||
}
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
func (db *BaseDialect) ColStringNoPk(col *Column) string {
|
||||
sql := db.dialect.Quote(col.Name) + " "
|
||||
|
||||
sql += db.dialect.SqlType(col) + " "
|
||||
|
||||
if db.dialect.ShowCreateNull() {
|
||||
if col.Nullable {
|
||||
sql += "NULL "
|
||||
} else {
|
||||
sql += "NOT NULL "
|
||||
}
|
||||
}
|
||||
|
||||
if col.Default != "" {
|
||||
sql += "DEFAULT " + db.dialect.Default(col) + " "
|
||||
}
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
func (db *BaseDialect) Limit(limit int64) string {
|
||||
return fmt.Sprintf(" LIMIT %d", limit)
|
||||
}
|
||||
|
||||
func (db *BaseDialect) LimitOffset(limit int64, offset int64) string {
|
||||
return fmt.Sprintf(" LIMIT %d OFFSET %d", limit, offset)
|
||||
}
|
||||
|
||||
func (db *BaseDialect) PreInsertId(table string, sess *xorm.Session) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *BaseDialect) PostInsertId(table string, sess *xorm.Session) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *BaseDialect) CleanDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *BaseDialect) NoOpSql() string {
|
||||
return "SELECT 0;"
|
||||
}
|
||||
|
||||
@@ -24,37 +24,58 @@ func (m *MigrationBase) GetCondition() MigrationCondition {
|
||||
type RawSqlMigration struct {
|
||||
MigrationBase
|
||||
|
||||
sqlite string
|
||||
mysql string
|
||||
postgres string
|
||||
sql map[string]string
|
||||
}
|
||||
|
||||
func NewRawSqlMigration(sql string) *RawSqlMigration {
|
||||
m := &RawSqlMigration{}
|
||||
if sql != "" {
|
||||
m.Default(sql)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *RawSqlMigration) Sql(dialect Dialect) string {
|
||||
switch dialect.DriverName() {
|
||||
case MYSQL:
|
||||
return m.mysql
|
||||
case SQLITE:
|
||||
return m.sqlite
|
||||
case POSTGRES:
|
||||
return m.postgres
|
||||
if m.sql != nil {
|
||||
if val := m.sql[dialect.DriverName()]; val != "" {
|
||||
return val
|
||||
}
|
||||
|
||||
if val := m.sql["default"]; val != "" {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
panic("db type not supported")
|
||||
return dialect.NoOpSql()
|
||||
}
|
||||
|
||||
func (m *RawSqlMigration) Set(dialect string, sql string) *RawSqlMigration {
|
||||
if m.sql == nil {
|
||||
m.sql = make(map[string]string)
|
||||
}
|
||||
|
||||
m.sql[dialect] = sql
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *RawSqlMigration) Default(sql string) *RawSqlMigration {
|
||||
return m.Set("default", sql)
|
||||
}
|
||||
|
||||
func (m *RawSqlMigration) Sqlite(sql string) *RawSqlMigration {
|
||||
m.sqlite = sql
|
||||
return m
|
||||
return m.Set(SQLITE, sql)
|
||||
}
|
||||
|
||||
func (m *RawSqlMigration) Mysql(sql string) *RawSqlMigration {
|
||||
m.mysql = sql
|
||||
return m
|
||||
return m.Set(MYSQL, sql)
|
||||
}
|
||||
|
||||
func (m *RawSqlMigration) Postgres(sql string) *RawSqlMigration {
|
||||
m.postgres = sql
|
||||
return m
|
||||
return m.Set(POSTGRES, sql)
|
||||
}
|
||||
|
||||
func (m *RawSqlMigration) Mssql(sql string) *RawSqlMigration {
|
||||
return m.Set(MSSQL, sql)
|
||||
}
|
||||
|
||||
type AddColumnMigration struct {
|
||||
|
||||
@@ -31,7 +31,7 @@ func NewMigrator(engine *xorm.Engine) *Migrator {
|
||||
mg.x = engine
|
||||
mg.Logger = log.New("migrator")
|
||||
mg.migrations = make([]Migration, 0)
|
||||
mg.dialect = NewDialect(mg.x.DriverName())
|
||||
mg.dialect = NewDialect(mg.x)
|
||||
return mg
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
|
||||
sql, args := condition.Sql(mg.dialect)
|
||||
results, err := sess.SQL(sql).Query(args...)
|
||||
if err != nil || len(results) == 0 {
|
||||
mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
|
||||
mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id())
|
||||
return sess.Rollback()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
type Mysql struct {
|
||||
BaseDialect
|
||||
}
|
||||
|
||||
func NewMysqlDialect() *Mysql {
|
||||
func NewMysqlDialect(engine *xorm.Engine) *Mysql {
|
||||
d := Mysql{}
|
||||
d.BaseDialect.dialect = &d
|
||||
d.BaseDialect.engine = engine
|
||||
d.BaseDialect.driverName = MYSQL
|
||||
return &d
|
||||
}
|
||||
@@ -24,10 +28,6 @@ func (db *Mysql) Quote(name string) string {
|
||||
return "`" + name + "`"
|
||||
}
|
||||
|
||||
func (db *Mysql) QuoteStr() string {
|
||||
return "`"
|
||||
}
|
||||
|
||||
func (db *Mysql) AutoIncrStr() string {
|
||||
return "AUTO_INCREMENT"
|
||||
}
|
||||
@@ -105,3 +105,23 @@ func (db *Mysql) UpdateTableSql(tableName string, columns []*Column) string {
|
||||
|
||||
return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
|
||||
}
|
||||
|
||||
func (db *Mysql) CleanDB() error {
|
||||
tables, _ := db.engine.DBMetas()
|
||||
sess := db.engine.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
for _, table := range tables {
|
||||
if _, err := sess.Exec("set foreign_key_checks = 0"); err != nil {
|
||||
return fmt.Errorf("failed to disable foreign key checks")
|
||||
}
|
||||
if _, err := sess.Exec("drop table " + table.Name + " ;"); err != nil {
|
||||
return fmt.Errorf("failed to delete table: %v, err: %v", table.Name, err)
|
||||
}
|
||||
if _, err := sess.Exec("set foreign_key_checks = 1"); err != nil {
|
||||
return fmt.Errorf("failed to disable foreign key checks")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,15 +4,18 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
type Postgres struct {
|
||||
BaseDialect
|
||||
}
|
||||
|
||||
func NewPostgresDialect() *Postgres {
|
||||
func NewPostgresDialect(engine *xorm.Engine) *Postgres {
|
||||
d := Postgres{}
|
||||
d.BaseDialect.dialect = &d
|
||||
d.BaseDialect.engine = engine
|
||||
d.BaseDialect.driverName = POSTGRES
|
||||
return &d
|
||||
}
|
||||
@@ -25,10 +28,6 @@ func (db *Postgres) Quote(name string) string {
|
||||
return "\"" + name + "\""
|
||||
}
|
||||
|
||||
func (db *Postgres) QuoteStr() string {
|
||||
return "\""
|
||||
}
|
||||
|
||||
func (b *Postgres) LikeStr() string {
|
||||
return "ILIKE"
|
||||
}
|
||||
@@ -117,8 +116,23 @@ func (db *Postgres) UpdateTableSql(tableName string, columns []*Column) string {
|
||||
var statements = []string{}
|
||||
|
||||
for _, col := range columns {
|
||||
statements = append(statements, "ALTER "+db.QuoteStr()+col.Name+db.QuoteStr()+" TYPE "+db.SqlType(col))
|
||||
statements = append(statements, "ALTER "+db.Quote(col.Name)+" TYPE "+db.SqlType(col))
|
||||
}
|
||||
|
||||
return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
|
||||
}
|
||||
|
||||
func (db *Postgres) CleanDB() error {
|
||||
sess := db.engine.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if _, err := sess.Exec("DROP SCHEMA public CASCADE;"); err != nil {
|
||||
return fmt.Errorf("Failed to drop schema public")
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("CREATE SCHEMA public;"); err != nil {
|
||||
return fmt.Errorf("Failed to create schema public")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package migrator
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
type Sqlite3 struct {
|
||||
BaseDialect
|
||||
}
|
||||
|
||||
func NewSqlite3Dialect() *Sqlite3 {
|
||||
func NewSqlite3Dialect(engine *xorm.Engine) *Sqlite3 {
|
||||
d := Sqlite3{}
|
||||
d.BaseDialect.dialect = &d
|
||||
d.BaseDialect.engine = engine
|
||||
d.BaseDialect.driverName = SQLITE
|
||||
return &d
|
||||
}
|
||||
@@ -21,10 +26,6 @@ func (db *Sqlite3) Quote(name string) string {
|
||||
return "`" + name + "`"
|
||||
}
|
||||
|
||||
func (db *Sqlite3) QuoteStr() string {
|
||||
return "`"
|
||||
}
|
||||
|
||||
func (db *Sqlite3) AutoIncrStr() string {
|
||||
return "AUTOINCREMENT"
|
||||
}
|
||||
@@ -77,3 +78,7 @@ func (db *Sqlite3) DropIndexSql(tableName string, index *Index) string {
|
||||
idxName := index.XName(tableName)
|
||||
return fmt.Sprintf("DROP INDEX %v", quote(idxName))
|
||||
}
|
||||
|
||||
func (db *Sqlite3) CleanDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const (
|
||||
POSTGRES = "postgres"
|
||||
SQLITE = "sqlite3"
|
||||
MYSQL = "mysql"
|
||||
MSSQL = "mssql"
|
||||
)
|
||||
|
||||
type Migration interface {
|
||||
|
||||
@@ -64,7 +64,7 @@ func UpdatePlaylist(cmd *m.UpdatePlaylistCommand) error {
|
||||
Interval: playlist.Interval,
|
||||
}
|
||||
|
||||
_, err := x.ID(cmd.Id).Cols("id", "name", "interval").Update(&playlist)
|
||||
_, err := x.ID(cmd.Id).Cols("name", "interval").Update(&playlist)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -43,6 +43,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
|
||||
Name: "TestOrg",
|
||||
UserId: 1,
|
||||
}
|
||||
|
||||
err := CreateOrg(&userCmd)
|
||||
So(err, ShouldBeNil)
|
||||
orgId = userCmd.Result.Id
|
||||
|
||||
@@ -92,7 +92,7 @@ func (sb *SearchBuilder) ToSql() (string, []interface{}) {
|
||||
LEFT OUTER JOIN dashboard folder on folder.id = dashboard.folder_id
|
||||
LEFT OUTER JOIN dashboard_tag on dashboard.id = dashboard_tag.dashboard_id`)
|
||||
|
||||
sb.sql.WriteString(" ORDER BY dashboard.title ASC LIMIT 5000")
|
||||
sb.sql.WriteString(" ORDER BY dashboard.title ASC" + dialect.Limit(5000))
|
||||
|
||||
return sb.sql.String(), sb.params
|
||||
}
|
||||
@@ -135,12 +135,11 @@ func (sb *SearchBuilder) buildTagQuery() {
|
||||
// this ends the inner select (tag filtered part)
|
||||
sb.sql.WriteString(`
|
||||
GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ?
|
||||
LIMIT ?) as ids
|
||||
ORDER BY dashboard.id` + dialect.Limit(int64(sb.limit)) + `) as ids
|
||||
INNER JOIN dashboard on ids.id = dashboard.id
|
||||
`)
|
||||
|
||||
sb.params = append(sb.params, len(sb.tags))
|
||||
sb.params = append(sb.params, sb.limit)
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) buildMainQuery() {
|
||||
@@ -153,8 +152,7 @@ func (sb *SearchBuilder) buildMainQuery() {
|
||||
sb.sql.WriteString(` WHERE `)
|
||||
sb.buildSearchWhereClause()
|
||||
|
||||
sb.sql.WriteString(` LIMIT ?) as ids INNER JOIN dashboard on ids.id = dashboard.id `)
|
||||
sb.params = append(sb.params, sb.limit)
|
||||
sb.sql.WriteString(` ORDER BY dashboard.title` + dialect.Limit(int64(sb.limit)) + `) as ids INNER JOIN dashboard on ids.id = dashboard.id `)
|
||||
}
|
||||
|
||||
func (sb *SearchBuilder) buildSearchWhereClause() {
|
||||
|
||||
@@ -4,13 +4,10 @@ import (
|
||||
"testing"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestSearchBuilder(t *testing.T) {
|
||||
dialect = migrator.NewDialect("sqlite3")
|
||||
|
||||
Convey("Testing building a search", t, func() {
|
||||
signedInUser := &m.SignedInUser{
|
||||
OrgId: 1,
|
||||
@@ -23,7 +20,7 @@ func TestSearchBuilder(t *testing.T) {
|
||||
sql, params := sb.IsStarred().WithTitle("test").ToSql()
|
||||
So(sql, ShouldStartWith, "SELECT")
|
||||
So(sql, ShouldContainSubstring, "INNER JOIN dashboard on ids.id = dashboard.id")
|
||||
So(sql, ShouldEndWith, "ORDER BY dashboard.title ASC LIMIT 5000")
|
||||
So(sql, ShouldContainSubstring, "ORDER BY dashboard.title ASC")
|
||||
So(len(params), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
|
||||
@@ -31,7 +28,7 @@ func TestSearchBuilder(t *testing.T) {
|
||||
sql, params := sb.WithTags([]string{"tag1", "tag2"}).ToSql()
|
||||
So(sql, ShouldStartWith, "SELECT")
|
||||
So(sql, ShouldContainSubstring, "LEFT OUTER JOIN dashboard_tag")
|
||||
So(sql, ShouldEndWith, "ORDER BY dashboard.title ASC LIMIT 5000")
|
||||
So(sql, ShouldContainSubstring, "ORDER BY dashboard.title ASC")
|
||||
So(len(params), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
@@ -67,3 +68,23 @@ func inTransactionWithRetry(callback dbTransactionFunc, retry int) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sess *DBSession) InsertId(bean interface{}) (int64, error) {
|
||||
table := sess.DB().Mapper.Obj2Table(getTypeName(bean))
|
||||
|
||||
dialect.PreInsertId(table, sess.Session)
|
||||
|
||||
id, err := sess.Session.InsertOne(bean)
|
||||
|
||||
dialect.PostInsertId(table, sess.Session)
|
||||
|
||||
return id, err
|
||||
}
|
||||
|
||||
func getTypeName(bean interface{}) (res string) {
|
||||
t := reflect.TypeOf(bean)
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
return t.Name()
|
||||
}
|
||||
|
||||
+169
-146
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
@@ -20,7 +21,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/go-xorm/xorm"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
@@ -28,39 +28,72 @@ import (
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/mssql"
|
||||
)
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type, Host, Name, User, Pwd, Path, SslMode string
|
||||
CaCertPath string
|
||||
ClientKeyPath string
|
||||
ClientCertPath string
|
||||
ServerCertName string
|
||||
MaxOpenConn int
|
||||
MaxIdleConn int
|
||||
ConnMaxLifetime int
|
||||
}
|
||||
|
||||
var (
|
||||
x *xorm.Engine
|
||||
dialect migrator.Dialect
|
||||
|
||||
HasEngine bool
|
||||
|
||||
DbCfg DatabaseConfig
|
||||
|
||||
UseSQLite3 bool
|
||||
sqlog log.Logger = log.New("sqlstore")
|
||||
sqlog log.Logger = log.New("sqlstore")
|
||||
)
|
||||
|
||||
func EnsureAdminUser() {
|
||||
func init() {
|
||||
registry.Register(®istry.Descriptor{
|
||||
Name: "SqlStore",
|
||||
Instance: &SqlStore{},
|
||||
InitPriority: registry.High,
|
||||
})
|
||||
}
|
||||
|
||||
type SqlStore struct {
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
|
||||
dbCfg DatabaseConfig
|
||||
engine *xorm.Engine
|
||||
log log.Logger
|
||||
skipEnsureAdmin bool
|
||||
}
|
||||
|
||||
func (ss *SqlStore) Init() error {
|
||||
ss.log = log.New("sqlstore")
|
||||
ss.readConfig()
|
||||
|
||||
engine, err := ss.getEngine()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Fail to connect to database: %v", err)
|
||||
}
|
||||
|
||||
ss.engine = engine
|
||||
|
||||
// temporarily still set global var
|
||||
x = engine
|
||||
dialect = migrator.NewDialect(x)
|
||||
migrator := migrator.NewMigrator(x)
|
||||
migrations.AddMigrations(migrator)
|
||||
|
||||
if err := migrator.Start(); err != nil {
|
||||
return fmt.Errorf("Migration failed err: %v", err)
|
||||
}
|
||||
|
||||
// Init repo instances
|
||||
annotations.SetRepository(&SqlAnnotationRepo{})
|
||||
|
||||
// ensure admin user
|
||||
if ss.skipEnsureAdmin {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ss.ensureAdminUser()
|
||||
}
|
||||
|
||||
func (ss *SqlStore) ensureAdminUser() error {
|
||||
statsQuery := m.GetSystemStatsQuery{}
|
||||
|
||||
if err := bus.Dispatch(&statsQuery); err != nil {
|
||||
log.Fatal(3, "Could not determine if admin user exists: %v", err)
|
||||
return
|
||||
fmt.Errorf("Could not determine if admin user exists: %v", err)
|
||||
}
|
||||
|
||||
if statsQuery.Result.Users > 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := m.CreateUserCommand{}
|
||||
@@ -70,105 +103,89 @@ func EnsureAdminUser() {
|
||||
cmd.IsAdmin = true
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
log.Error(3, "Failed to create default admin user", err)
|
||||
return
|
||||
return fmt.Errorf("Failed to create admin user: %v", err)
|
||||
}
|
||||
|
||||
log.Info("Created default admin user: %v", setting.AdminUser)
|
||||
}
|
||||
ss.log.Info("Created default admin user: %v", setting.AdminUser)
|
||||
|
||||
func NewEngine() *xorm.Engine {
|
||||
x, err := getEngine()
|
||||
|
||||
if err != nil {
|
||||
sqlog.Crit("Fail to connect to database", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = SetEngine(x)
|
||||
|
||||
if err != nil {
|
||||
sqlog.Error("Fail to initialize orm engine", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
func SetEngine(engine *xorm.Engine) (err error) {
|
||||
x = engine
|
||||
dialect = migrator.NewDialect(x.DriverName())
|
||||
|
||||
migrator := migrator.NewMigrator(x)
|
||||
migrations.AddMigrations(migrator)
|
||||
|
||||
if err := migrator.Start(); err != nil {
|
||||
return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err)
|
||||
}
|
||||
|
||||
// Init repo instances
|
||||
annotations.SetRepository(&SqlAnnotationRepo{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEngine() (*xorm.Engine, error) {
|
||||
LoadConfig()
|
||||
func (ss *SqlStore) buildConnectionString() (string, error) {
|
||||
cnnstr := ss.dbCfg.ConnectionString
|
||||
|
||||
cnnstr := ""
|
||||
switch DbCfg.Type {
|
||||
case "mysql":
|
||||
// special case used by integration tests
|
||||
if cnnstr != "" {
|
||||
return cnnstr, nil
|
||||
}
|
||||
|
||||
switch ss.dbCfg.Type {
|
||||
case migrator.MYSQL:
|
||||
protocol := "tcp"
|
||||
if strings.HasPrefix(DbCfg.Host, "/") {
|
||||
if strings.HasPrefix(ss.dbCfg.Host, "/") {
|
||||
protocol = "unix"
|
||||
}
|
||||
|
||||
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true",
|
||||
DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name)
|
||||
ss.dbCfg.User, ss.dbCfg.Pwd, protocol, ss.dbCfg.Host, ss.dbCfg.Name)
|
||||
|
||||
if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" {
|
||||
tlsCert, err := makeCert("custom", DbCfg)
|
||||
if ss.dbCfg.SslMode == "true" || ss.dbCfg.SslMode == "skip-verify" {
|
||||
tlsCert, err := makeCert("custom", ss.dbCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
mysql.RegisterTLSConfig("custom", tlsCert)
|
||||
cnnstr += "&tls=custom"
|
||||
}
|
||||
case "postgres":
|
||||
case migrator.POSTGRES:
|
||||
var host, port = "127.0.0.1", "5432"
|
||||
fields := strings.Split(DbCfg.Host, ":")
|
||||
fields := strings.Split(ss.dbCfg.Host, ":")
|
||||
if len(fields) > 0 && len(strings.TrimSpace(fields[0])) > 0 {
|
||||
host = fields[0]
|
||||
}
|
||||
if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 {
|
||||
port = fields[1]
|
||||
}
|
||||
if DbCfg.Pwd == "" {
|
||||
DbCfg.Pwd = "''"
|
||||
if ss.dbCfg.Pwd == "" {
|
||||
ss.dbCfg.Pwd = "''"
|
||||
}
|
||||
if DbCfg.User == "" {
|
||||
DbCfg.User = "''"
|
||||
if ss.dbCfg.User == "" {
|
||||
ss.dbCfg.User = "''"
|
||||
}
|
||||
cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode, DbCfg.ClientCertPath, DbCfg.ClientKeyPath, DbCfg.CaCertPath)
|
||||
case "sqlite3":
|
||||
if !filepath.IsAbs(DbCfg.Path) {
|
||||
DbCfg.Path = filepath.Join(setting.DataPath, DbCfg.Path)
|
||||
cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", ss.dbCfg.User, ss.dbCfg.Pwd, host, port, ss.dbCfg.Name, ss.dbCfg.SslMode, ss.dbCfg.ClientCertPath, ss.dbCfg.ClientKeyPath, ss.dbCfg.CaCertPath)
|
||||
case migrator.SQLITE:
|
||||
// special case for tests
|
||||
if !filepath.IsAbs(ss.dbCfg.Path) {
|
||||
ss.dbCfg.Path = filepath.Join(setting.DataPath, ss.dbCfg.Path)
|
||||
}
|
||||
os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm)
|
||||
cnnstr = "file:" + DbCfg.Path + "?cache=shared&mode=rwc"
|
||||
os.MkdirAll(path.Dir(ss.dbCfg.Path), os.ModePerm)
|
||||
cnnstr = "file:" + ss.dbCfg.Path + "?cache=shared&mode=rwc"
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown database type: %s", DbCfg.Type)
|
||||
return "", fmt.Errorf("Unknown database type: %s", ss.dbCfg.Type)
|
||||
}
|
||||
|
||||
sqlog.Info("Initializing DB", "dbtype", DbCfg.Type)
|
||||
engine, err := xorm.NewEngine(DbCfg.Type, cnnstr)
|
||||
return cnnstr, nil
|
||||
}
|
||||
|
||||
func (ss *SqlStore) getEngine() (*xorm.Engine, error) {
|
||||
connectionString, err := ss.buildConnectionString()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
engine.SetMaxOpenConns(DbCfg.MaxOpenConn)
|
||||
engine.SetMaxIdleConns(DbCfg.MaxIdleConn)
|
||||
engine.SetConnMaxLifetime(time.Second * time.Duration(DbCfg.ConnMaxLifetime))
|
||||
debugSql := setting.Raw.Section("database").Key("log_queries").MustBool(false)
|
||||
sqlog.Info("Connecting to DB", "dbtype", ss.dbCfg.Type)
|
||||
engine, err := xorm.NewEngine(ss.dbCfg.Type, connectionString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
engine.SetMaxOpenConns(ss.dbCfg.MaxOpenConn)
|
||||
engine.SetMaxIdleConns(ss.dbCfg.MaxIdleConn)
|
||||
engine.SetConnMaxLifetime(time.Second * time.Duration(ss.dbCfg.ConnMaxLifetime))
|
||||
|
||||
// configure sql logging
|
||||
debugSql := ss.Cfg.Raw.Section("database").Key("log_queries").MustBool(false)
|
||||
if !debugSql {
|
||||
engine.SetLogger(&xorm.DiscardLogger{})
|
||||
} else {
|
||||
@@ -180,101 +197,95 @@ func getEngine() (*xorm.Engine, error) {
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func LoadConfig() {
|
||||
sec := setting.Raw.Section("database")
|
||||
func (ss *SqlStore) readConfig() {
|
||||
sec := ss.Cfg.Raw.Section("database")
|
||||
|
||||
cfgURL := sec.Key("url").String()
|
||||
if len(cfgURL) != 0 {
|
||||
dbURL, _ := url.Parse(cfgURL)
|
||||
DbCfg.Type = dbURL.Scheme
|
||||
DbCfg.Host = dbURL.Host
|
||||
ss.dbCfg.Type = dbURL.Scheme
|
||||
ss.dbCfg.Host = dbURL.Host
|
||||
|
||||
pathSplit := strings.Split(dbURL.Path, "/")
|
||||
if len(pathSplit) > 1 {
|
||||
DbCfg.Name = pathSplit[1]
|
||||
ss.dbCfg.Name = pathSplit[1]
|
||||
}
|
||||
|
||||
userInfo := dbURL.User
|
||||
if userInfo != nil {
|
||||
DbCfg.User = userInfo.Username()
|
||||
DbCfg.Pwd, _ = userInfo.Password()
|
||||
ss.dbCfg.User = userInfo.Username()
|
||||
ss.dbCfg.Pwd, _ = userInfo.Password()
|
||||
}
|
||||
} else {
|
||||
DbCfg.Type = sec.Key("type").String()
|
||||
DbCfg.Host = sec.Key("host").String()
|
||||
DbCfg.Name = sec.Key("name").String()
|
||||
DbCfg.User = sec.Key("user").String()
|
||||
if len(DbCfg.Pwd) == 0 {
|
||||
DbCfg.Pwd = sec.Key("password").String()
|
||||
}
|
||||
ss.dbCfg.Type = sec.Key("type").String()
|
||||
ss.dbCfg.Host = sec.Key("host").String()
|
||||
ss.dbCfg.Name = sec.Key("name").String()
|
||||
ss.dbCfg.User = sec.Key("user").String()
|
||||
ss.dbCfg.ConnectionString = sec.Key("connection_string").String()
|
||||
ss.dbCfg.Pwd = sec.Key("password").String()
|
||||
}
|
||||
DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0)
|
||||
DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0)
|
||||
DbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400)
|
||||
|
||||
if DbCfg.Type == "sqlite3" {
|
||||
UseSQLite3 = true
|
||||
// only allow one connection as sqlite3 has multi threading issues that cause table locks
|
||||
// DbCfg.MaxIdleConn = 1
|
||||
// DbCfg.MaxOpenConn = 1
|
||||
}
|
||||
DbCfg.SslMode = sec.Key("ssl_mode").String()
|
||||
DbCfg.CaCertPath = sec.Key("ca_cert_path").String()
|
||||
DbCfg.ClientKeyPath = sec.Key("client_key_path").String()
|
||||
DbCfg.ClientCertPath = sec.Key("client_cert_path").String()
|
||||
DbCfg.ServerCertName = sec.Key("server_cert_name").String()
|
||||
DbCfg.Path = sec.Key("path").MustString("data/grafana.db")
|
||||
ss.dbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0)
|
||||
ss.dbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(2)
|
||||
ss.dbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400)
|
||||
|
||||
ss.dbCfg.SslMode = sec.Key("ssl_mode").String()
|
||||
ss.dbCfg.CaCertPath = sec.Key("ca_cert_path").String()
|
||||
ss.dbCfg.ClientKeyPath = sec.Key("client_key_path").String()
|
||||
ss.dbCfg.ClientCertPath = sec.Key("client_cert_path").String()
|
||||
ss.dbCfg.ServerCertName = sec.Key("server_cert_name").String()
|
||||
ss.dbCfg.Path = sec.Key("path").MustString("data/grafana.db")
|
||||
}
|
||||
|
||||
var (
|
||||
dbSqlite = "sqlite"
|
||||
dbMySql = "mysql"
|
||||
dbPostgres = "postgres"
|
||||
)
|
||||
func InitTestDB(t *testing.T) *SqlStore {
|
||||
sqlstore := &SqlStore{}
|
||||
sqlstore.skipEnsureAdmin = true
|
||||
|
||||
func InitTestDB(t *testing.T) *xorm.Engine {
|
||||
selectedDb := dbSqlite
|
||||
// selectedDb := dbMySql
|
||||
// selectedDb := dbPostgres
|
||||
|
||||
var x *xorm.Engine
|
||||
var err error
|
||||
dbType := migrator.SQLITE
|
||||
|
||||
// environment variable present for test db?
|
||||
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
|
||||
selectedDb = db
|
||||
dbType = db
|
||||
}
|
||||
|
||||
switch strings.ToLower(selectedDb) {
|
||||
case dbMySql:
|
||||
x, err = xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
|
||||
case dbPostgres:
|
||||
x, err = xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
|
||||
// set test db config
|
||||
sqlstore.Cfg = setting.NewCfg()
|
||||
sec, _ := sqlstore.Cfg.Raw.NewSection("database")
|
||||
sec.NewKey("type", dbType)
|
||||
|
||||
switch dbType {
|
||||
case "mysql":
|
||||
sec.NewKey("connection_string", sqlutil.TestDB_Mysql.ConnStr)
|
||||
case "postgres":
|
||||
sec.NewKey("connection_string", sqlutil.TestDB_Postgres.ConnStr)
|
||||
default:
|
||||
x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
|
||||
sec.NewKey("connection_string", sqlutil.TestDB_Sqlite3.ConnStr)
|
||||
}
|
||||
|
||||
x.DatabaseTZ = time.UTC
|
||||
x.TZLocation = time.UTC
|
||||
|
||||
// x.ShowSQL()
|
||||
|
||||
// need to get engine to clean db before we init
|
||||
engine, err := xorm.NewEngine(dbType, sec.Key("connection_string").String())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to init test database: %v", err)
|
||||
}
|
||||
|
||||
sqlutil.CleanDB(x)
|
||||
|
||||
if err := SetEngine(x); err != nil {
|
||||
t.Fatal(err)
|
||||
dialect = migrator.NewDialect(engine)
|
||||
if err := dialect.CleanDB(); err != nil {
|
||||
t.Fatalf("Failed to clean test db %v", err)
|
||||
}
|
||||
|
||||
return x
|
||||
if err := sqlstore.Init(); err != nil {
|
||||
t.Fatalf("Failed to init test database: %v", err)
|
||||
}
|
||||
|
||||
//// sqlstore.engine.DatabaseTZ = time.UTC
|
||||
//// sqlstore.engine.TZLocation = time.UTC
|
||||
|
||||
return sqlstore
|
||||
}
|
||||
|
||||
func IsTestDbMySql() bool {
|
||||
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
|
||||
return db == dbMySql
|
||||
return db == migrator.MYSQL
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -282,8 +293,20 @@ func IsTestDbMySql() bool {
|
||||
|
||||
func IsTestDbPostgres() bool {
|
||||
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
|
||||
return db == dbPostgres
|
||||
return db == migrator.POSTGRES
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type, Host, Name, User, Pwd, Path, SslMode string
|
||||
CaCertPath string
|
||||
ClientKeyPath string
|
||||
ClientCertPath string
|
||||
ServerCertName string
|
||||
ConnectionString string
|
||||
MaxOpenConn int
|
||||
MaxIdleConn int
|
||||
ConnMaxLifetime int
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package sqlutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
)
|
||||
|
||||
type TestDB struct {
|
||||
DriverName string
|
||||
ConnStr string
|
||||
@@ -15,34 +9,3 @@ var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:"}
|
||||
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci"}
|
||||
var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
|
||||
var TestDB_Mssql = TestDB{DriverName: "mssql", ConnStr: "server=localhost;port=1433;database=grafanatest;user id=grafana;password=Password!"}
|
||||
|
||||
func CleanDB(x *xorm.Engine) {
|
||||
if x.DriverName() == "postgres" {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
if _, err := sess.Exec("DROP SCHEMA public CASCADE;"); err != nil {
|
||||
panic("Failed to drop schema public")
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("CREATE SCHEMA public;"); err != nil {
|
||||
panic("Failed to create schema public")
|
||||
}
|
||||
} else if x.DriverName() == "mysql" {
|
||||
tables, _ := x.DBMetas()
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
for _, table := range tables {
|
||||
if _, err := sess.Exec("set foreign_key_checks = 0"); err != nil {
|
||||
panic("failed to disable foreign key checks")
|
||||
}
|
||||
if _, err := sess.Exec("drop table " + table.Name + " ;"); err != nil {
|
||||
panic(fmt.Sprintf("failed to delete table: %v, err: %v", table.Name, err))
|
||||
}
|
||||
if _, err := sess.Exec("set foreign_key_checks = 1"); err != nil {
|
||||
panic("failed to disable foreign key checks")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,9 +161,8 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
|
||||
sql.WriteString(` order by team.name asc`)
|
||||
|
||||
if query.Limit != 0 {
|
||||
sql.WriteString(` limit ? offset ?`)
|
||||
offset := query.Limit * (query.Page - 1)
|
||||
params = append(params, query.Limit, offset)
|
||||
sql.WriteString(dialect.LimitOffset(int64(query.Limit), int64(offset)))
|
||||
}
|
||||
|
||||
if err := x.Sql(sql.String(), params...).Find(&query.Result.Teams); err != nil {
|
||||
|
||||
@@ -60,8 +60,14 @@ func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *DBSession) (int64, error
|
||||
org.Created = time.Now()
|
||||
org.Updated = time.Now()
|
||||
|
||||
if _, err := sess.Insert(&org); err != nil {
|
||||
return 0, err
|
||||
if org.Id != 0 {
|
||||
if _, err := sess.InsertId(&org); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else {
|
||||
if _, err := sess.InsertOne(&org); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
sess.publishAfterCommit(&events.OrgCreated{
|
||||
|
||||
+22
-9
@@ -52,12 +52,11 @@ var (
|
||||
ApplicationName string
|
||||
|
||||
// Paths
|
||||
LogsPath string
|
||||
HomePath string
|
||||
DataPath string
|
||||
PluginsPath string
|
||||
ProvisioningPath string
|
||||
CustomInitPath = "conf/custom.ini"
|
||||
LogsPath string
|
||||
HomePath string
|
||||
DataPath string
|
||||
PluginsPath string
|
||||
CustomInitPath = "conf/custom.ini"
|
||||
|
||||
// Log settings.
|
||||
LogModes []string
|
||||
@@ -125,6 +124,7 @@ var (
|
||||
AuthProxyAutoSignUp bool
|
||||
AuthProxyLdapSyncTtl int
|
||||
AuthProxyWhitelist string
|
||||
AuthProxyHeaders map[string]string
|
||||
|
||||
// Basic Auth
|
||||
BasicAuthEnabled bool
|
||||
@@ -187,6 +187,9 @@ var (
|
||||
type Cfg struct {
|
||||
Raw *ini.File
|
||||
|
||||
// Paths
|
||||
ProvisioningPath string
|
||||
|
||||
// SMTP email settings
|
||||
Smtp SmtpSettings
|
||||
|
||||
@@ -492,7 +495,9 @@ func validateStaticRootPath() error {
|
||||
}
|
||||
|
||||
func NewCfg() *Cfg {
|
||||
return &Cfg{}
|
||||
return &Cfg{
|
||||
Raw: ini.Empty(),
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
@@ -516,7 +521,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
Env = iniFile.Section("").Key("app_mode").MustString("development")
|
||||
InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name")
|
||||
PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath)
|
||||
ProvisioningPath = makeAbsolute(iniFile.Section("paths").Key("provisioning").String(), HomePath)
|
||||
cfg.ProvisioningPath = makeAbsolute(iniFile.Section("paths").Key("provisioning").String(), HomePath)
|
||||
server := iniFile.Section("server")
|
||||
AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server)
|
||||
|
||||
@@ -611,6 +616,14 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
AuthProxyLdapSyncTtl = authProxy.Key("ldap_sync_ttl").MustInt()
|
||||
AuthProxyWhitelist = authProxy.Key("whitelist").String()
|
||||
|
||||
AuthProxyHeaders = make(map[string]string)
|
||||
for _, propertyAndHeader := range util.SplitString(authProxy.Key("headers").String()) {
|
||||
split := strings.SplitN(propertyAndHeader, ":", 2)
|
||||
if len(split) == 2 {
|
||||
AuthProxyHeaders[split[0]] = split[1]
|
||||
}
|
||||
}
|
||||
|
||||
// basic auth
|
||||
authBasic := iniFile.Section("auth.basic")
|
||||
BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
|
||||
@@ -719,6 +732,6 @@ func (cfg *Cfg) LogConfigSources() {
|
||||
logger.Info("Path Data", "path", DataPath)
|
||||
logger.Info("Path Logs", "path", LogsPath)
|
||||
logger.Info("Path Plugins", "path", PluginsPath)
|
||||
logger.Info("Path Provisioning", "path", ProvisioningPath)
|
||||
logger.Info("Path Provisioning", "path", cfg.ProvisioningPath)
|
||||
logger.Info("App mode " + Env)
|
||||
}
|
||||
|
||||
+55
-43
@@ -1,68 +1,71 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
jaegercfg "github.com/uber/jaeger-client-go/config"
|
||||
ini "gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
logger log.Logger = log.New("tracing")
|
||||
)
|
||||
|
||||
type TracingSettings struct {
|
||||
Enabled bool
|
||||
Address string
|
||||
CustomTags map[string]string
|
||||
SamplerType string
|
||||
SamplerParam float64
|
||||
func init() {
|
||||
registry.RegisterService(&TracingService{})
|
||||
}
|
||||
|
||||
func Init(file *ini.File) (io.Closer, error) {
|
||||
settings := parseSettings(file)
|
||||
return internalInit(settings)
|
||||
type TracingService struct {
|
||||
enabled bool
|
||||
address string
|
||||
customTags map[string]string
|
||||
samplerType string
|
||||
samplerParam float64
|
||||
log log.Logger
|
||||
closer io.Closer
|
||||
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
}
|
||||
|
||||
func parseSettings(file *ini.File) *TracingSettings {
|
||||
settings := &TracingSettings{}
|
||||
func (ts *TracingService) Init() error {
|
||||
ts.log = log.New("tracing")
|
||||
ts.parseSettings()
|
||||
|
||||
var section, err = setting.Raw.GetSection("tracing.jaeger")
|
||||
if ts.enabled {
|
||||
ts.initGlobalTracer()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TracingService) parseSettings() {
|
||||
var section, err = ts.Cfg.Raw.GetSection("tracing.jaeger")
|
||||
if err != nil {
|
||||
return settings
|
||||
return
|
||||
}
|
||||
|
||||
settings.Address = section.Key("address").MustString("")
|
||||
if settings.Address != "" {
|
||||
settings.Enabled = true
|
||||
ts.address = section.Key("address").MustString("")
|
||||
if ts.address != "" {
|
||||
ts.enabled = true
|
||||
}
|
||||
|
||||
settings.CustomTags = splitTagSettings(section.Key("always_included_tag").MustString(""))
|
||||
settings.SamplerType = section.Key("sampler_type").MustString("")
|
||||
settings.SamplerParam = section.Key("sampler_param").MustFloat64(1)
|
||||
|
||||
return settings
|
||||
ts.customTags = splitTagSettings(section.Key("always_included_tag").MustString(""))
|
||||
ts.samplerType = section.Key("sampler_type").MustString("")
|
||||
ts.samplerParam = section.Key("sampler_param").MustFloat64(1)
|
||||
}
|
||||
|
||||
func internalInit(settings *TracingSettings) (io.Closer, error) {
|
||||
if !settings.Enabled {
|
||||
return &nullCloser{}, nil
|
||||
}
|
||||
|
||||
func (ts *TracingService) initGlobalTracer() error {
|
||||
cfg := jaegercfg.Configuration{
|
||||
Disabled: !settings.Enabled,
|
||||
Disabled: !ts.enabled,
|
||||
Sampler: &jaegercfg.SamplerConfig{
|
||||
Type: settings.SamplerType,
|
||||
Param: settings.SamplerParam,
|
||||
Type: ts.samplerType,
|
||||
Param: ts.samplerParam,
|
||||
},
|
||||
Reporter: &jaegercfg.ReporterConfig{
|
||||
LogSpans: false,
|
||||
LocalAgentHostPort: settings.Address,
|
||||
LocalAgentHostPort: ts.address,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -71,18 +74,31 @@ func internalInit(settings *TracingSettings) (io.Closer, error) {
|
||||
options := []jaegercfg.Option{}
|
||||
options = append(options, jaegercfg.Logger(jLogger))
|
||||
|
||||
for tag, value := range settings.CustomTags {
|
||||
for tag, value := range ts.customTags {
|
||||
options = append(options, jaegercfg.Tag(tag, value))
|
||||
}
|
||||
|
||||
tracer, closer, err := cfg.New("grafana", options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
opentracing.InitGlobalTracer(tracer)
|
||||
logger.Info("Initializing Jaeger tracer", "address", settings.Address)
|
||||
return closer, nil
|
||||
|
||||
ts.closer = closer
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TracingService) Run(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
|
||||
if ts.closer != nil {
|
||||
ts.log.Info("Closing tracing")
|
||||
ts.closer.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitTagSettings(input string) map[string]string {
|
||||
@@ -110,7 +126,3 @@ func (jlw *jaegerLogWrapper) Error(msg string) {
|
||||
func (jlw *jaegerLogWrapper) Infof(msg string, args ...interface{}) {
|
||||
jlw.logger.Info(msg, args)
|
||||
}
|
||||
|
||||
type nullCloser struct{}
|
||||
|
||||
func (*nullCloser) Close() error { return nil }
|
||||
|
||||
@@ -10,6 +10,7 @@ import Graph from './Graph';
|
||||
import Table from './Table';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
|
||||
import { decodePathComponent } from 'app/core/utils/location_util';
|
||||
|
||||
function makeTimeSeriesList(dataList, options) {
|
||||
return dataList.map((seriesData, index) => {
|
||||
@@ -38,6 +39,19 @@ function makeTimeSeriesList(dataList, options) {
|
||||
});
|
||||
}
|
||||
|
||||
function parseInitialQueries(initial) {
|
||||
if (!initial) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(decodePathComponent(initial));
|
||||
return parsed.queries.map(q => q.query);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
interface IExploreState {
|
||||
datasource: any;
|
||||
datasourceError: any;
|
||||
@@ -58,6 +72,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initialQueries = parseInitialQueries(props.routeParams.initial);
|
||||
this.state = {
|
||||
datasource: null,
|
||||
datasourceError: null,
|
||||
@@ -65,7 +80,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
graphResult: null,
|
||||
latency: 0,
|
||||
loading: false,
|
||||
queries: ensureQueries(),
|
||||
queries: ensureQueries(initialQueries),
|
||||
requestOptions: null,
|
||||
showingGraph: true,
|
||||
showingTable: true,
|
||||
@@ -77,7 +92,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
const datasource = await this.props.datasourceSrv.get();
|
||||
const testResult = await datasource.testDatasource();
|
||||
if (testResult.status === 'success') {
|
||||
this.setState({ datasource, datasourceError: null, datasourceLoading: false });
|
||||
this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
|
||||
} else {
|
||||
this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false });
|
||||
}
|
||||
|
||||
@@ -6,13 +6,16 @@ class QueryRow extends PureComponent<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
query: '',
|
||||
edited: false,
|
||||
query: props.query || '',
|
||||
};
|
||||
}
|
||||
|
||||
handleChangeQuery = value => {
|
||||
const { index, onChangeQuery } = this.props;
|
||||
this.setState({ query: value });
|
||||
const { query } = this.state;
|
||||
const edited = query !== value;
|
||||
this.setState({ edited, query: value });
|
||||
if (onChangeQuery) {
|
||||
onChangeQuery(value, index);
|
||||
}
|
||||
@@ -41,6 +44,7 @@ class QueryRow extends PureComponent<any, any> {
|
||||
|
||||
render() {
|
||||
const { request } = this.props;
|
||||
const { edited, query } = this.state;
|
||||
return (
|
||||
<div className="query-row">
|
||||
<div className="query-row-tools">
|
||||
@@ -52,7 +56,12 @@ class QueryRow extends PureComponent<any, any> {
|
||||
</button>
|
||||
</div>
|
||||
<div className="query-field-wrapper">
|
||||
<QueryField onPressEnter={this.handlePressEnter} onQueryChange={this.handleChangeQuery} request={request} />
|
||||
<QueryField
|
||||
initialQuery={edited ? null : query}
|
||||
onPressEnter={this.handlePressEnter}
|
||||
onQueryChange={this.handleChangeQuery}
|
||||
request={request}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -63,7 +72,9 @@ export default class QueryRows extends PureComponent<any, any> {
|
||||
render() {
|
||||
const { className = '', queries, ...handlers } = this.props;
|
||||
return (
|
||||
<div className={className}>{queries.map((q, index) => <QueryRow key={q.key} index={index} {...handlers} />)}</div>
|
||||
<div className={className}>
|
||||
{queries.map((q, index) => <QueryRow key={q.key} index={index} query={q.query} {...handlers} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export class Analytics {
|
||||
});
|
||||
ga.l = +new Date();
|
||||
ga('create', (<any>config).googleAnalyticsId, 'auto');
|
||||
ga('set', 'anonymizeIp', true);
|
||||
return ga;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import _ from 'lodash';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { encodePathComponent } from 'app/core/utils/location_util';
|
||||
|
||||
import Mousetrap from 'mousetrap';
|
||||
import 'mousetrap-global-bind';
|
||||
@@ -13,7 +14,7 @@ export class KeybindingSrv {
|
||||
timepickerOpen = false;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $rootScope, private $location) {
|
||||
constructor(private $rootScope, private $location, private datasourceSrv) {
|
||||
// clear out all shortcuts on route change
|
||||
$rootScope.$on('$routeChangeSuccess', () => {
|
||||
Mousetrap.reset();
|
||||
@@ -176,6 +177,17 @@ export class KeybindingSrv {
|
||||
}
|
||||
});
|
||||
|
||||
this.bind('x', async () => {
|
||||
if (dashboard.meta.focusPanelId) {
|
||||
const panel = dashboard.getPanelById(dashboard.meta.focusPanelId);
|
||||
const datasource = await this.datasourceSrv.get(panel.datasource);
|
||||
if (datasource && datasource.supportsExplore) {
|
||||
const exploreState = encodePathComponent(JSON.stringify(datasource.getExploreState(panel)));
|
||||
this.$location.url(`/explore/${exploreState}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// delete panel
|
||||
this.bind('p r', () => {
|
||||
if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) {
|
||||
|
||||
@@ -30,17 +30,17 @@ describe('file_export', () => {
|
||||
it('should export points in proper order', () => {
|
||||
let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat);
|
||||
const expectedText =
|
||||
'Series;Time;Value\n' +
|
||||
'series_1;1500026100;1\n' +
|
||||
'series_1;1500026200;2\n' +
|
||||
'series_1;1500026300;null\n' +
|
||||
'series_1;1500026400;null\n' +
|
||||
'series_1;1500026500;null\n' +
|
||||
'series_1;1500026600;6\n' +
|
||||
'series_2;1500026100;11\n' +
|
||||
'series_2;1500026200;12\n' +
|
||||
'series_2;1500026300;13\n' +
|
||||
'series_2;1500026500;15\n';
|
||||
'"Series";"Time";"Value"\r\n' +
|
||||
'"series_1";"1500026100";1\r\n' +
|
||||
'"series_1";"1500026200";2\r\n' +
|
||||
'"series_1";"1500026300";null\r\n' +
|
||||
'"series_1";"1500026400";null\r\n' +
|
||||
'"series_1";"1500026500";null\r\n' +
|
||||
'"series_1";"1500026600";6\r\n' +
|
||||
'"series_2";"1500026100";11\r\n' +
|
||||
'"series_2";"1500026200";12\r\n' +
|
||||
'"series_2";"1500026300";13\r\n' +
|
||||
'"series_2";"1500026500";15';
|
||||
|
||||
expect(text).toBe(expectedText);
|
||||
});
|
||||
@@ -50,15 +50,76 @@ describe('file_export', () => {
|
||||
it('should export points in proper order', () => {
|
||||
let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat);
|
||||
const expectedText =
|
||||
'Time;series_1;series_2\n' +
|
||||
'1500026100;1;11\n' +
|
||||
'1500026200;2;12\n' +
|
||||
'1500026300;null;13\n' +
|
||||
'1500026400;null;null\n' +
|
||||
'1500026500;null;15\n' +
|
||||
'1500026600;6;null\n';
|
||||
'"Time";"series_1";"series_2"\r\n' +
|
||||
'"1500026100";1;11\r\n' +
|
||||
'"1500026200";2;12\r\n' +
|
||||
'"1500026300";null;13\r\n' +
|
||||
'"1500026400";null;null\r\n' +
|
||||
'"1500026500";null;15\r\n' +
|
||||
'"1500026600";6;null';
|
||||
|
||||
expect(text).toBe(expectedText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when exporting table data to csv', () => {
|
||||
it('should properly escape special characters and quote all string values', () => {
|
||||
const inputTable = {
|
||||
columns: [
|
||||
{ title: 'integer_value' },
|
||||
{ text: 'string_value' },
|
||||
{ title: 'float_value' },
|
||||
{ text: 'boolean_value' },
|
||||
],
|
||||
rows: [
|
||||
[123, 'some_string', 1.234, true],
|
||||
[0o765, 'some string with " in the middle', 1e-2, false],
|
||||
[0o765, 'some string with "" in the middle', 1e-2, false],
|
||||
[0o765, 'some string with """ in the middle', 1e-2, false],
|
||||
[0o765, '"some string with " at the beginning', 1e-2, false],
|
||||
[0o765, 'some string with " at the end"', 1e-2, false],
|
||||
[0x123, 'some string with \n in the middle', 10.01, false],
|
||||
[0b1011, 'some string with ; in the middle', -12.34, true],
|
||||
[123, 'some string with ;; in the middle', -12.34, true],
|
||||
],
|
||||
};
|
||||
|
||||
const returnedText = fileExport.convertTableDataToCsv(inputTable, false);
|
||||
|
||||
const expectedText =
|
||||
'"integer_value";"string_value";"float_value";"boolean_value"\r\n' +
|
||||
'123;"some_string";1.234;true\r\n' +
|
||||
'501;"some string with "" in the middle";0.01;false\r\n' +
|
||||
'501;"some string with """" in the middle";0.01;false\r\n' +
|
||||
'501;"some string with """""" in the middle";0.01;false\r\n' +
|
||||
'501;"""some string with "" at the beginning";0.01;false\r\n' +
|
||||
'501;"some string with "" at the end""";0.01;false\r\n' +
|
||||
'291;"some string with \n in the middle";10.01;false\r\n' +
|
||||
'11;"some string with ; in the middle";-12.34;true\r\n' +
|
||||
'123;"some string with ;; in the middle";-12.34;true';
|
||||
|
||||
expect(returnedText).toBe(expectedText);
|
||||
});
|
||||
|
||||
it('should decode HTML encoded characters', function() {
|
||||
const inputTable = {
|
||||
columns: [{ text: 'string_value' }],
|
||||
rows: [
|
||||
['"&ä'],
|
||||
['<strong>"some html"</strong>'],
|
||||
['<a href="http://something/index.html">some text</a>'],
|
||||
],
|
||||
};
|
||||
|
||||
const returnedText = fileExport.convertTableDataToCsv(inputTable, false);
|
||||
|
||||
const expectedText =
|
||||
'"string_value"\r\n' +
|
||||
'"""&ä"\r\n' +
|
||||
'"<strong>""some html""</strong>"\r\n' +
|
||||
'"<a href=""http://something/index.html"">some text</a>"';
|
||||
|
||||
expect(returnedText).toBe(expectedText);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user