Merge remote-tracking branch 'origin/master' into alerting_reminder

This commit is contained in:
Marcus Efraimsson 2018-08-22 18:08:50 +02:00
commit 34e448c6d4
No known key found for this signature in database
GPG Key ID: EBFE0FB04612DD4A
23 changed files with 574 additions and 71 deletions

2
.gitignore vendored
View File

@ -71,4 +71,4 @@ debug.test
/vendor/**/appengine*
*.orig
/devenv/dashboards/bulk-testing/*.json
/devenv/bulk-dashboards/*.json

View File

@ -20,6 +20,7 @@
* **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
* **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie)
* **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
* **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890)
* **Postgres/MySQL/MSSQL**: New $__unixEpochGroup and $__unixEpochGroupAlias macros [#12892](https://github.com/grafana/grafana/issues/12892), thx [@svenklemm](https://github.com/svenklemm)
* **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm)
* **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)

View File

@ -5,5 +5,5 @@ providers:
folder: 'Bulk dashboards'
type: file
options:
path: devenv/dashboards/bulk-testing
path: devenv/bulk-dashboards

View File

@ -7,11 +7,11 @@ bulkDashboard() {
COUNTER=0
MAX=400
while [ $COUNTER -lt $MAX ]; do
jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }"
jsonnet -o "bulk-dashboards/dashboard${COUNTER}.json" -e "local bulkDash = import 'bulk-dashboards/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }"
let COUNTER=COUNTER+1
done
ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
ln -s -f -r ./bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
}
requiresJsonnet() {

View File

@ -153,7 +153,7 @@ There are a couple of configuration options which need to be set up in Grafana U
Once these two properties are set, you can send the alerts to Kafka for further processing or throttling.
### All supported notifier
### All supported notifiers
Name | Type |Support images | Support reminders
-----|------------ | ------ | ------ |
@ -170,6 +170,7 @@ Threema | `threema` | yes | yes
Pushover | `pushover` | no | yes
Telegram | `telegram` | no | yes
Line | `line` | no | yes
Microsoft Teams | `teams` | yes | yes
Prometheus Alertmanager | `prometheus-alertmanager` | no | no

View File

@ -78,7 +78,13 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
user := userQuery.Result
// validate remember me cookie
if val, _ := c.GetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName); val != user.Login {
signingKey := user.Rands + user.Password
if len(signingKey) < 10 {
c.Logger.Error("Invalid user signingKey")
return false
}
if val, _ := c.GetSuperSecureCookie(signingKey, setting.CookieRememberName); val != user.Login {
return false
}

View File

@ -1,6 +1,12 @@
package migrations
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
import (
"fmt"
"github.com/go-xorm/xorm"
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/util"
)
func addUserMigrations(mg *Migrator) {
userV1 := Table{
@ -107,4 +113,37 @@ func addUserMigrations(mg *Migrator) {
mg.AddMigration("Add last_seen_at column to user", NewAddColumnMigration(userV2, &Column{
Name: "last_seen_at", Type: DB_DateTime, Nullable: true,
}))
// Adds salt & rands for old users who used ldap or oauth
mg.AddMigration("Add missing user data", &AddMissingUserSaltAndRandsMigration{})
}
type AddMissingUserSaltAndRandsMigration struct {
MigrationBase
}
func (m *AddMissingUserSaltAndRandsMigration) Sql(dialect Dialect) string {
return "code migration"
}
type TempUserDTO struct {
Id int64
Login string
}
func (m *AddMissingUserSaltAndRandsMigration) Exec(sess *xorm.Session, mg *Migrator) error {
users := make([]*TempUserDTO, 0)
err := sess.Sql(fmt.Sprintf("SELECT id, login from %s WHERE rands = ''", mg.Dialect.Quote("user"))).Find(&users)
if err != nil {
return err
}
for _, user := range users {
_, err := sess.Exec("UPDATE "+mg.Dialect.Quote("user")+" SET salt = ?, rands = ? WHERE id = ?", util.GetRandomString(10), util.GetRandomString(10), user.Id)
if err != nil {
return err
}
}
return nil
}

View File

@ -12,7 +12,7 @@ import (
type Migrator struct {
x *xorm.Engine
dialect Dialect
Dialect Dialect
migrations []Migration
Logger log.Logger
}
@ -31,7 +31,7 @@ func NewMigrator(engine *xorm.Engine) *Migrator {
mg.x = engine
mg.Logger = log.New("migrator")
mg.migrations = make([]Migration, 0)
mg.dialect = NewDialect(mg.x)
mg.Dialect = NewDialect(mg.x)
return mg
}
@ -86,7 +86,7 @@ func (mg *Migrator) Start() error {
continue
}
sql := m.Sql(mg.dialect)
sql := m.Sql(mg.Dialect)
record := MigrationLog{
MigrationId: m.Id(),
@ -122,7 +122,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
condition := m.GetCondition()
if condition != nil {
sql, args := condition.Sql(mg.dialect)
sql, args := condition.Sql(mg.Dialect)
results, err := sess.SQL(sql).Query(args...)
if err != nil || len(results) == 0 {
mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id())
@ -130,7 +130,13 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
}
}
_, err := sess.Exec(m.Sql(mg.dialect))
var err error
if codeMigration, ok := m.(CodeMigration); ok {
err = codeMigration.Exec(sess, mg)
} else {
_, err = sess.Exec(m.Sql(mg.Dialect))
}
if err != nil {
mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
return err

View File

@ -3,6 +3,8 @@ package migrator
import (
"fmt"
"strings"
"github.com/go-xorm/xorm"
)
const (
@ -19,6 +21,11 @@ type Migration interface {
GetCondition() MigrationCondition
}
type CodeMigration interface {
Migration
Exec(sess *xorm.Session, migrator *Migrator) error
}
type SQLType string
type ColumnType string

View File

@ -113,9 +113,10 @@ func CreateUser(ctx context.Context, cmd *m.CreateUserCommand) error {
LastSeenAt: time.Now().AddDate(-10, 0, 0),
}
user.Salt = util.GetRandomString(10)
user.Rands = util.GetRandomString(10)
if len(cmd.Password) > 0 {
user.Salt = util.GetRandomString(10)
user.Rands = util.GetRandomString(10)
user.Password = util.EncodePassword(cmd.Password, user.Salt)
}

View File

@ -15,6 +15,28 @@ func TestUserDataAccess(t *testing.T) {
Convey("Testing DB", t, func() {
InitTestDB(t)
Convey("Creating a user", func() {
cmd := &m.CreateUserCommand{
Email: "usertest@test.com",
Name: "user name",
Login: "user_test_login",
}
err := CreateUser(context.Background(), cmd)
So(err, ShouldBeNil)
Convey("Loading a user", func() {
query := m.GetUserByIdQuery{Id: cmd.Result.Id}
err := GetUserById(&query)
So(err, ShouldBeNil)
So(query.Result.Email, ShouldEqual, "usertest@test.com")
So(query.Result.Password, ShouldEqual, "")
So(query.Result.Rands, ShouldHaveLength, 10)
So(query.Result.Salt, ShouldHaveLength, 10)
})
})
Convey("Given 5 users", func() {
var err error
var cmd *m.CreateUserCommand

View File

@ -207,6 +207,7 @@ export class Explore extends React.Component<any, IExploreState> {
datasourceError: null,
datasourceLoading: true,
graphResult: null,
latency: 0,
logsResult: null,
queryErrors: [],
queryHints: [],
@ -254,7 +255,10 @@ export class Explore extends React.Component<any, IExploreState> {
this.setState({
graphResult: null,
logsResult: null,
latency: 0,
queries: ensureQueries(),
queryErrors: [],
queryHints: [],
tableResult: null,
});
};
@ -276,8 +280,10 @@ export class Explore extends React.Component<any, IExploreState> {
onClickSplit = () => {
const { onChangeSplit } = this.props;
const state = { ...this.state };
state.queries = state.queries.map(({ edited, ...rest }) => rest);
if (onChangeSplit) {
onChangeSplit(true, this.state);
onChangeSplit(true, state);
}
};

View File

@ -94,6 +94,25 @@ describe('PromQueryField typeahead handling', () => {
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
it('returns label suggestions on label context but leaves out labels that already exist', () => {
const instance = shallow(
<PromQueryField {...defaultProps} labelKeys={{ '{job="foo"}': ['bar', 'job'] }} />
).instance() as PromQueryField;
const value = Plain.deserialize('{job="foo",}');
const range = value.selection.merge({
anchorOffset: 11,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.getTypeahead({
text: '',
prefix: '',
wrapperClasses: ['context-labels'],
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
it('returns a refresher on label context and unavailable metric', () => {
const instance = shallow(
<PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />

View File

@ -10,7 +10,7 @@ import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index';
import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql';
import BracesPlugin from './slate-plugins/braces';
import RunnerPlugin from './slate-plugins/runner';
import { processLabels, RATE_RANGES, cleanText, getCleanSelector } from './utils/prometheus';
import { processLabels, RATE_RANGES, cleanText, parseSelector } from './utils/prometheus';
import TypeaheadField, {
Suggestion,
@ -328,7 +328,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
// foo{bar="1"}
const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
const selector = getCleanSelector(selectorString, selectorString.length - 2);
const selector = parseSelector(selectorString, selectorString.length - 2).selector;
const labelKeys = this.state.labelKeys[selector];
if (labelKeys) {
@ -353,12 +353,15 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
// Get normalized selector
let selector;
let parsedSelector;
try {
selector = getCleanSelector(line, cursorOffset);
parsedSelector = parseSelector(line, cursorOffset);
selector = parsedSelector.selector;
} catch {
selector = EMPTY_SELECTOR;
}
const containsMetric = selector.indexOf('__name__=') > -1;
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
// Label values
@ -374,8 +377,11 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
// Label keys
const labelKeys = this.state.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
if (labelKeys) {
context = 'context-labels';
suggestions.push({ label: `Labels`, items: labelKeys.map(wrapLabel) });
const possibleKeys = _.difference(labelKeys, existingKeys);
if (possibleKeys.length > 0) {
context = 'context-labels';
suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
}
}
}

View File

@ -331,7 +331,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
}
break;
}
case 'Enter':
case 'Tab': {
if (this.menuEl) {
// Dont blur input

View File

@ -1,33 +1,61 @@
import { getCleanSelector } from './prometheus';
import { parseSelector } from './prometheus';
describe('parseSelector()', () => {
let parsed;
describe('getCleanSelector()', () => {
it('returns a clean selector from an empty selector', () => {
expect(getCleanSelector('{}', 1)).toBe('{}');
parsed = parseSelector('{}', 1);
expect(parsed.selector).toBe('{}');
expect(parsed.labelKeys).toEqual([]);
});
it('throws if selector is broken', () => {
expect(() => getCleanSelector('{foo')).toThrow();
expect(() => parseSelector('{foo')).toThrow();
});
it('returns the selector sorted by label key', () => {
expect(getCleanSelector('{foo="bar"}')).toBe('{foo="bar"}');
expect(getCleanSelector('{foo="bar",baz="xx"}')).toBe('{baz="xx",foo="bar"}');
parsed = parseSelector('{foo="bar"}');
expect(parsed.selector).toBe('{foo="bar"}');
expect(parsed.labelKeys).toEqual(['foo']);
parsed = parseSelector('{foo="bar",baz="xx"}');
expect(parsed.selector).toBe('{baz="xx",foo="bar"}');
});
it('returns a clean selector from an incomplete one', () => {
expect(getCleanSelector('{foo}')).toBe('{}');
expect(getCleanSelector('{foo="bar",baz}')).toBe('{foo="bar"}');
expect(getCleanSelector('{foo="bar",baz="}')).toBe('{foo="bar"}');
parsed = parseSelector('{foo}');
expect(parsed.selector).toBe('{}');
parsed = parseSelector('{foo="bar",baz}');
expect(parsed.selector).toBe('{foo="bar"}');
parsed = parseSelector('{foo="bar",baz="}');
expect(parsed.selector).toBe('{foo="bar"}');
});
it('throws if not inside a selector', () => {
expect(() => getCleanSelector('foo{}', 0)).toThrow();
expect(() => getCleanSelector('foo{} + bar{}', 5)).toThrow();
expect(() => parseSelector('foo{}', 0)).toThrow();
expect(() => parseSelector('foo{} + bar{}', 5)).toThrow();
});
it('returns the selector nearest to the cursor offset', () => {
expect(() => getCleanSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow();
expect(getCleanSelector('{foo="bar"} + {foo="bar"}', 1)).toBe('{foo="bar"}');
expect(getCleanSelector('{foo="bar"} + {baz="xx"}', 1)).toBe('{foo="bar"}');
expect(getCleanSelector('{baz="xx"} + {foo="bar"}', 16)).toBe('{foo="bar"}');
expect(() => parseSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow();
parsed = parseSelector('{foo="bar"} + {foo="bar"}', 1);
expect(parsed.selector).toBe('{foo="bar"}');
parsed = parseSelector('{foo="bar"} + {baz="xx"}', 1);
expect(parsed.selector).toBe('{foo="bar"}');
parsed = parseSelector('{baz="xx"} + {foo="bar"}', 16);
expect(parsed.selector).toBe('{foo="bar"}');
});
it('returns a selector with metric if metric is given', () => {
expect(getCleanSelector('bar{foo}', 4)).toBe('{__name__="bar"}');
expect(getCleanSelector('baz{foo="bar"}', 12)).toBe('{__name__="baz",foo="bar"}');
parsed = parseSelector('bar{foo}', 4);
expect(parsed.selector).toBe('{__name__="bar"}');
parsed = parseSelector('baz{foo="bar"}', 12);
expect(parsed.selector).toBe('{__name__="baz",foo="bar"}');
});
});

View File

@ -29,11 +29,14 @@ export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/;
const selectorRegexp = /\{[^}]*?\}/;
const labelRegexp = /\b\w+="[^"\n]*?"/g;
export function getCleanSelector(query: string, cursorOffset = 1): string {
export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } {
if (!query.match(selectorRegexp)) {
// Special matcher for metrics
if (query.match(/^\w+$/)) {
return `{__name__="${query}"}`;
return {
selector: `{__name__="${query}"}`,
labelKeys: ['__name__'],
};
}
throw new Error('Query must contain a selector: ' + query);
}
@ -79,10 +82,10 @@ export function getCleanSelector(query: string, cursorOffset = 1): string {
}
// Build sorted selector
const cleanSelector = Object.keys(labels)
.sort()
.map(key => `${key}=${labels[key]}`)
.join(',');
const labelKeys = Object.keys(labels).sort();
const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(',');
return ['{', cleanSelector, '}'].join('');
const selectorString = ['{', cleanSelector, '}'].join('');
return { labelKeys, selector: selectorString };
}

View File

@ -0,0 +1,108 @@
import { Graph } from './dag';
describe('Directed acyclic graph', () => {
describe('Given a graph with nodes with different links in between them', () => {
let dag = new Graph();
let nodeA = dag.createNode('A');
let nodeB = dag.createNode('B');
let nodeC = dag.createNode('C');
let nodeD = dag.createNode('D');
let nodeE = dag.createNode('E');
let nodeF = dag.createNode('F');
let nodeG = dag.createNode('G');
let nodeH = dag.createNode('H');
let nodeI = dag.createNode('I');
dag.link([nodeB, nodeC, nodeD, nodeE, nodeF, nodeG, nodeH], nodeA);
dag.link([nodeC, nodeD, nodeE, nodeF, nodeI], nodeB);
dag.link([nodeD, nodeE, nodeF, nodeG], nodeC);
dag.link([nodeE, nodeF], nodeD);
dag.link([nodeF, nodeG], nodeE);
//printGraph(dag);
it('nodes in graph should have expected edges', () => {
expect(nodeA.inputEdges).toHaveLength(7);
expect(nodeA.outputEdges).toHaveLength(0);
expect(nodeA.edges).toHaveLength(7);
expect(nodeB.inputEdges).toHaveLength(5);
expect(nodeB.outputEdges).toHaveLength(1);
expect(nodeB.edges).toHaveLength(6);
expect(nodeC.inputEdges).toHaveLength(4);
expect(nodeC.outputEdges).toHaveLength(2);
expect(nodeC.edges).toHaveLength(6);
expect(nodeD.inputEdges).toHaveLength(2);
expect(nodeD.outputEdges).toHaveLength(3);
expect(nodeD.edges).toHaveLength(5);
expect(nodeE.inputEdges).toHaveLength(2);
expect(nodeE.outputEdges).toHaveLength(4);
expect(nodeE.edges).toHaveLength(6);
expect(nodeF.inputEdges).toHaveLength(0);
expect(nodeF.outputEdges).toHaveLength(5);
expect(nodeF.edges).toHaveLength(5);
expect(nodeG.inputEdges).toHaveLength(0);
expect(nodeG.outputEdges).toHaveLength(3);
expect(nodeG.edges).toHaveLength(3);
expect(nodeH.inputEdges).toHaveLength(0);
expect(nodeH.outputEdges).toHaveLength(1);
expect(nodeH.edges).toHaveLength(1);
expect(nodeI.inputEdges).toHaveLength(0);
expect(nodeI.outputEdges).toHaveLength(1);
expect(nodeI.edges).toHaveLength(1);
expect(nodeA.getEdgeFrom(nodeB)).not.toBeUndefined();
expect(nodeB.getEdgeTo(nodeA)).not.toBeUndefined();
});
it('when optimizing input edges for node A should return node B and H', () => {
const actual = nodeA.getOptimizedInputEdges().map(e => e.inputNode);
expect(actual).toHaveLength(2);
expect(actual).toEqual(expect.arrayContaining([nodeB, nodeH]));
});
it('when optimizing input edges for node B should return node C', () => {
const actual = nodeB.getOptimizedInputEdges().map(e => e.inputNode);
expect(actual).toHaveLength(2);
expect(actual).toEqual(expect.arrayContaining([nodeC, nodeI]));
});
it('when optimizing input edges for node C should return node D', () => {
const actual = nodeC.getOptimizedInputEdges().map(e => e.inputNode);
expect(actual).toHaveLength(1);
expect(actual).toEqual(expect.arrayContaining([nodeD]));
});
it('when optimizing input edges for node D should return node E', () => {
const actual = nodeD.getOptimizedInputEdges().map(e => e.inputNode);
expect(actual).toHaveLength(1);
expect(actual).toEqual(expect.arrayContaining([nodeE]));
});
it('when optimizing input edges for node E should return node F and G', () => {
const actual = nodeE.getOptimizedInputEdges().map(e => e.inputNode);
expect(actual).toHaveLength(2);
expect(actual).toEqual(expect.arrayContaining([nodeF, nodeG]));
});
it('when optimizing input edges for node F should return zero nodes', () => {
const actual = nodeF.getOptimizedInputEdges();
expect(actual).toHaveLength(0);
});
it('when optimizing input edges for node G should return zero nodes', () => {
const actual = nodeG.getOptimizedInputEdges();
expect(actual).toHaveLength(0);
});
it('when optimizing input edges for node H should return zero nodes', () => {
const actual = nodeH.getOptimizedInputEdges();
expect(actual).toHaveLength(0);
});
});
});

View File

@ -0,0 +1,201 @@
export class Edge {
inputNode: Node;
outputNode: Node;
_linkTo(node, direction) {
if (direction <= 0) {
node.inputEdges.push(this);
}
if (direction >= 0) {
node.outputEdges.push(this);
}
node.edges.push(this);
}
link(inputNode: Node, outputNode: Node) {
this.unlink();
this.inputNode = inputNode;
this.outputNode = outputNode;
this._linkTo(inputNode, 1);
this._linkTo(outputNode, -1);
return this;
}
unlink() {
let pos;
let inode = this.inputNode;
let onode = this.outputNode;
if (!(inode && onode)) {
return;
}
pos = inode.edges.indexOf(this);
if (pos > -1) {
inode.edges.splice(pos, 1);
}
pos = onode.edges.indexOf(this);
if (pos > -1) {
onode.edges.splice(pos, 1);
}
pos = inode.outputEdges.indexOf(this);
if (pos > -1) {
inode.outputEdges.splice(pos, 1);
}
pos = onode.inputEdges.indexOf(this);
if (pos > -1) {
onode.inputEdges.splice(pos, 1);
}
this.inputNode = null;
this.outputNode = null;
}
}
export class Node {
name: string;
edges: Edge[];
inputEdges: Edge[];
outputEdges: Edge[];
constructor(name: string) {
this.name = name;
this.edges = [];
this.inputEdges = [];
this.outputEdges = [];
}
getEdgeFrom(from: string | Node): Edge {
if (!from) {
return null;
}
if (typeof from === 'object') {
return this.inputEdges.find(e => e.inputNode.name === from.name);
}
return this.inputEdges.find(e => e.inputNode.name === from);
}
getEdgeTo(to: string | Node): Edge {
if (!to) {
return null;
}
if (typeof to === 'object') {
return this.outputEdges.find(e => e.outputNode.name === to.name);
}
return this.outputEdges.find(e => e.outputNode.name === to);
}
getOptimizedInputEdges(): Edge[] {
let toBeRemoved = [];
this.inputEdges.forEach(e => {
let inputEdgesNodes = e.inputNode.inputEdges.map(e => e.inputNode);
inputEdgesNodes.forEach(n => {
let edgeToRemove = n.getEdgeTo(this.name);
if (edgeToRemove) {
toBeRemoved.push(edgeToRemove);
}
});
});
return this.inputEdges.filter(e => toBeRemoved.indexOf(e) === -1);
}
}
export class Graph {
nodes = {};
constructor() {}
createNode(name: string): Node {
const n = new Node(name);
this.nodes[name] = n;
return n;
}
createNodes(names: string[]): Node[] {
let nodes = [];
names.forEach(name => {
nodes.push(this.createNode(name));
});
return nodes;
}
link(input: string | string[] | Node | Node[], output: string | string[] | Node | Node[]): Edge[] {
let inputArr = [];
let outputArr = [];
let inputNodes = [];
let outputNodes = [];
if (input instanceof Array) {
inputArr = input;
} else {
inputArr = [input];
}
if (output instanceof Array) {
outputArr = output;
} else {
outputArr = [output];
}
for (let n = 0; n < inputArr.length; n++) {
const i = inputArr[n];
if (typeof i === 'string') {
inputNodes.push(this.getNode(i));
} else {
inputNodes.push(i);
}
}
for (let n = 0; n < outputArr.length; n++) {
const i = outputArr[n];
if (typeof i === 'string') {
outputNodes.push(this.getNode(i));
} else {
outputNodes.push(i);
}
}
let edges = [];
inputNodes.forEach(input => {
outputNodes.forEach(output => {
edges.push(this.createEdge().link(input, output));
});
});
return edges;
}
createEdge(): Edge {
return new Edge();
}
getNode(name: string): Node {
return this.nodes[name];
}
}
export const printGraph = (g: Graph) => {
Object.keys(g.nodes).forEach(name => {
const n = g.nodes[name];
let outputEdges = n.outputEdges.map(e => e.outputNode.name).join(', ');
if (!outputEdges) {
outputEdges = '<none>';
}
let inputEdges = n.inputEdges.map(e => e.inputNode.name).join(', ');
if (!inputEdges) {
inputEdges = '<none>';
}
console.log(`${n.name}:\n - links to: ${outputEdges}\n - links from: ${inputEdges}`);
});
};

View File

@ -2,6 +2,7 @@ import angular from 'angular';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import { variableTypes } from './variable';
import { Graph } from 'app/core/utils/dag';
export class VariableSrv {
dashboard: any;
@ -120,16 +121,13 @@ export class VariableSrv {
return this.$q.when();
}
// cascade updates to variables that use this variable
var promises = _.map(this.variables, otherVariable => {
if (otherVariable === variable) {
return;
}
if (otherVariable.dependsOn(variable)) {
return this.updateOptions(otherVariable);
}
});
const g = this.createGraph();
const promises = g
.getNode(variable.name)
.getOptimizedInputEdges()
.map(e => {
return this.updateOptions(this.variables.find(v => v.name === e.inputNode.name));
});
return this.$q.all(promises).then(() => {
if (emitChangeEvents) {
@ -288,6 +286,26 @@ export class VariableSrv {
filter.operator = options.operator;
this.variableUpdated(variable, true);
}
createGraph() {
let g = new Graph();
this.variables.forEach(v1 => {
g.createNode(v1.name);
this.variables.forEach(v2 => {
if (v1 === v2) {
return;
}
if (v1.dependsOn(v2)) {
g.link(v1.name, v2.name);
}
});
});
return g;
}
}
coreModule.service('variableSrv', VariableSrv);

View File

@ -18,18 +18,18 @@
<span class="gf-form-label width-9">For each value of</span>
<dash-repeat-option panel="ctrl.panel"></dash-repeat-option>
</div>
<div class="gf-form" ng-show="ctrl.panel.repeat">
<span class="gf-form-label width-9">Min width</span>
<select class="gf-form-input" ng-model="ctrl.panel.minSpan" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]">
<option value=""></option>
</select>
</div>
<div class="gf-form" ng-show="ctrl.panel.repeat">
<span class="gf-form-label width-9">Direction</span>
<select class="gf-form-input" ng-model="ctrl.panel.repeatDirection" ng-options="f.value as f.text for f in [{value: 'v', text: 'Vertical'}, {value: 'h', text: 'Horizontal'}]">
<option value=""></option>
</select>
</div>
<div class="gf-form" ng-show="ctrl.panel.repeat && ctrl.panel.repeatDirection == 'h'">
<span class="gf-form-label width-9">Min width</span>
<select class="gf-form-input" ng-model="ctrl.panel.minSpan" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]">
<option value=""></option>
</select>
</div>
</div>
<panel-links-editor panel="ctrl.panel"></panel-links-editor>

View File

@ -39,7 +39,7 @@ export function addLabelToQuery(query: string, key: string, value: string): stri
// Add empty selector to bare metric name
let previousWord;
query = query.replace(/(\w+)\b(?![\({=",])/g, (match, word, offset) => {
query = query.replace(/(\w+)\b(?![\(\]{=",])/g, (match, word, offset) => {
// Check if inside a selector
const nextSelectorStart = query.slice(offset).indexOf('{');
const nextSelectorEnd = query.slice(offset).indexOf('}');
@ -110,10 +110,9 @@ export function determineQueryHints(series: any[], datasource?: any): any[] {
// Check for monotony
const datapoints: [number, number][] = s.datapoints;
const simpleMetric = query.trim().match(/^\w+$/);
if (simpleMetric && datapoints.length > 1) {
if (datapoints.length > 1) {
let increasing = false;
const monotonic = datapoints.every((dp, index) => {
const monotonic = datapoints.filter(dp => dp[0] !== null).every((dp, index) => {
if (index === 0) {
return true;
}
@ -122,18 +121,25 @@ export function determineQueryHints(series: any[], datasource?: any): any[] {
return dp[0] >= datapoints[index - 1][0];
});
if (increasing && monotonic) {
const label = 'Time series is monotonously increasing.';
return {
label,
index,
fix: {
const simpleMetric = query.trim().match(/^\w+$/);
let label = 'Time series is monotonously increasing.';
let fix;
if (simpleMetric) {
fix = {
label: 'Fix by adding rate().',
action: {
type: 'ADD_RATE',
query,
index,
},
},
};
} else {
label = `${label} Try applying a rate() function.`;
}
return {
label,
index,
fix,
};
}
}

View File

@ -213,6 +213,30 @@ describe('PrometheusDatasource', () => {
});
});
it('returns a rate hint w/o action for a complex monotonously increasing series', () => {
const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'sum(metric)', responseIndex: 0 }];
const hints = determineQueryHints(series);
expect(hints.length).toBe(1);
expect(hints[0].label).toContain('rate()');
expect(hints[0].fix).toBeUndefined();
});
it('returns a rate hint for a monotonously increasing series with missing data', () => {
const series = [{ datapoints: [[23, 1000], [null, 1001], [24, 1002]], query: 'metric', responseIndex: 0 }];
const hints = determineQueryHints(series);
expect(hints.length).toBe(1);
expect(hints[0]).toMatchObject({
label: 'Time series is monotonously increasing.',
index: 0,
fix: {
action: {
type: 'ADD_RATE',
query: 'metric',
},
},
});
});
it('returns a histogram hint for a bucket series', () => {
const series = [{ datapoints: [[23, 1000]], query: 'metric_bucket', responseIndex: 0 }];
const hints = determineQueryHints(series);
@ -351,6 +375,7 @@ describe('PrometheusDatasource', () => {
expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe(
'foo{bar="baz",instance="my-host.com:9100"}'
);
expect(addLabelToQuery('rate(metric[1m])', 'foo', 'bar')).toBe('rate(metric{foo="bar"}[1m])');
});
});