Merge branch 'master' into WPH95-feature/add_es_alerting

This commit is contained in:
Marcus Efraimsson 2018-05-23 14:35:45 +02:00
commit 1324a67cbd
No known key found for this signature in database
GPG Key ID: EBFE0FB04612DD4A
49 changed files with 618 additions and 406 deletions

View File

@ -13,6 +13,10 @@
* **Security**: Fix XSS vulnerabilities in dashboard links [#11813](https://github.com/grafana/grafana/pull/11813) * **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) * **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.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) # 5.1.2 (2018-05-09)
* **Database**: Fix MySql migration issue [#11862](https://github.com/grafana/grafana/issues/11862) * **Database**: Fix MySql migration issue [#11862](https://github.com/grafana/grafana/issues/11862)

View File

@ -94,7 +94,7 @@ deleteDatasources:
orgId: 1 orgId: 1
# list of datasources to insert/update depending # list of datasources to insert/update depending
# whats available in the database # what's available in the database
datasources: datasources:
# <string, required> name of the datasource. Required # <string, required> name of the datasource. Required
- name: Graphite - name: Graphite
@ -154,7 +154,7 @@ Since not all datasources have the same configuration settings we only have the
| tlsAuthWithCACert | boolean | *All* | Enable TLS authentication using CA cert | | tlsAuthWithCACert | boolean | *All* | Enable TLS authentication using CA cert |
| tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
| graphiteVersion | string | Graphite | Graphite version | | graphiteVersion | string | Graphite | Graphite version |
| timeInterval | string | Elastic, Influxdb & Prometheus | Lowest interval/step value that should be used for this data source | | timeInterval | string | Elastic, InfluxDB & Prometheus | Lowest interval/step value that should be used for this data source |
| esVersion | string | Elastic | Elasticsearch version as an number (2/5/56) | | esVersion | string | Elastic | Elasticsearch version as an number (2/5/56) |
| timeField | string | Elastic | Which field that should be used as timestamp | | timeField | string | Elastic | Which field that should be used as timestamp |
| interval | string | Elastic | Index date time format | | interval | string | Elastic | Index date time format |
@ -162,9 +162,9 @@ Since not all datasources have the same configuration settings we only have the
| assumeRoleArn | string | Cloudwatch | ARN of Assume Role | | assumeRoleArn | string | Cloudwatch | ARN of Assume Role |
| defaultRegion | string | Cloudwatch | AWS region | | defaultRegion | string | Cloudwatch | AWS region |
| customMetricsNamespaces | string | Cloudwatch | Namespaces of Custom Metrics | | customMetricsNamespaces | string | Cloudwatch | Namespaces of Custom Metrics |
| tsdbVersion | string | OpenTsdb | Version | | tsdbVersion | string | OpenTSDB | Version |
| tsdbResolution | string | OpenTsdb | Resolution | | tsdbResolution | string | OpenTSDB | Resolution |
| sslmode | string | Postgre | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' | | sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' |
#### Secure Json Data #### Secure Json Data
@ -177,8 +177,8 @@ Secure json data is a map of settings that will be encrypted with [secret key](/
| tlsCACert | string | *All* |CA cert for out going requests | | tlsCACert | string | *All* |CA cert for out going requests |
| tlsClientCert | string | *All* |TLS Client cert for outgoing requests | | tlsClientCert | string | *All* |TLS Client cert for outgoing requests |
| tlsClientKey | string | *All* |TLS Client key for outgoing requests | | tlsClientKey | string | *All* |TLS Client key for outgoing requests |
| password | string | Postgre | password | | password | string | PostgreSQL | password |
| user | string | Postgre | user | | user | string | PostgreSQL | user |
| accessKey | string | Cloudwatch | Access key for connecting to Cloudwatch | | accessKey | string | Cloudwatch | Access key for connecting to Cloudwatch |
| secretKey | string | Cloudwatch | Secret key for connecting to Cloudwatch | | secretKey | string | Cloudwatch | Secret key for connecting to Cloudwatch |

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) - **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) - **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) - **Opsgenie**: Add OpsGenie notification integratiion [#6687](https://github.com/grafana/grafana/issues/6687), thx [@kylemcc](https://github.com/kylemcc)

View File

@ -10,7 +10,7 @@ parent = "whatsnew"
weight = -1 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). 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. Just like the last release this one contains lots bug fixes and minor improvements.

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) * **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) * **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) * **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) * **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) * **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) * **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo)

View File

@ -49,18 +49,15 @@ Content-Type: application/json
{ {
"id": 1, "id": 1,
"dashboardId": 1, "dashboardId": 1,
"dashboardUId": "ABcdEFghij"
"dashboardSlug": "sensors",
"panelId": 1, "panelId": 1,
"name": "fire place sensor", "name": "fire place sensor",
"message": "Someone is trying to break in through the fire place",
"state": "alerting", "state": "alerting",
"message": "Someone is trying to break in through the fire place",
"newStateDate": "2018-05-14T05:55:20+02:00",
"evalDate": "0001-01-01T00:00:00Z", "evalDate": "0001-01-01T00:00:00Z",
"evalData": [ "evalData": null,
{
"metric": "fire",
"tags": null,
"value": 5.349999999999999
}
"newStateDate": "2016-12-25",
"executionError": "", "executionError": "",
"url": "http://grafana.com/dashboard/db/sensors" "url": "http://grafana.com/dashboard/db/sensors"
} }
@ -88,16 +85,35 @@ Content-Type: application/json
{ {
"id": 1, "id": 1,
"dashboardId": 1, "dashboardId": 1,
"dashboardUId": "ABcdEFghij"
"dashboardSlug": "sensors",
"panelId": 1, "panelId": 1,
"name": "fire place sensor", "name": "fire place sensor",
"message": "Someone is trying to break in through the fire place",
"state": "alerting", "state": "alerting",
"newStateDate": "2016-12-25", "message": "Someone is trying to break in through the fire place",
"newStateDate": "2018-05-14T05:55:20+02:00",
"evalDate": "0001-01-01T00:00:00Z",
"evalData": "evalMatches": [
{
"metric": "movement",
"tags": {
"name": "fireplace_chimney"
},
"value": 98.765
}
],
"executionError": "", "executionError": "",
"url": "http://grafana.com/dashboard/db/sensors" "url": "http://grafana.com/dashboard/db/sensors"
} }
``` ```
**Important Note**:
"evalMatches" data is cached in the db when and only when the state of the alert changes
(e.g. transitioning from "ok" to "alerting" state).
If data from one server triggers the alert first and, before that server is seen leaving alerting state,
a second server also enters a state that would trigger the alert, the second server will not be visible in "evalMatches" data.
## Pause alert ## Pause alert
`POST /api/alerts/:id/pause` `POST /api/alerts/:id/pause`

View File

@ -93,8 +93,6 @@ Directory where grafana will automatically scan and look for plugins
### provisioning ### 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 Folder that contains [provisioning](/administration/provisioning) config files that grafana will apply on startup. Dashboards will be reloaded when the json files changes
## [server] ## [server]
@ -717,7 +715,7 @@ Analytics ID here. By default this feature is disabled.
## [dashboards] ## [dashboards]
### versions_to_keep (introduced in v5.0) ### versions_to_keep
Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1. Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1.

View File

@ -15,7 +15,7 @@ weight = 1
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Stable for Debian-based Linux | [grafana_5.1.2_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.2_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) 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 ```bash
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.2_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 apt-get install -y adduser libfontconfig
sudo dpkg -i grafana_5.1.2_amd64.deb sudo dpkg -i grafana_5.1.3_amd64.deb
``` ```
<!-- ## Install Latest Beta <!-- ## Install Latest Beta

View File

@ -15,7 +15,7 @@ weight = 2
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.2 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.2-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) 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. You can install Grafana using Yum directly.
```bash ```bash
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.2-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 <!-- ## Install Beta
@ -42,15 +42,15 @@ Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat: #### On CentOS / Fedora / Redhat:
```bash ```bash
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.2-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 yum install initscripts fontconfig
$ sudo rpm -Uvh grafana-5.1.2-1.x86_64.rpm $ sudo rpm -Uvh grafana-5.1.3-1.x86_64.rpm
``` ```
#### On OpenSuse: #### On OpenSuse:
```bash ```bash
$ sudo rpm -i --nodeps grafana-5.1.2-1.x86_64.rpm $ sudo rpm -i --nodeps grafana-5.1.3-1.x86_64.rpm
``` ```
## Install via YUM Repository ## Install via YUM Repository

View File

@ -12,7 +12,7 @@ weight = 3
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Latest stable package for Windows | [grafana-5.1.2.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.2.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) 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)

View File

@ -25,7 +25,7 @@ export class MyPanelCtrl extends PanelCtrl {
... ...
``` ```
In this case, make sure the template has a single `<div>...</div>` root. The plugin loader will modifiy that element adding a scrollbar. In this case, make sure the template has a single `<div>...</div>` root. The plugin loader will modify that element adding a scrollbar.

View File

@ -94,7 +94,7 @@ weight = 10
</a> </a>
<figcaption> <figcaption>
<a href="https://youtu.be/FC13uhFRsVw?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" target="_blank" rel="noopener noreferrer"> <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> </a>
</figcaption> </figcaption>
</figure> </figure>

View File

@ -22,7 +22,9 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli
Args: flag.Args(), Args: flag.Args(),
}) })
sqlstore.NewEngine() engine := &sqlstore.SqlStore{}
engine.Cfg = cfg
engine.Init()
if err := command(cmd); err != nil { if err := command(cmd); err != nil {
logger.Errorf("\n%s: ", color.RedString("Error")) logger.Errorf("\n%s: ", color.RedString("Error"))

View File

@ -8,7 +8,6 @@ import (
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strconv" "strconv"
"time" "time"
@ -16,14 +15,12 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/dashboards"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social" "github.com/grafana/grafana/pkg/social"
@ -37,6 +34,7 @@ import (
_ "github.com/grafana/grafana/pkg/services/notifications" _ "github.com/grafana/grafana/pkg/services/notifications"
_ "github.com/grafana/grafana/pkg/services/provisioning" _ "github.com/grafana/grafana/pkg/services/provisioning"
_ "github.com/grafana/grafana/pkg/services/search" _ "github.com/grafana/grafana/pkg/services/search"
_ "github.com/grafana/grafana/pkg/services/sqlstore"
_ "github.com/grafana/grafana/pkg/tracing" _ "github.com/grafana/grafana/pkg/tracing"
) )
@ -70,17 +68,12 @@ func (g *GrafanaServerImpl) Run() error {
g.loadConfiguration() g.loadConfiguration()
g.writePIDFile() g.writePIDFile()
// initSql
sqlstore.NewEngine() // TODO: this should return an error
sqlstore.EnsureAdminUser()
login.Init() login.Init()
social.NewOAuthService() social.NewOAuthService()
serviceGraph := inject.Graph{} serviceGraph := inject.Graph{}
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()}) serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
serviceGraph.Provide(&inject.Object{Value: g.cfg}) 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.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
// self registered services // self registered services
@ -88,7 +81,7 @@ func (g *GrafanaServerImpl) Run() error {
// Add all services to dependency graph // Add all services to dependency graph
for _, service := range services { for _, service := range services {
serviceGraph.Provide(&inject.Object{Value: service}) serviceGraph.Provide(&inject.Object{Value: service.Instance})
} }
serviceGraph.Provide(&inject.Object{Value: g}) serviceGraph.Provide(&inject.Object{Value: g})
@ -100,25 +93,27 @@ func (g *GrafanaServerImpl) Run() error {
// Init & start services // Init & start services
for _, service := range services { for _, service := range services {
if registry.IsDisabled(service) { if registry.IsDisabled(service.Instance) {
continue continue
} }
g.log.Info("Initializing " + reflect.TypeOf(service).Elem().Name()) g.log.Info("Initializing " + service.Name)
if err := service.Init(); err != nil { if err := service.Instance.Init(); err != nil {
return fmt.Errorf("Service init failed: %v", err) return fmt.Errorf("Service init failed: %v", err)
} }
} }
// Start background services // Start background services
for index := range services { for _, srv := range services {
service, ok := services[index].(registry.BackgroundService) // variable needed for accessing loop variable in function callback
descriptor := srv
service, ok := srv.Instance.(registry.BackgroundService)
if !ok { if !ok {
continue continue
} }
if registry.IsDisabled(services[index]) { if registry.IsDisabled(descriptor.Instance) {
continue continue
} }
@ -133,9 +128,9 @@ func (g *GrafanaServerImpl) Run() error {
// If error is not canceled then the service crashed // If error is not canceled then the service crashed
if err != context.Canceled && err != nil { if err != context.Canceled && err != nil {
g.log.Error("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err) g.log.Error("Stopped "+descriptor.Name, "reason", err)
} else { } else {
g.log.Info("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err) g.log.Info("Stopped "+descriptor.Name, "reason", err)
} }
// Mark that we are in shutdown mode // Mark that we are in shutdown mode

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"github.com/grafana/grafana/pkg/components/null" "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/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
@ -79,6 +80,14 @@ func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSou
qr.ErrorString = r.Error 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() { for _, s := range r.GetSeries() {
points := tsdb.TimeSeriesPoints{} points := tsdb.TimeSeriesPoints{}

View File

@ -2,15 +2,35 @@ package registry
import ( import (
"context" "context"
"reflect"
"sort"
) )
var services = []Service{} type Descriptor struct {
Name string
func RegisterService(srv Service) { Instance Service
services = append(services, srv) 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 return services
} }
@ -27,7 +47,18 @@ type BackgroundService interface {
Run(ctx context.Context) error Run(ctx context.Context) error
} }
type HasInitPriority interface {
GetInitPriority() Priority
}
func IsDisabled(srv Service) bool { func IsDisabled(srv Service) bool {
canBeDisabled, ok := srv.(CanBeDisabled) canBeDisabled, ok := srv.(CanBeDisabled)
return ok && canBeDisabled.IsDisabled() return ok && canBeDisabled.IsDisabled()
} }
type Priority int
const (
High Priority = 100
Low Priority = 0
)

View File

@ -8,9 +8,9 @@ import (
) )
var ( var (
simpleDashboardConfig = "./test-configs/dashboards-from-disk" simpleDashboardConfig = "./testdata/test-configs/dashboards-from-disk"
oldVersion = "./test-configs/version-0" oldVersion = "./testdata/test-configs/version-0"
brokenConfigs = "./test-configs/broken-configs" brokenConfigs = "./testdata/test-configs/broken-configs"
) )
func TestDashboardsAsConfig(t *testing.T) { func TestDashboardsAsConfig(t *testing.T) {

View File

@ -15,10 +15,10 @@ import (
) )
var ( var (
defaultDashboards = "./test-dashboards/folder-one" defaultDashboards = "./testdata/test-dashboards/folder-one"
brokenDashboards = "./test-dashboards/broken-dashboards" brokenDashboards = "./testdata/test-dashboards/broken-dashboards"
oneDashboard = "./test-dashboards/one-dashboard" oneDashboard = "./testdata/test-dashboards/one-dashboard"
containingId = "./test-dashboards/containing-id" containingId = "./testdata/test-dashboards/containing-id"
fakeService *fakeDashboardProvisioningService fakeService *fakeDashboardProvisioningService
) )

View File

@ -4,7 +4,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/go-xorm/xorm"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
@ -110,14 +109,14 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
} }
func TestDeleteExpiredSnapshots(t *testing.T) { func TestDeleteExpiredSnapshots(t *testing.T) {
x := InitTestDB(t) sqlstore := InitTestDB(t)
Convey("Testing dashboard snapshots clean up", t, func() { Convey("Testing dashboard snapshots clean up", t, func() {
setting.SnapShotRemoveExpired = true setting.SnapShotRemoveExpired = true
notExpiredsnapshot := createTestSnapshot(x, "key1", 1200) notExpiredsnapshot := createTestSnapshot(sqlstore, "key1", 48000)
createTestSnapshot(x, "key2", -1200) createTestSnapshot(sqlstore, "key2", -1200)
createTestSnapshot(x, "key3", -1200) createTestSnapshot(sqlstore, "key3", -1200)
err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{}) err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{})
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -146,7 +145,7 @@ func TestDeleteExpiredSnapshots(t *testing.T) {
}) })
} }
func createTestSnapshot(x *xorm.Engine, key string, expires int64) *m.DashboardSnapshot { func createTestSnapshot(sqlstore *SqlStore, key string, expires int64) *m.DashboardSnapshot {
cmd := m.CreateDashboardSnapshotCommand{ cmd := m.CreateDashboardSnapshotCommand{
Key: key, Key: key,
DeleteKey: "delete" + key, DeleteKey: "delete" + key,
@ -163,7 +162,7 @@ func createTestSnapshot(x *xorm.Engine, key string, expires int64) *m.DashboardS
// Set expiry date manually - to be able to create expired snapshots // Set expiry date manually - to be able to create expired snapshots
if expires < 0 { if expires < 0 {
expireDate := time.Now().Add(time.Second * time.Duration(expires)) expireDate := time.Now().Add(time.Second * time.Duration(expires))
_, err = x.Exec("UPDATE dashboard_snapshot SET expires = ? WHERE id = ?", expireDate, cmd.Result.Id) _, err = sqlstore.engine.Exec("UPDATE dashboard_snapshot SET expires = ? WHERE id = ?", expireDate, cmd.Result.Id)
So(err, ShouldBeNil) So(err, ShouldBeNil)
} }

View File

@ -39,7 +39,7 @@ func TestMigrations(t *testing.T) {
has, err := x.SQL(sql).Get(&r) has, err := x.SQL(sql).Get(&r)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(has, ShouldBeTrue) 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) So(r.Count, ShouldEqual, expectedMigrations)
mg = NewMigrator(x) mg = NewMigrator(x)

View File

@ -48,27 +48,6 @@ func addOrgMigrations(mg *Migrator) {
mg.AddMigration("create org_user table v1", NewAddTableMigration(orgUserV1)) mg.AddMigration("create org_user table v1", NewAddTableMigration(orgUserV1))
addTableIndicesMigrations(mg, "v1", 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{ mg.AddMigration("Update org table charset", NewTableCharsetMigration("org", []*Column{
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false}, {Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "address1", Type: DB_NVarchar, Length: 255, Nullable: true}, {Name: "address1", Type: DB_NVarchar, Length: 255, Nullable: true},

View File

@ -125,7 +125,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
sql, args := condition.Sql(mg.dialect) sql, args := condition.Sql(mg.dialect)
results, err := sess.SQL(sql).Query(args...) results, err := sess.SQL(sql).Query(args...)
if err != nil || len(results) == 0 { 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() return sess.Rollback()
} }
} }

View File

@ -43,6 +43,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
Name: "TestOrg", Name: "TestOrg",
UserId: 1, UserId: 1,
} }
err := CreateOrg(&userCmd) err := CreateOrg(&userCmd)
So(err, ShouldBeNil) So(err, ShouldBeNil)
orgId = userCmd.Result.Id orgId = userCmd.Result.Id

View File

@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models" 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/annotations"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/migrations"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
@ -27,39 +28,72 @@ import (
_ "github.com/grafana/grafana/pkg/tsdb/mssql" _ "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 ( var (
x *xorm.Engine x *xorm.Engine
dialect migrator.Dialect 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{} statsQuery := m.GetSystemStatsQuery{}
if err := bus.Dispatch(&statsQuery); err != nil { if err := bus.Dispatch(&statsQuery); err != nil {
log.Fatal(3, "Could not determine if admin user exists: %v", err) fmt.Errorf("Could not determine if admin user exists: %v", err)
return
} }
if statsQuery.Result.Users > 0 { if statsQuery.Result.Users > 0 {
return return nil
} }
cmd := m.CreateUserCommand{} cmd := m.CreateUserCommand{}
@ -69,109 +103,89 @@ func EnsureAdminUser() {
cmd.IsAdmin = true cmd.IsAdmin = true
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
log.Error(3, "Failed to create default admin user", err) return fmt.Errorf("Failed to create admin user: %v", err)
return
} }
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)
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 return nil
} }
func getEngine() (*xorm.Engine, error) { func (ss *SqlStore) buildConnectionString() (string, error) {
LoadConfig() cnnstr := ss.dbCfg.ConnectionString
cnnstr := "" // special case used by integration tests
switch DbCfg.Type { if cnnstr != "" {
return cnnstr, nil
}
switch ss.dbCfg.Type {
case migrator.MYSQL: case migrator.MYSQL:
protocol := "tcp" protocol := "tcp"
if strings.HasPrefix(DbCfg.Host, "/") { if strings.HasPrefix(ss.dbCfg.Host, "/") {
protocol = "unix" protocol = "unix"
} }
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true", cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true",
url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Pwd), protocol, DbCfg.Host, url.PathEscape(DbCfg.Name)) ss.dbCfg.User, ss.dbCfg.Pwd, protocol, ss.dbCfg.Host, ss.dbCfg.Name)
if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" { if ss.dbCfg.SslMode == "true" || ss.dbCfg.SslMode == "skip-verify" {
tlsCert, err := makeCert("custom", DbCfg) tlsCert, err := makeCert("custom", ss.dbCfg)
if err != nil { if err != nil {
return nil, err return "", err
} }
mysql.RegisterTLSConfig("custom", tlsCert) mysql.RegisterTLSConfig("custom", tlsCert)
cnnstr += "&tls=custom" cnnstr += "&tls=custom"
} }
case migrator.POSTGRES: case migrator.POSTGRES:
var host, port = "127.0.0.1", "5432" 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 { if len(fields) > 0 && len(strings.TrimSpace(fields[0])) > 0 {
host = fields[0] host = fields[0]
} }
if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 { if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 {
port = fields[1] port = fields[1]
} }
cnnstr = fmt.Sprintf("user='%s' password='%s' host='%s' port='%s' dbname='%s' sslmode='%s' sslcert='%s' sslkey='%s' sslrootcert='%s'", if ss.dbCfg.Pwd == "" {
strings.Replace(DbCfg.User, `'`, `\'`, -1), ss.dbCfg.Pwd = "''"
strings.Replace(DbCfg.Pwd, `'`, `\'`, -1),
strings.Replace(host, `'`, `\'`, -1),
strings.Replace(port, `'`, `\'`, -1),
strings.Replace(DbCfg.Name, `'`, `\'`, -1),
strings.Replace(DbCfg.SslMode, `'`, `\'`, -1),
strings.Replace(DbCfg.ClientCertPath, `'`, `\'`, -1),
strings.Replace(DbCfg.ClientKeyPath, `'`, `\'`, -1),
strings.Replace(DbCfg.CaCertPath, `'`, `\'`, -1),
)
case migrator.SQLITE:
if !filepath.IsAbs(DbCfg.Path) {
DbCfg.Path = filepath.Join(setting.DataPath, DbCfg.Path)
} }
os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm) if ss.dbCfg.User == "" {
cnnstr = "file:" + DbCfg.Path + "?cache=shared&mode=rwc" ss.dbCfg.User = "''"
}
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(ss.dbCfg.Path), os.ModePerm)
cnnstr = "file:" + ss.dbCfg.Path + "?cache=shared&mode=rwc"
default: 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) return cnnstr, nil
engine, err := xorm.NewEngine(DbCfg.Type, cnnstr) }
func (ss *SqlStore) getEngine() (*xorm.Engine, error) {
connectionString, err := ss.buildConnectionString()
if err != nil { if err != nil {
return nil, err return nil, err
} }
engine.SetMaxOpenConns(DbCfg.MaxOpenConn) sqlog.Info("Connecting to DB", "dbtype", ss.dbCfg.Type)
engine.SetMaxIdleConns(DbCfg.MaxIdleConn) engine, err := xorm.NewEngine(ss.dbCfg.Type, connectionString)
engine.SetConnMaxLifetime(time.Second * time.Duration(DbCfg.ConnMaxLifetime)) if err != nil {
debugSql := setting.Raw.Section("database").Key("log_queries").MustBool(false) 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 { if !debugSql {
engine.SetLogger(&xorm.DiscardLogger{}) engine.SetLogger(&xorm.DiscardLogger{})
} else { } else {
@ -183,95 +197,90 @@ func getEngine() (*xorm.Engine, error) {
return engine, nil return engine, nil
} }
func LoadConfig() { func (ss *SqlStore) readConfig() {
sec := setting.Raw.Section("database") sec := ss.Cfg.Raw.Section("database")
cfgURL := sec.Key("url").String() cfgURL := sec.Key("url").String()
if len(cfgURL) != 0 { if len(cfgURL) != 0 {
dbURL, _ := url.Parse(cfgURL) dbURL, _ := url.Parse(cfgURL)
DbCfg.Type = dbURL.Scheme ss.dbCfg.Type = dbURL.Scheme
DbCfg.Host = dbURL.Host ss.dbCfg.Host = dbURL.Host
pathSplit := strings.Split(dbURL.Path, "/") pathSplit := strings.Split(dbURL.Path, "/")
if len(pathSplit) > 1 { if len(pathSplit) > 1 {
DbCfg.Name = pathSplit[1] ss.dbCfg.Name = pathSplit[1]
} }
userInfo := dbURL.User userInfo := dbURL.User
if userInfo != nil { if userInfo != nil {
DbCfg.User = userInfo.Username() ss.dbCfg.User = userInfo.Username()
DbCfg.Pwd, _ = userInfo.Password() ss.dbCfg.Pwd, _ = userInfo.Password()
} }
} else { } else {
DbCfg.Type = sec.Key("type").String() ss.dbCfg.Type = sec.Key("type").String()
DbCfg.Host = sec.Key("host").String() ss.dbCfg.Host = sec.Key("host").String()
DbCfg.Name = sec.Key("name").String() ss.dbCfg.Name = sec.Key("name").String()
DbCfg.User = sec.Key("user").String() ss.dbCfg.User = sec.Key("user").String()
if len(DbCfg.Pwd) == 0 { ss.dbCfg.ConnectionString = sec.Key("connection_string").String()
DbCfg.Pwd = sec.Key("password").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" { ss.dbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0)
UseSQLite3 = true ss.dbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(2)
// only allow one connection as sqlite3 has multi threading issues that cause table locks ss.dbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400)
// DbCfg.MaxIdleConn = 1
// DbCfg.MaxOpenConn = 1 ss.dbCfg.SslMode = sec.Key("ssl_mode").String()
} ss.dbCfg.CaCertPath = sec.Key("ca_cert_path").String()
DbCfg.SslMode = sec.Key("ssl_mode").String() ss.dbCfg.ClientKeyPath = sec.Key("client_key_path").String()
DbCfg.CaCertPath = sec.Key("ca_cert_path").String() ss.dbCfg.ClientCertPath = sec.Key("client_cert_path").String()
DbCfg.ClientKeyPath = sec.Key("client_key_path").String() ss.dbCfg.ServerCertName = sec.Key("server_cert_name").String()
DbCfg.ClientCertPath = sec.Key("client_cert_path").String() ss.dbCfg.Path = sec.Key("path").MustString("data/grafana.db")
DbCfg.ServerCertName = sec.Key("server_cert_name").String()
DbCfg.Path = sec.Key("path").MustString("data/grafana.db")
} }
func InitTestDB(t *testing.T) *xorm.Engine { func InitTestDB(t *testing.T) *SqlStore {
selectedDb := migrator.SQLITE sqlstore := &SqlStore{}
// selectedDb := migrator.MYSQL sqlstore.skipEnsureAdmin = true
// selectedDb := migrator.POSTGRES
var x *xorm.Engine dbType := migrator.SQLITE
var err error
// environment variable present for test db? // environment variable present for test db?
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present { if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
selectedDb = db dbType = db
} }
switch strings.ToLower(selectedDb) { // set test db config
case migrator.MYSQL: sqlstore.Cfg = setting.NewCfg()
x, err = xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr) sec, _ := sqlstore.Cfg.Raw.NewSection("database")
case migrator.POSTGRES: sec.NewKey("type", dbType)
x, err = xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
switch dbType {
case "mysql":
sec.NewKey("connection_string", sqlutil.TestDB_Mysql.ConnStr)
case "postgres":
sec.NewKey("connection_string", sqlutil.TestDB_Postgres.ConnStr)
default: 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 // need to get engine to clean db before we init
x.TZLocation = time.UTC engine, err := xorm.NewEngine(dbType, sec.Key("connection_string").String())
if err != nil { if err != nil {
t.Fatalf("Failed to init test database: %v", err) t.Fatalf("Failed to init test database: %v", err)
} }
dialect = migrator.NewDialect(x) dialect = migrator.NewDialect(engine)
if err := dialect.CleanDB(); err != nil {
err = dialect.CleanDB()
if err != nil {
t.Fatalf("Failed to clean test db %v", err) t.Fatalf("Failed to clean test db %v", err)
} }
if err := SetEngine(x); err != nil { if err := sqlstore.Init(); err != nil {
t.Fatal(err) t.Fatalf("Failed to init test database: %v", err)
} }
// x.ShowSQL() //// sqlstore.engine.DatabaseTZ = time.UTC
//// sqlstore.engine.TZLocation = time.UTC
return x return sqlstore
} }
func IsTestDbMySql() bool { func IsTestDbMySql() bool {
@ -289,3 +298,15 @@ func IsTestDbPostgres() bool {
return false 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
}

View File

@ -495,7 +495,9 @@ func validateStaticRootPath() error {
} }
func NewCfg() *Cfg { func NewCfg() *Cfg {
return &Cfg{} return &Cfg{
Raw: ini.Empty(),
}
} }
func (cfg *Cfg) Load(args *CommandLineArgs) error { func (cfg *Cfg) Load(args *CommandLineArgs) error {

View File

@ -989,17 +989,17 @@ kbn.getUnitFormats = function() {
{ {
text: 'velocity', text: 'velocity',
submenu: [ submenu: [
{ text: 'm/s', value: 'velocityms' }, { text: 'metres/second (m/s)', value: 'velocityms' },
{ text: 'km/h', value: 'velocitykmh' }, { text: 'kilometers/hour (km/h)', value: 'velocitykmh' },
{ text: 'mph', value: 'velocitymph' }, { text: 'miles/hour (mph)', value: 'velocitymph' },
{ text: 'knot (kn)', value: 'velocityknot' }, { text: 'knot (kn)', value: 'velocityknot' },
], ],
}, },
{ {
text: 'volume', text: 'volume',
submenu: [ submenu: [
{ text: 'millilitre', value: 'mlitre' }, { text: 'millilitre (mL)', value: 'mlitre' },
{ text: 'litre', value: 'litre' }, { text: 'litre (L)', value: 'litre' },
{ text: 'cubic metre', value: 'm3' }, { text: 'cubic metre', value: 'm3' },
{ text: 'Normal cubic metre', value: 'Nm3' }, { text: 'Normal cubic metre', value: 'Nm3' },
{ text: 'cubic decimetre', value: 'dm3' }, { text: 'cubic decimetre', value: 'dm3' },

View File

@ -312,7 +312,7 @@ class MetricsPanelCtrl extends PanelCtrl {
getAdditionalMenuItems() { getAdditionalMenuItems() {
const items = []; const items = [];
if (this.datasource.supportsExplore) { if (this.datasource && this.datasource.supportsExplore) {
items.push({ items.push({
text: 'Explore', text: 'Explore',
click: 'ctrl.explore();', click: 'ctrl.explore();',

View File

@ -0,0 +1,65 @@
jest.mock('app/core/core', () => ({}));
import { MetricsPanelCtrl } from '../metrics_panel_ctrl';
import q from 'q';
import { PanelModel } from 'app/features/dashboard/panel_model';
describe('MetricsPanelCtrl', () => {
let ctrl;
beforeEach(() => {
ctrl = setupController();
});
describe('when getting additional menu items', () => {
let additionalItems;
describe('and has no datasource set', () => {
beforeEach(() => {
additionalItems = ctrl.getAdditionalMenuItems();
});
it('should not return any items', () => {
expect(additionalItems.length).toBe(0);
});
});
describe('and has datasource set that supports explore', () => {
beforeEach(() => {
ctrl.datasource = { supportsExplore: true };
additionalItems = ctrl.getAdditionalMenuItems();
});
it('should not return any items', () => {
expect(additionalItems.length).toBe(1);
});
});
});
});
function setupController() {
const injectorStub = {
get: type => {
switch (type) {
case '$q': {
return q;
}
default: {
return jest.fn();
}
}
},
};
const scope = {
panel: { events: [] },
appEvent: jest.fn(),
onAppEvent: jest.fn(),
$on: jest.fn(),
colors: [],
};
MetricsPanelCtrl.prototype.panel = new PanelModel({ type: 'test' });
return new MetricsPanelCtrl(scope, injectorStub);
}

View File

@ -11,23 +11,14 @@ export default class ResponseParser {
return []; return [];
} }
var influxdb11format = query.toLowerCase().indexOf('show tag values') >= 0;
var res = {}; var res = {};
_.each(influxResults.series, serie => { _.each(influxResults.series, serie => {
_.each(serie.values, value => { _.each(serie.values, value => {
if (_.isArray(value)) { if (_.isArray(value)) {
// In general, there are 2 possible shapes for the returned value. if (influxdb11format) {
// The first one is a two-element array, addUnique(res, value[1] || value[0]);
// where the first element is somewhat a metadata value:
// the tag name for SHOW TAG VALUES queries,
// the time field for SELECT queries, etc.
// The second shape is an one-element array,
// that is containing an immediate value.
// For example, SHOW FIELD KEYS queries return such shape.
// Note, pre-0.11 versions return
// the second shape for SHOW TAG VALUES queries
// (while the newer versions—first).
if (value[1] !== undefined) {
addUnique(res, value[1]);
} else { } else {
addUnique(res, value[0]); addUnique(res, value[0]);
} }
@ -38,7 +29,7 @@ export default class ResponseParser {
}); });
return _.map(res, value => { return _.map(res, value => {
return { text: value.toString() }; return { text: value };
}); });
} }
} }

View File

@ -85,32 +85,6 @@ describe('influxdb response parser', () => {
}); });
}); });
describe('SELECT response', () => {
var query = 'SELECT "usage_iowait" FROM "cpu" LIMIT 10';
var response = {
results: [
{
series: [
{
name: 'cpu',
columns: ['time', 'usage_iowait'],
values: [[1488465190006040638, 0.0], [1488465190006040638, 15.0], [1488465190006040638, 20.2]],
},
],
},
],
};
var result = parser.parse(query, response);
it('should return second column', () => {
expect(_.size(result)).toBe(3);
expect(result[0].text).toBe('0');
expect(result[1].text).toBe('15');
expect(result[2].text).toBe('20.2');
});
});
describe('SHOW FIELD response', () => { describe('SHOW FIELD response', () => {
var query = 'SHOW FIELD KEYS FROM "cpu"'; var query = 'SHOW FIELD KEYS FROM "cpu"';
describe('response from 0.10.0', () => { describe('response from 0.10.0', () => {

View File

@ -27,6 +27,7 @@ export class PrometheusDatasource {
withCredentials: any; withCredentials: any;
metricsNameCache: any; metricsNameCache: any;
interval: string; interval: string;
queryTimeout: string;
httpMethod: string; httpMethod: string;
resultTransformer: ResultTransformer; resultTransformer: ResultTransformer;
@ -42,6 +43,7 @@ export class PrometheusDatasource {
this.basicAuth = instanceSettings.basicAuth; this.basicAuth = instanceSettings.basicAuth;
this.withCredentials = instanceSettings.withCredentials; this.withCredentials = instanceSettings.withCredentials;
this.interval = instanceSettings.jsonData.timeInterval || '15s'; this.interval = instanceSettings.jsonData.timeInterval || '15s';
this.queryTimeout = instanceSettings.jsonData.queryTimeout;
this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET';
this.resultTransformer = new ResultTransformer(templateSrv); this.resultTransformer = new ResultTransformer(templateSrv);
} }
@ -107,10 +109,18 @@ export class PrometheusDatasource {
return this.templateSrv.variableExists(target.expr); return this.templateSrv.variableExists(target.expr);
} }
clampRange(start, end, step) {
const clampedEnd = Math.ceil(end / step) * step;
const clampedRange = Math.floor((end - start) / step) * step;
return {
end: clampedEnd,
start: clampedEnd - clampedRange,
};
}
query(options) { query(options) {
var start = this.getPrometheusTime(options.range.from, false); var start = this.getPrometheusTime(options.range.from, false);
var end = this.getPrometheusTime(options.range.to, true); var end = this.getPrometheusTime(options.range.to, true);
var range = Math.ceil(end - start);
var queries = []; var queries = [];
var activeTargets = []; var activeTargets = [];
@ -123,7 +133,7 @@ export class PrometheusDatasource {
} }
activeTargets.push(target); activeTargets.push(target);
queries.push(this.createQuery(target, options, range)); queries.push(this.createQuery(target, options, start, end));
} }
// No valid targets, return the empty result to save a round trip. // No valid targets, return the empty result to save a round trip.
@ -133,7 +143,7 @@ export class PrometheusDatasource {
var allQueryPromise = _.map(queries, query => { var allQueryPromise = _.map(queries, query => {
if (!query.instant) { if (!query.instant) {
return this.performTimeSeriesQuery(query, start, end); return this.performTimeSeriesQuery(query, query.start, query.end);
} else { } else {
return this.performInstantQuery(query, end); return this.performInstantQuery(query, end);
} }
@ -147,7 +157,8 @@ export class PrometheusDatasource {
throw response.error; throw response.error;
} }
let transformerOptions = { // Keeping original start/end for transformers
const transformerOptions = {
format: activeTargets[index].format, format: activeTargets[index].format,
step: queries[index].step, step: queries[index].step,
legendFormat: activeTargets[index].legendFormat, legendFormat: activeTargets[index].legendFormat,
@ -165,9 +176,10 @@ export class PrometheusDatasource {
}); });
} }
createQuery(target, options, range) { createQuery(target, options, start, end) {
var query: any = {}; var query: any = {};
query.instant = target.instant; query.instant = target.instant;
var range = Math.ceil(end - start);
var interval = kbn.interval_to_seconds(options.interval); var interval = kbn.interval_to_seconds(options.interval);
// Minimum interval ("Min step"), if specified for the query. or same as interval otherwise // Minimum interval ("Min step"), if specified for the query. or same as interval otherwise
@ -191,6 +203,12 @@ export class PrometheusDatasource {
// Only replace vars in expression after having (possibly) updated interval vars // Only replace vars in expression after having (possibly) updated interval vars
query.expr = this.templateSrv.replace(target.expr, scopedVars, this.interpolateQueryExpr); query.expr = this.templateSrv.replace(target.expr, scopedVars, this.interpolateQueryExpr);
query.requestId = options.panelId + target.refId; query.requestId = options.panelId + target.refId;
// Align query interval with step
const adjusted = this.clampRange(start, end, query.step);
query.start = adjusted.start;
query.end = adjusted.end;
return query; return query;
} }
@ -215,6 +233,9 @@ export class PrometheusDatasource {
end: end, end: end,
step: query.step, step: query.step,
}; };
if (this.queryTimeout) {
data['timeout'] = this.queryTimeout;
}
return this._request(url, data, { requestId: query.requestId }); return this._request(url, data, { requestId: query.requestId });
} }
@ -224,6 +245,9 @@ export class PrometheusDatasource {
query: query.expr, query: query.expr,
time: time, time: time,
}; };
if (this.queryTimeout) {
data['timeout'] = this.queryTimeout;
}
return this._request(url, data, { requestId: query.requestId }); return this._request(url, data, { requestId: query.requestId });
} }
@ -270,22 +294,18 @@ export class PrometheusDatasource {
return this.$q.when([]); return this.$q.when([]);
} }
var interpolated = this.templateSrv.replace(expr, {}, this.interpolateQueryExpr); var step = annotation.step || '60s';
var step = '60s';
if (annotation.step) {
step = this.templateSrv.replace(annotation.step);
}
var start = this.getPrometheusTime(options.range.from, false); var start = this.getPrometheusTime(options.range.from, false);
var end = this.getPrometheusTime(options.range.to, true); var end = this.getPrometheusTime(options.range.to, true);
var query = { // Unsetting min interval
expr: interpolated, const queryOptions = {
step: this.adjustInterval(kbn.interval_to_seconds(step), 0, Math.ceil(end - start), 1) + 's', ...options,
interval: '0s',
}; };
const query = this.createQuery({ expr, interval: step }, queryOptions, start, end);
var self = this; var self = this;
return this.performTimeSeriesQuery(query, start, end).then(function(results) { return this.performTimeSeriesQuery(query, query.start, query.end).then(function(results) {
var eventList = []; var eventList = [];
tagKeys = tagKeys.split(','); tagKeys = tagKeys.split(',');

View File

@ -7,8 +7,18 @@
<span class="gf-form-label width-8">Scrape interval</span> <span class="gf-form-label width-8">Scrape interval</span>
<input type="text" class="gf-form-input width-8" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="15s"></input> <input type="text" class="gf-form-input width-8" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="15s"></input>
<info-popover mode="right-absolute"> <info-popover mode="right-absolute">
Set this to your global scrape interval defined in your Prometheus config file. This will be used as a lower limit for Set this to your global scrape interval defined in your Prometheus config file. This will be used as a lower limit for the
the Prometheus step query parameter. Prometheus step query parameter.
</info-popover>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-8">Query timeout</span>
<input type="text" class="gf-form-input width-8" ng-model="ctrl.current.jsonData.queryTimeout" spellcheck='false' placeholder="60s"></input>
<info-popover mode="right-absolute">
Set the Prometheus query timeout.
</info-popover> </info-popover>
</div> </div>
</div> </div>

View File

@ -14,8 +14,8 @@
data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()"> data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
</input> </input>
<info-popover mode="right-absolute"> <info-popover mode="right-absolute">
Controls the name of the time series, using name or pattern. For example <span ng-non-bindable>{{hostname}}</span> will be replaced with label value for Controls the name of the time series, using name or pattern. For example
the label hostname. <span ng-non-bindable>{{hostname}}</span> will be replaced with label value for the label hostname.
</info-popover> </info-popover>
</div> </div>
@ -25,7 +25,8 @@
placeholder="{{ctrl.panelCtrl.interval}}" data-min-length=0 data-items=100 ng-model-onblur ng-change="ctrl.refreshMetricData()" placeholder="{{ctrl.panelCtrl.interval}}" data-min-length=0 data-items=100 ng-model-onblur ng-change="ctrl.refreshMetricData()"
/> />
<info-popover mode="right-absolute"> <info-popover mode="right-absolute">
Leave blank for auto handling based on time range and panel width Leave blank for auto handling based on time range and panel width. Note that the actual dates used in the query will be adjusted
to a multiple of the interval step.
</info-popover> </info-popover>
</div> </div>

View File

@ -4,6 +4,12 @@ import $ from 'jquery';
import helpers from 'test/specs/helpers'; import helpers from 'test/specs/helpers';
import { PrometheusDatasource } from '../datasource'; import { PrometheusDatasource } from '../datasource';
const SECOND = 1000;
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
const time = ({ hours = 0, seconds = 0, minutes = 0 }) => moment(hours * HOUR + minutes * MINUTE + seconds * SECOND);
describe('PrometheusDatasource', function() { describe('PrometheusDatasource', function() {
var ctx = new helpers.ServiceTestContext(); var ctx = new helpers.ServiceTestContext();
var instanceSettings = { var instanceSettings = {
@ -29,18 +35,16 @@ describe('PrometheusDatasource', function() {
$httpBackend.when('GET', /\.html$/).respond(''); $httpBackend.when('GET', /\.html$/).respond('');
}) })
); );
describe('When querying prometheus with one target using query editor target spec', function() { describe('When querying prometheus with one target using query editor target spec', function() {
var results; var results;
var urlExpected =
'proxied/api/v1/query_range?query=' +
encodeURIComponent('test{job="testjob"}') +
'&start=1443438675&end=1443460275&step=60';
var query = { var query = {
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
interval: '60s', interval: '60s',
}; };
// Interval alignment with step
var urlExpected =
'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=120&end=240&step=60';
var response = { var response = {
status: 'success', status: 'success',
data: { data: {
@ -48,7 +52,7 @@ describe('PrometheusDatasource', function() {
result: [ result: [
{ {
metric: { __name__: 'test', job: 'testjob' }, metric: { __name__: 'test', job: 'testjob' },
values: [[1443454528, '3846']], values: [[60, '3846']],
}, },
], ],
}, },
@ -70,8 +74,8 @@ describe('PrometheusDatasource', function() {
}); });
describe('When querying prometheus with one target which return multiple series', function() { describe('When querying prometheus with one target which return multiple series', function() {
var results; var results;
var start = 1443438675; var start = 60;
var end = 1443460275; var end = 360;
var step = 60; var step = 60;
var urlExpected = var urlExpected =
'proxied/api/v1/query_range?query=' + 'proxied/api/v1/query_range?query=' +
@ -83,7 +87,7 @@ describe('PrometheusDatasource', function() {
'&step=' + '&step=' +
step; step;
var query = { var query = {
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ seconds: start }), to: time({ seconds: end }) },
targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
interval: '60s', interval: '60s',
}; };
@ -139,9 +143,9 @@ describe('PrometheusDatasource', function() {
}); });
describe('When querying prometheus with one target and instant = true', function() { describe('When querying prometheus with one target and instant = true', function() {
var results; var results;
var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=1443460275'; var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
var query = { var query = {
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
interval: '60s', interval: '60s',
}; };
@ -152,7 +156,7 @@ describe('PrometheusDatasource', function() {
result: [ result: [
{ {
metric: { __name__: 'test', job: 'testjob' }, metric: { __name__: 'test', job: 'testjob' },
value: [1443454528, '3846'], value: [123, '3846'],
}, },
], ],
}, },
@ -177,7 +181,7 @@ describe('PrometheusDatasource', function() {
var urlExpected = var urlExpected =
'proxied/api/v1/query_range?query=' + 'proxied/api/v1/query_range?query=' +
encodeURIComponent('ALERTS{alertstate="firing"}') + encodeURIComponent('ALERTS{alertstate="firing"}') +
'&start=1443438675&end=1443460275&step=60s'; '&start=120&end=180&step=60';
var options = { var options = {
annotation: { annotation: {
expr: 'ALERTS{alertstate="firing"}', expr: 'ALERTS{alertstate="firing"}',
@ -186,8 +190,8 @@ describe('PrometheusDatasource', function() {
textFormat: '{{instance}}', textFormat: '{{instance}}',
}, },
range: { range: {
from: moment(1443438674760), from: time({ seconds: 63 }),
to: moment(1443460274760), to: time({ seconds: 123 }),
}, },
}; };
var response = { var response = {
@ -203,7 +207,7 @@ describe('PrometheusDatasource', function() {
instance: 'testinstance', instance: 'testinstance',
job: 'testjob', job: 'testjob',
}, },
values: [[1443454528, '1']], values: [[123, '1']],
}, },
], ],
}, },
@ -221,15 +225,15 @@ describe('PrometheusDatasource', function() {
expect(results[0].tags).to.contain('testjob'); expect(results[0].tags).to.contain('testjob');
expect(results[0].title).to.be('InstanceDown'); expect(results[0].title).to.be('InstanceDown');
expect(results[0].text).to.be('testinstance'); expect(results[0].text).to.be('testinstance');
expect(results[0].time).to.be(1443454528 * 1000); expect(results[0].time).to.be(123 * 1000);
}); });
}); });
describe('When resultFormat is table and instant = true', function() { describe('When resultFormat is table and instant = true', function() {
var results; var results;
var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=1443460275'; var urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
var query = { var query = {
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ seconds: 63 }), to: time({ seconds: 123 }) },
targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }], targets: [{ expr: 'test{job="testjob"}', format: 'time_series', instant: true }],
interval: '60s', interval: '60s',
}; };
@ -240,7 +244,7 @@ describe('PrometheusDatasource', function() {
result: [ result: [
{ {
metric: { __name__: 'test', job: 'testjob' }, metric: { __name__: 'test', job: 'testjob' },
value: [1443454528, '3846'], value: [123, '3846'],
}, },
], ],
}, },
@ -270,8 +274,8 @@ describe('PrometheusDatasource', function() {
it('should be min interval when greater than auto interval', function() { it('should be min interval when greater than auto interval', function() {
var query = { var query = {
// 6 hour range // 6 minute range
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
targets: [ targets: [
{ {
expr: 'test', expr: 'test',
@ -280,7 +284,7 @@ describe('PrometheusDatasource', function() {
], ],
interval: '5s', interval: '5s',
}; };
var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=10'; var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
@ -288,12 +292,12 @@ describe('PrometheusDatasource', function() {
it('step should never go below 1', function() { it('step should never go below 1', function() {
var query = { var query = {
// 6 hour range // 6 minute range
range: { from: moment(1508318768202), to: moment(1508318770118) }, range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
targets: [{ expr: 'test' }], targets: [{ expr: 'test' }],
interval: '100ms', interval: '100ms',
}; };
var urlExpected = 'proxied/api/v1/query_range?query=test&start=1508318769&end=1508318771&step=1'; var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=1';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
@ -301,8 +305,8 @@ describe('PrometheusDatasource', function() {
it('should be auto interval when greater than min interval', function() { it('should be auto interval when greater than min interval', function() {
var query = { var query = {
// 6 hour range // 6 minute range
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
targets: [ targets: [
{ {
expr: 'test', expr: 'test',
@ -311,7 +315,7 @@ describe('PrometheusDatasource', function() {
], ],
interval: '10s', interval: '10s',
}; };
var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=10'; var urlExpected = 'proxied/api/v1/query_range?query=test&start=60&end=420&step=10';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
@ -319,19 +323,21 @@ describe('PrometheusDatasource', function() {
it('should result in querying fewer than 11000 data points', function() { it('should result in querying fewer than 11000 data points', function() {
var query = { var query = {
// 6 hour range // 6 hour range
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ hours: 1 }), to: time({ hours: 7 }) },
targets: [{ expr: 'test' }], targets: [{ expr: 'test' }],
interval: '1s', interval: '1s',
}; };
var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=2'; var end = 7 * 60 * 60;
var start = 60 * 60;
var urlExpected = 'proxied/api/v1/query_range?query=test&start=' + start + '&end=' + end + '&step=2';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
}); });
it('should not apply min interval when interval * intervalFactor greater', function() { it('should not apply min interval when interval * intervalFactor greater', function() {
var query = { var query = {
// 6 hour range // 6 minute range
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
targets: [ targets: [
{ {
expr: 'test', expr: 'test',
@ -341,15 +347,16 @@ describe('PrometheusDatasource', function() {
], ],
interval: '5s', interval: '5s',
}; };
var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=50'; // times get rounded up to interval
var urlExpected = 'proxied/api/v1/query_range?query=test&start=100&end=450&step=50';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
}); });
it('should apply min interval when interval * intervalFactor smaller', function() { it('should apply min interval when interval * intervalFactor smaller', function() {
var query = { var query = {
// 6 hour range // 6 minute range
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
targets: [ targets: [
{ {
expr: 'test', expr: 'test',
@ -359,15 +366,15 @@ describe('PrometheusDatasource', function() {
], ],
interval: '5s', interval: '5s',
}; };
var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=15'; var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=60&end=420&step=15';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
}); });
it('should apply intervalFactor to auto interval when greater', function() { it('should apply intervalFactor to auto interval when greater', function() {
var query = { var query = {
// 6 hour range // 6 minute range
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
targets: [ targets: [
{ {
expr: 'test', expr: 'test',
@ -377,7 +384,8 @@ describe('PrometheusDatasource', function() {
], ],
interval: '10s', interval: '10s',
}; };
var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=100'; // times get rounded up to interval
var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=200&end=500&step=100';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
@ -385,7 +393,7 @@ describe('PrometheusDatasource', function() {
it('should not not be affected by the 11000 data points limit when large enough', function() { it('should not not be affected by the 11000 data points limit when large enough', function() {
var query = { var query = {
// 1 week range // 1 week range
range: { from: moment(1443438674760), to: moment(1444043474760) }, range: { from: time({}), to: time({ hours: 7 * 24 }) },
targets: [ targets: [
{ {
expr: 'test', expr: 'test',
@ -394,7 +402,9 @@ describe('PrometheusDatasource', function() {
], ],
interval: '10s', interval: '10s',
}; };
var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1444043475&step=100'; var end = 7 * 24 * 60 * 60;
var start = 0;
var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=100';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
@ -402,7 +412,7 @@ describe('PrometheusDatasource', function() {
it('should be determined by the 11000 data points limit when too small', function() { it('should be determined by the 11000 data points limit when too small', function() {
var query = { var query = {
// 1 week range // 1 week range
range: { from: moment(1443438674760), to: moment(1444043474760) }, range: { from: time({}), to: time({ hours: 7 * 24 }) },
targets: [ targets: [
{ {
expr: 'test', expr: 'test',
@ -411,12 +421,15 @@ describe('PrometheusDatasource', function() {
], ],
interval: '5s', interval: '5s',
}; };
var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1444043475&step=60'; var end = 7 * 24 * 60 * 60;
var start = 0;
var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=' + start + '&end=' + end + '&step=60';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
}); });
}); });
describe('The __interval and __interval_ms template variables', function() { describe('The __interval and __interval_ms template variables', function() {
var response = { var response = {
status: 'success', status: 'success',
@ -428,8 +441,8 @@ describe('PrometheusDatasource', function() {
it('should be unchanged when auto interval is greater than min interval', function() { it('should be unchanged when auto interval is greater than min interval', function() {
var query = { var query = {
// 6 hour range // 6 minute range
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
targets: [ targets: [
{ {
expr: 'rate(test[$__interval])', expr: 'rate(test[$__interval])',
@ -443,9 +456,7 @@ describe('PrometheusDatasource', function() {
}, },
}; };
var urlExpected = var urlExpected =
'proxied/api/v1/query_range?query=' + 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10';
encodeURIComponent('rate(test[10s])') +
'&start=1443438675&end=1443460275&step=10';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
@ -457,8 +468,8 @@ describe('PrometheusDatasource', function() {
}); });
it('should be min interval when it is greater than auto interval', function() { it('should be min interval when it is greater than auto interval', function() {
var query = { var query = {
// 6 hour range // 6 minute range
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
targets: [ targets: [
{ {
expr: 'rate(test[$__interval])', expr: 'rate(test[$__interval])',
@ -472,9 +483,7 @@ describe('PrometheusDatasource', function() {
}, },
}; };
var urlExpected = var urlExpected =
'proxied/api/v1/query_range?query=' + 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + '&start=60&end=420&step=10';
encodeURIComponent('rate(test[10s])') +
'&start=1443438675&end=1443460275&step=10';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
@ -486,8 +495,8 @@ describe('PrometheusDatasource', function() {
}); });
it('should account for intervalFactor', function() { it('should account for intervalFactor', function() {
var query = { var query = {
// 6 hour range // 6 minute range
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
targets: [ targets: [
{ {
expr: 'rate(test[$__interval])', expr: 'rate(test[$__interval])',
@ -502,9 +511,7 @@ describe('PrometheusDatasource', function() {
}, },
}; };
var urlExpected = var urlExpected =
'proxied/api/v1/query_range?query=' + 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[100s])') + '&start=200&end=500&step=100';
encodeURIComponent('rate(test[100s])') +
'&start=1443438675&end=1443460275&step=100';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
@ -516,8 +523,8 @@ describe('PrometheusDatasource', function() {
}); });
it('should be interval * intervalFactor when greater than min interval', function() { it('should be interval * intervalFactor when greater than min interval', function() {
var query = { var query = {
// 6 hour range // 6 minute range
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
targets: [ targets: [
{ {
expr: 'rate(test[$__interval])', expr: 'rate(test[$__interval])',
@ -532,9 +539,7 @@ describe('PrometheusDatasource', function() {
}, },
}; };
var urlExpected = var urlExpected =
'proxied/api/v1/query_range?query=' + 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[50s])') + '&start=100&end=450&step=50';
encodeURIComponent('rate(test[50s])') +
'&start=1443438675&end=1443460275&step=50';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
@ -546,8 +551,8 @@ describe('PrometheusDatasource', function() {
}); });
it('should be min interval when greater than interval * intervalFactor', function() { it('should be min interval when greater than interval * intervalFactor', function() {
var query = { var query = {
// 6 hour range // 6 minute range
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ minutes: 1 }), to: time({ minutes: 7 }) },
targets: [ targets: [
{ {
expr: 'rate(test[$__interval])', expr: 'rate(test[$__interval])',
@ -562,9 +567,7 @@ describe('PrometheusDatasource', function() {
}, },
}; };
var urlExpected = var urlExpected =
'proxied/api/v1/query_range?query=' + 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[15s])') + '&start=60&end=420&step=15';
encodeURIComponent('rate(test[15s])') +
'&start=1443438675&end=1443460275&step=15';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
@ -577,7 +580,7 @@ describe('PrometheusDatasource', function() {
it('should be determined by the 11000 data points limit, accounting for intervalFactor', function() { it('should be determined by the 11000 data points limit, accounting for intervalFactor', function() {
var query = { var query = {
// 1 week range // 1 week range
range: { from: moment(1443438674760), to: moment(1444043474760) }, range: { from: time({}), to: time({ hours: 7 * 24 }) },
targets: [ targets: [
{ {
expr: 'rate(test[$__interval])', expr: 'rate(test[$__interval])',
@ -590,10 +593,16 @@ describe('PrometheusDatasource', function() {
__interval_ms: { text: 5 * 1000, value: 5 * 1000 }, __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
}, },
}; };
var end = 7 * 24 * 60 * 60;
var start = 0;
var urlExpected = var urlExpected =
'proxied/api/v1/query_range?query=' + 'proxied/api/v1/query_range?query=' +
encodeURIComponent('rate(test[60s])') + encodeURIComponent('rate(test[60s])') +
'&start=1443438675&end=1444043475&step=60'; '&start=' +
start +
'&end=' +
end +
'&step=60';
ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query); ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation(); ctx.$httpBackend.verifyNoOutstandingExpectation();
@ -604,6 +613,29 @@ describe('PrometheusDatasource', function() {
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
}); });
}); });
describe('Step alignment of intervals', function() {
it('does not modify already aligned intervals with perfect step', function() {
const range = ctx.ds.clampRange(0, 3, 3);
expect(range.start).to.be(0);
expect(range.end).to.be(3);
});
it('does modify end-aligned intervals to reflect number of steps possible', function() {
const range = ctx.ds.clampRange(1, 6, 3);
expect(range.start).to.be(3);
expect(range.end).to.be(6);
});
it('does align intervals that are a multiple of steps', function() {
const range = ctx.ds.clampRange(1, 4, 3);
expect(range.start).to.be(3);
expect(range.end).to.be(6);
});
it('does align intervals that are not a multiple of steps', function() {
const range = ctx.ds.clampRange(1, 5, 3);
expect(range.start).to.be(3);
expect(range.end).to.be(6);
});
});
}); });
describe('PrometheusDatasource for POST', function() { describe('PrometheusDatasource for POST', function() {
@ -635,12 +667,12 @@ describe('PrometheusDatasource for POST', function() {
var urlExpected = 'proxied/api/v1/query_range'; var urlExpected = 'proxied/api/v1/query_range';
var dataExpected = $.param({ var dataExpected = $.param({
query: 'test{job="testjob"}', query: 'test{job="testjob"}',
start: 1443438675, start: 2 * 60,
end: 1443460275, end: 3 * 60,
step: 60, step: 60,
}); });
var query = { var query = {
range: { from: moment(1443438674760), to: moment(1443460274760) }, range: { from: time({ minutes: 1, seconds: 3 }), to: time({ minutes: 2, seconds: 3 }) },
targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
interval: '60s', interval: '60s',
}; };
@ -651,7 +683,7 @@ describe('PrometheusDatasource for POST', function() {
result: [ result: [
{ {
metric: { __name__: 'test', job: 'testjob' }, metric: { __name__: 'test', job: 'testjob' },
values: [[1443454528, '3846']], values: [[2 * 60, '3846']],
}, },
], ],
}, },

View File

@ -674,7 +674,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) {
return; return;
} }
if ((ranges.ctrlKey || ranges.metaKey) && dashboard.meta.canEdit) { if ((ranges.ctrlKey || ranges.metaKey) && (dashboard.meta.canEdit || dashboard.meta.canMakeEditable)) {
// Add annotation // Add annotation
setTimeout(() => { setTimeout(() => {
eventManager.updateTime(ranges.xaxis); eventManager.updateTime(ranges.xaxis);
@ -695,7 +695,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) {
return; return;
} }
if ((pos.ctrlKey || pos.metaKey) && dashboard.meta.canEdit) { if ((pos.ctrlKey || pos.metaKey) && (dashboard.meta.canEdit || dashboard.meta.canMakeEditable)) {
// Skip if range selected (added in "plotselected" event handler) // Skip if range selected (added in "plotselected" event handler)
let isRangeSelection = pos.x !== pos.x1; let isRangeSelection = pos.x !== pos.x1;
if (!isRangeSelection) { if (!isRangeSelection) {

View File

@ -287,6 +287,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
destroyScrollbar(); destroyScrollbar();
legendScrollbar = baron(scrollbarParams); legendScrollbar = baron(scrollbarParams);
} }
// #11830 - compensates for Firefox scrollbar calculation error in the baron framework
scroller[0].style.marginRight = '-' + (scroller[0].offsetWidth - scroller[0].clientWidth) + 'px';
legendScrollbar.scroll(); legendScrollbar.scroll();
} }

View File

@ -44,10 +44,18 @@ div.flot-text {
padding: $panel-padding; padding: $panel-padding;
height: calc(100% - 27px); height: calc(100% - 27px);
position: relative; position: relative;
// Fixes scrolling on mobile devices // Fixes scrolling on mobile devices
overflow: auto; overflow: auto;
} }
// For larger screens, set back to hidden to avoid double scroll bars
@include media-breakpoint-up(md) {
.panel-content {
overflow: hidden;
}
}
.panel-title-container { .panel-title-container {
min-height: 9px; min-height: 9px;
cursor: move; cursor: move;

40
scripts/tag_release.sh Executable file
View File

@ -0,0 +1,40 @@
#/bin/bash
# abort if we get any error
set -e
_tag=$1
_branch="$(git rev-parse --abbrev-ref HEAD)"
if [ "${_tag}" == "" ]; then
echo "Missing version param. ex './scripts/tag_release.sh v5.1.1'"
exit 1
fi
if [ "${_branch}" == "master" ]; then
echo "you cannot tag releases from the master branch"
echo "please checkout the release branch"
echo "ex 'git checkout v5.1.x'"
exit 1
fi
# always make sure to pull latest changes from origin
echo "pulling latest changes from ${_branch}"
git pull origin ${_branch}
# create signed tag for latest commit
git tag -s "${_tag}" -m "release ${_tag}"
# verify the signed tag
git tag -v "${_tag}"
echo "Make sure the tag is signed as expected"
echo "press [y] to push the tags"
read -n 1 confirm
if [ "${confirm}" == "y" ]; then
git push origin "${_branch}" --tags
else
echo "Abort! "
fi

View File

@ -31,11 +31,24 @@ const entries = HOT ? {
vendor: require('./dependencies'), vendor: require('./dependencies'),
}; };
const output = HOT ? {
path: path.resolve(__dirname, '../../public/build'),
filename: '[name].[hash].js',
publicPath: "/public/build/",
} : {
path: path.resolve(__dirname, '../../public/build'),
filename: '[name].[hash].js',
// Keep publicPath relative for host.com/grafana/ deployments
publicPath: "public/build/",
};
module.exports = merge(common, { module.exports = merge(common, {
devtool: "cheap-module-source-map", devtool: "cheap-module-source-map",
entry: entries, entry: entries,
output: output,
resolve: { resolve: {
extensions: ['.scss', '.ts', '.tsx', '.es6', '.js', '.json', '.svg', '.woff2', '.png'], extensions: ['.scss', '.ts', '.tsx', '.es6', '.js', '.json', '.svg', '.woff2', '.png'],
}, },
@ -66,23 +79,20 @@ module.exports = merge(common, {
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
exclude: /node_modules/, exclude: /node_modules/,
use: [ use: {
{
loader: 'babel-loader',
options: {
plugins: [
'syntax-dynamic-import',
'react-hot-loader/babel',
],
},
},
{
loader: 'awesome-typescript-loader', loader: 'awesome-typescript-loader',
options: { options: {
useCache: true, useCache: true,
useBabel: HOT,
babelOptions: {
babelrc: false,
plugins: [
'syntax-dynamic-import',
'react-hot-loader/babel'
]
}
}, },
} }
]
}, },
require('./sass.rule.js')({ require('./sass.rule.js')({
sourceMap: true, minimize: false, preserveUrl: HOT sourceMap: true, minimize: false, preserveUrl: HOT