Merge remote-tracking branch 'upstream/master' into postgres-query-builder

This commit is contained in:
Sven Klemm
2018-05-19 12:27:23 +02:00
139 changed files with 1951 additions and 928 deletions
+18
View File
@@ -0,0 +1,18 @@
.awcache
.dockerignore
.git
.gitignore
.github
data*
dist
docker
docs
dump.rdb
node_modules
/local
/tmp
/vendor
*.yml
*.md
/vendor
/tmp
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2
View File
@@ -1,3 +1,5 @@
-include local/Makefile
all: deps build
deps-go:
+10 -16
View File
@@ -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
+13 -1
View File
@@ -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%%;
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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\.&#124;test2)</code> | Formats multi-value variable into a regex string
`pipe` | ${servers:pipe} | `'test.', 'test2'` | <code>test.&#124;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",
+2
View File
@@ -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**:
+2 -2
View File
@@ -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:
+5 -3
View File
@@ -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.
+3 -3
View File
@@ -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
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+19 -10
View File
@@ -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
+2
View File
@@ -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. |
+4 -4
View File
@@ -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(/)?(.*)`
+1 -1
View File
@@ -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>
+3
View File
@@ -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
View File
@@ -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")
+3 -1
View File
@@ -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"))
+8 -24
View File
@@ -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))
}
}
+57 -53
View File
@@ -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() {
+9
View File
@@ -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
View File
@@ -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 {
+14
View File
@@ -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{
+5 -1
View File
@@ -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
}
}
+11
View File
@@ -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,
+7 -6
View File
@@ -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{}
+36 -5
View File
@@ -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
)
+12 -12
View File
@@ -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))
+173
View File
@@ -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/")
})
})
})
}
+7 -6
View File
@@ -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,
})
}
+13 -7
View File
@@ -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
)
+25 -15
View File
@@ -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()
}
+1 -1
View File
@@ -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)
})
+5 -5
View File
@@ -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)
+16 -3
View File
@@ -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)
+1 -25
View File
@@ -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);"))
}
+2 -41
View File
@@ -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)
}
+103 -11
View File
@@ -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;"
}
+38 -17
View File
@@ -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 {
+2 -2
View File
@@ -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
}
+1
View File
@@ -9,6 +9,7 @@ const (
POSTGRES = "postgres"
SQLITE = "sqlite3"
MYSQL = "mysql"
MSSQL = "mssql"
)
type Migration interface {
+1 -1
View File
@@ -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
+1
View File
@@ -43,6 +43,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
Name: "TestOrg",
UserId: 1,
}
err := CreateOrg(&userCmd)
So(err, ShouldBeNil)
orgId = userCmd.Result.Id
+3 -5
View File
@@ -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() {
+2 -5
View File
@@ -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)
})
})
+21
View File
@@ -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
View File
@@ -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(&registry.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
}
-37
View File
@@ -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")
}
}
}
}
+1 -2
View File
@@ -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 {
+8 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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 }
+17 -2
View File
@@ -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 });
}
+15 -4
View File
@@ -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>
);
}
}
+1
View File
@@ -19,6 +19,7 @@ export class Analytics {
});
ga.l = +new Date();
ga('create', (<any>config).googleAnalyticsId, 'auto');
ga('set', 'anonymizeIp', true);
return ga;
}
+13 -1
View File
@@ -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) {
+79 -18
View File
@@ -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: [
['&quot;&amp;&auml;'],
['<strong>&quot;some html&quot;</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