Merge remote-tracking branch 'grafana/master' into influx-db-query2

This commit is contained in:
ryan 2017-04-17 14:17:01 -07:00
commit 8c5972dc55
13 changed files with 134 additions and 94 deletions

View File

@ -13,6 +13,7 @@
## Minor Enchancements
* **Prometheus**: Make Prometheus query field a textarea [#7663](https://github.com/grafana/grafana/issues/7663), thx [@hagen1778](https://github.com/hagen1778)
* **Prometheus**: Step parameter changed semantics to min step to reduce the load on Prometheus and rendering in browser [#8073](https://github.com/grafana/grafana/pull/8073), thx [@bobrik](https://github.com/bobrik)
* **Templating**: Should not be possible to create self-referencing (recursive) template variable definitions [#7614](https://github.com/grafana/grafana/issues/7614) thx [@thuck](https://github.com/thuck)
* **Cloudwatch**: Correctly obtain IAM roles within ECS container tasks [#7892](https://github.com/grafana/grafana/issues/7892) thx [@gomlgs](https://github.com/gomlgs)
* **Units**: New number format: Scientific notation [#7781](https://github.com/grafana/grafana/issues/7781) thx [@cadnce](https://github.com/cadnce)

View File

@ -12,8 +12,8 @@ parent = "http_api"
# Alerting API
You can use the Alerting API to get information about alerts and their states but this API cannot be used to modify the alert.
To create new alerts or modify them you need to update the dashboard json that contains the alerts.
You can use the Alerting API to get information about alerts and their states but this API cannot be used to modify the alert.
To create new alerts or modify them you need to update the dashboard json that contains the alerts.
This API can also be used to create, update and delete alert notifications.
@ -115,7 +115,7 @@ This API can also be used to create, update and delete alert notifications.
HTTP/1.1 200
Content-Type: application/json
{
"id": 1,
"name": "Team A",
@ -127,11 +127,11 @@ This API can also be used to create, update and delete alert notifications.
## Create alert notification
`POST /api/alerts-notifications`
`POST /api/alert-notifications`
**Example Request**:
POST /api/alerts-notifications HTTP/1.1
POST /api/alert-notifications HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
@ -144,29 +144,29 @@ This API can also be used to create, update and delete alert notifications.
"addresses": "carl@grafana.com;dev@grafana.com"
}
}
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{
"id": 1,
"id": 1,
"name": "new alert notification",
"type": "email",
"isDefault": false,
"settings": { addresses: "carl@grafana.com;dev@grafana.com"} }
"created": "2017-01-01 12:34",
"created": "2017-01-01 12:34",
"updated": "2017-01-01 12:34"
}
## Update alert notification
`PUT /api/alerts-notifications/1`
`PUT /api/alert-notifications/1`
**Example Request**:
PUT /api/alerts-notifications/1 HTTP/1.1
PUT /api/alert-notifications/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
@ -176,29 +176,29 @@ This API can also be used to create, update and delete alert notifications.
"name": "new alert notification", //Required
"type": "email", //Required
"isDefault": false,
"settings": {
"settings": {
"addresses: "carl@grafana.com;dev@grafana.com"
}
}
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{
"id": 1,
"id": 1,
"name": "new alert notification",
"type": "email",
"isDefault": false,
"settings": { addresses: "carl@grafana.com;dev@grafana.com"} }
"created": "2017-01-01 12:34",
"created": "2017-01-01 12:34",
"updated": "2017-01-01 12:34"
}
## Delete alert notification
`DELETE /api/alerts-notifications/:notificationId`
`DELETE /api/alert-notifications/:notificationId`
**Example Request**:

View File

@ -34,7 +34,7 @@ func New(logger string, ctx ...interface{}) Logger {
func Trace(format string, v ...interface{}) {
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
message = fmt.Sprintf(format, v...)
} else {
message = format
}
@ -45,7 +45,7 @@ func Trace(format string, v ...interface{}) {
func Debug(format string, v ...interface{}) {
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
message = fmt.Sprintf(format, v...)
} else {
message = format
}
@ -60,7 +60,7 @@ func Debug2(message string, v ...interface{}) {
func Info(format string, v ...interface{}) {
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
message = fmt.Sprintf(format, v...)
} else {
message = format
}
@ -75,7 +75,7 @@ func Info2(message string, v ...interface{}) {
func Warn(format string, v ...interface{}) {
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
message = fmt.Sprintf(format, v...)
} else {
message = format
}
@ -88,7 +88,7 @@ func Warn2(message string, v ...interface{}) {
}
func Error(skip int, format string, v ...interface{}) {
Root.Error(fmt.Sprintf(format, v))
Root.Error(fmt.Sprintf(format, v...))
}
func Error2(message string, v ...interface{}) {
@ -96,7 +96,7 @@ func Error2(message string, v ...interface{}) {
}
func Critical(skip int, format string, v ...interface{}) {
Root.Crit(fmt.Sprintf(format, v))
Root.Crit(fmt.Sprintf(format, v...))
}
func Fatal(skip int, format string, v ...interface{}) {

View File

@ -13,7 +13,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
func initContextWithAuthProxy(ctx *Context) bool {
func initContextWithAuthProxy(ctx *Context, orgId int64) bool {
if !setting.AuthProxyEnabled {
return false
}
@ -30,6 +30,7 @@ func initContextWithAuthProxy(ctx *Context) bool {
}
query := getSignedInUserQueryForProxyAuth(proxyHeaderValue)
query.OrgId = orgId
if err := bus.Dispatch(query); err != nil {
if err != m.ErrUserNotFound {
ctx.Handle(500, "Failed to find user specified in auth proxy header", err)
@ -46,7 +47,7 @@ func initContextWithAuthProxy(ctx *Context) bool {
ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
return true
}
query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id}
query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id, OrgId: orgId}
if err := bus.Dispatch(query); err != nil {
ctx.Handle(500, "Failed find user after creation", err)
return true

View File

@ -39,6 +39,12 @@ func GetContextHandler() macaron.Handler {
Logger: log.New("context"),
}
orgId := int64(0)
orgIdHeader := ctx.Req.Header.Get("X-Grafana-Org-Id")
if orgIdHeader != "" {
orgId, _ = strconv.ParseInt(orgIdHeader, 10, 64)
}
// the order in which these are tested are important
// look for api key in Authorization header first
// then init session and look for userId in session
@ -46,9 +52,9 @@ func GetContextHandler() macaron.Handler {
// then test if anonymous access is enabled
if initContextWithRenderAuth(ctx) ||
initContextWithApiKey(ctx) ||
initContextWithBasicAuth(ctx) ||
initContextWithAuthProxy(ctx) ||
initContextWithUserSessionCookie(ctx) ||
initContextWithBasicAuth(ctx, orgId) ||
initContextWithAuthProxy(ctx, orgId) ||
initContextWithUserSessionCookie(ctx, orgId) ||
initContextWithAnonymousUser(ctx) {
}
@ -68,18 +74,18 @@ func initContextWithAnonymousUser(ctx *Context) bool {
if err := bus.Dispatch(&orgQuery); err != nil {
log.Error(3, "Anonymous access organization error: '%s': %s", setting.AnonymousOrgName, err)
return false
} else {
ctx.IsSignedIn = false
ctx.AllowAnonymous = true
ctx.SignedInUser = &m.SignedInUser{}
ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole)
ctx.OrgId = orgQuery.Result.Id
ctx.OrgName = orgQuery.Result.Name
return true
}
ctx.IsSignedIn = false
ctx.AllowAnonymous = true
ctx.SignedInUser = &m.SignedInUser{}
ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole)
ctx.OrgId = orgQuery.Result.Id
ctx.OrgName = orgQuery.Result.Name
return true
}
func initContextWithUserSessionCookie(ctx *Context) bool {
func initContextWithUserSessionCookie(ctx *Context, orgId int64) bool {
// initialize session
if err := ctx.Session.Start(ctx); err != nil {
ctx.Logger.Error("Failed to start session", "error", err)
@ -91,15 +97,15 @@ func initContextWithUserSessionCookie(ctx *Context) bool {
return false
}
query := m.GetSignedInUserQuery{UserId: userId}
query := m.GetSignedInUserQuery{UserId: userId, OrgId: orgId}
if err := bus.Dispatch(&query); err != nil {
ctx.Logger.Error("Failed to get user with id", "userId", userId)
return false
} else {
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
return true
}
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
return true
}
func initContextWithApiKey(ctx *Context) bool {
@ -114,30 +120,31 @@ func initContextWithApiKey(ctx *Context) bool {
ctx.JsonApiErr(401, "Invalid API key", err)
return true
}
// fetch key
keyQuery := m.GetApiKeyByNameQuery{KeyName: decoded.Name, OrgId: decoded.OrgId}
if err := bus.Dispatch(&keyQuery); err != nil {
ctx.JsonApiErr(401, "Invalid API key", err)
return true
} else {
apikey := keyQuery.Result
}
// validate api key
if !apikeygen.IsValid(decoded, apikey.Key) {
ctx.JsonApiErr(401, "Invalid API key", err)
return true
}
apikey := keyQuery.Result
ctx.IsSignedIn = true
ctx.SignedInUser = &m.SignedInUser{}
ctx.OrgRole = apikey.Role
ctx.ApiKeyId = apikey.Id
ctx.OrgId = apikey.OrgId
// validate api key
if !apikeygen.IsValid(decoded, apikey.Key) {
ctx.JsonApiErr(401, "Invalid API key", err)
return true
}
ctx.IsSignedIn = true
ctx.SignedInUser = &m.SignedInUser{}
ctx.OrgRole = apikey.Role
ctx.ApiKeyId = apikey.Id
ctx.OrgId = apikey.OrgId
return true
}
func initContextWithBasicAuth(ctx *Context) bool {
func initContextWithBasicAuth(ctx *Context, orgId int64) bool {
if !setting.BasicAuthEnabled {
return false
@ -168,15 +175,15 @@ func initContextWithBasicAuth(ctx *Context) bool {
return true
}
query := m.GetSignedInUserQuery{UserId: user.Id}
query := m.GetSignedInUserQuery{UserId: user.Id, OrgId: orgId}
if err := bus.Dispatch(&query); err != nil {
ctx.JsonApiErr(401, "Authentication error", err)
return true
} else {
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
return true
}
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
return true
}
// Handle handles and logs error by given status.

View File

@ -117,6 +117,7 @@ type GetSignedInUserQuery struct {
UserId int64
Login string
Email string
OrgId int64
Result *SignedInUser
}

View File

@ -1,6 +1,7 @@
package sqlstore
import (
"strconv"
"strings"
"time"
@ -273,7 +274,7 @@ func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
}
if !valid {
return fmt.Errorf("user does not belong ot org")
return fmt.Errorf("user does not belong to org")
}
return inTransaction(func(sess *xorm.Session) error {
@ -319,19 +320,24 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
}
func GetSignedInUser(query *m.GetSignedInUserQuery) error {
orgId := "u.org_id"
if query.OrgId > 0 {
orgId = strconv.FormatInt(query.OrgId, 10)
}
var rawSql = `SELECT
u.id as user_id,
u.is_admin as is_grafana_admin,
u.email as email,
u.login as login,
u.name as name,
u.help_flags1 as help_flags1,
org.name as org_name,
org_user.role as org_role,
org.id as org_id
FROM ` + dialect.Quote("user") + ` as u
LEFT OUTER JOIN org_user on org_user.org_id = u.org_id and org_user.user_id = u.id
LEFT OUTER JOIN org on org.id = u.org_id `
u.id as user_id,
u.is_admin as is_grafana_admin,
u.email as email,
u.login as login,
u.name as name,
u.help_flags1 as help_flags1,
org.name as org_name,
org_user.role as org_role,
org.id as org_id
FROM ` + dialect.Quote("user") + ` as u
LEFT OUTER JOIN org_user on org_user.org_id = ` + orgId + ` and org_user.user_id = u.id
LEFT OUTER JOIN org on org.id = org_user.org_id `
sess := x.Table("user")
if query.UserId > 0 {

View File

@ -40,7 +40,14 @@ export function infoPopover() {
openOn: openOn,
hoverOpenDelay: 400,
tetherOptions: {
offset: offset
offset: offset,
constraints: [
{
to: 'window',
attachment: 'together',
pin: true
}
],
}
});

View File

@ -9,8 +9,8 @@ export class BackendSrv {
inFlightRequests = {};
HTTP_REQUEST_CANCELLED = -1;
/** @ngInject */
constructor(private $http, private alertSrv, private $rootScope, private $q, private $timeout) {
/** @ngInject */
constructor(private $http, private alertSrv, private $rootScope, private $q, private $timeout, private contextSrv) {
}
get(url, params?) {
@ -63,12 +63,18 @@ export class BackendSrv {
request(options) {
options.retry = options.retry || 0;
var requestIsLocal = options.url.indexOf('/') === 0;
var requestIsLocal = !options.url.match(/^http/);
var firstAttempt = options.retry === 0;
if (requestIsLocal && !options.hasSubUrl) {
options.url = config.appSubUrl + options.url;
options.hasSubUrl = true;
if (requestIsLocal) {
if (this.contextSrv.user && this.contextSrv.user.orgId) {
options.headers = options.headers || {};
options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
}
if (options.url.indexOf("/") === 0) {
options.url = options.url.substring(1);
}
}
return this.$http(options).then(results => {
@ -125,16 +131,23 @@ export class BackendSrv {
this.addCanceler(requestId, canceler);
}
var requestIsLocal = options.url.indexOf('/') === 0;
var requestIsLocal = !options.url.match(/^http/);
var firstAttempt = options.retry === 0;
if (requestIsLocal && !options.hasSubUrl && options.retry === 0) {
options.url = config.appSubUrl + options.url;
}
if (requestIsLocal) {
if (this.contextSrv.user && this.contextSrv.user.orgId) {
options.headers = options.headers || {};
options.headers['X-Grafana-Org-Id'] = this.contextSrv.user.orgId;
}
if (requestIsLocal && options.headers && options.headers.Authorization) {
options.headers['X-DS-Authorization'] = options.headers.Authorization;
delete options.headers.Authorization;
if (options.url.indexOf("/") === 0) {
options.url = options.url.substring(1);
}
if (options.headers && options.headers.Authorization) {
options.headers['X-DS-Authorization'] = options.headers.Authorization;
delete options.headers.Authorization;
}
}
return this.$http(options).catch(err => {

View File

@ -54,7 +54,7 @@
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 2">
<h5>Stacking and fill</h5>
<ul>
<li>When stacking is enabled it important that points align</li>
<li>When stacking is enabled it is important that points align</li>
<li>If there are missing points for one series it can cause gaps or missing bars</li>
<li>You must use fill(0), and select a group by time low limit</li>
<li>Use the group by time option below your queries and specify for example &gt;10s if your metrics are written every 10 seconds</li>

View File

@ -89,7 +89,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
var intervalFactor = target.intervalFactor || 1;
target.step = query.step = this.calculateInterval(interval, intervalFactor);
var range = Math.ceil(end - start);
target.step = query.step = this.adjustStep(query.step, range);
target.step = query.step = this.adjustStep(query.step, this.intervalSeconds(options.interval), range);
queries.push(query);
});
@ -122,13 +122,13 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
});
};
this.adjustStep = function(step, range) {
this.adjustStep = function(step, autoStep, range) {
// Prometheus drop query if range/step > 11000
// calibrate step if it is too big
if (step !== 0 && range / step > 11000) {
return Math.ceil(range / 11000);
}
return step;
return Math.max(step, autoStep);
};
this.performTimeSeriesQuery = function(query, start, end) {
@ -189,7 +189,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
var end = this.getPrometheusTime(options.range.to, true);
var query = {
expr: interpolated,
step: this.adjustStep(kbn.interval_to_seconds(step), Math.ceil(end - start)) + 's'
step: this.adjustStep(kbn.interval_to_seconds(step), 0, Math.ceil(end - start)) + 's'
};
var self = this;
@ -229,6 +229,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
};
this.calculateInterval = function(interval, intervalFactor) {
return Math.ceil(this.intervalSeconds(interval) * intervalFactor);
};
this.intervalSeconds = function(interval) {
var m = interval.match(durationSplitRegexp);
var dur = moment.duration(parseInt(m[1]), m[2]);
var sec = dur.asSeconds();
@ -236,7 +240,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
sec = 1;
}
return Math.ceil(sec * intervalFactor);
return sec;
};
this.transformMetricData = function(md, options, start, end) {

View File

@ -14,7 +14,7 @@
</input>
</div>
<div class="gf-form">
<label class="gf-form-label width-5">Step</label>
<label class="gf-form-label">Min step</label>
<input type="text" class="gf-form-input max-width-5" ng-model="ctrl.target.interval"
data-placement="right"
spellcheck='false'

View File

@ -58,7 +58,7 @@ class GettingStartedPanelCtrl extends PanelCtrl {
icon: 'icon-gf icon-gf-users',
href: 'org/users?gettingstarted',
check: () => {
return this.backendSrv.get('api/org/users').then(res => {
return this.backendSrv.get('/api/org/users').then(res => {
return res.length > 1;
});
}
@ -71,7 +71,7 @@ class GettingStartedPanelCtrl extends PanelCtrl {
icon: 'icon-gf icon-gf-apps',
href: 'https://grafana.com/plugins?utm_source=grafana_getting_started',
check: () => {
return this.backendSrv.get('api/plugins', {embedded: 0, core: 0}).then(plugins => {
return this.backendSrv.get('/api/plugins', {embedded: 0, core: 0}).then(plugins => {
return plugins.length > 0;
});
}