mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 02:10:45 -06:00
Merge remote-tracking branch 'origin/master' into alerting_reminder
This commit is contained in:
commit
34e448c6d4
2
.gitignore
vendored
2
.gitignore
vendored
@ -71,4 +71,4 @@ debug.test
|
||||
/vendor/**/appengine*
|
||||
*.orig
|
||||
|
||||
/devenv/dashboards/bulk-testing/*.json
|
||||
/devenv/bulk-dashboards/*.json
|
||||
|
@ -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)
|
||||
|
@ -5,5 +5,5 @@ providers:
|
||||
folder: 'Bulk dashboards'
|
||||
type: file
|
||||
options:
|
||||
path: devenv/dashboards/bulk-testing
|
||||
path: devenv/bulk-dashboards
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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'] }} />
|
||||
|
@ -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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -331,7 +331,7 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Enter':
|
||||
case 'Tab': {
|
||||
if (this.menuEl) {
|
||||
// Dont blur input
|
||||
|
@ -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"}');
|
||||
});
|
||||
});
|
||||
|
@ -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 };
|
||||
}
|
||||
|
108
public/app/core/utils/dag.test.ts
Normal file
108
public/app/core/utils/dag.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
201
public/app/core/utils/dag.ts
Normal file
201
public/app/core/utils/dag.ts
Normal 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}`);
|
||||
});
|
||||
};
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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])');
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user