Merge remote-tracking branch 'origin/develop' into gauge-value-mappings

This commit is contained in:
Peter Holmberg 2018-12-07 15:10:32 +01:00
commit 4f39df900c
64 changed files with 1411 additions and 420 deletions

View File

@ -1,5 +1,5 @@
// uses code from https://github.com/antonholmquist/jason/blob/master/jason.go // uses code from https://github.com/antonholmquist/jason/blob/master/jason.go
// MIT Licence // MIT License
package dynmap package dynmap

View File

@ -1,5 +1,5 @@
// uses code from https://github.com/antonholmquist/jason/blob/master/jason.go // uses code from https://github.com/antonholmquist/jason/blob/master/jason.go
// MIT Licence // MIT License
package dynmap package dynmap

View File

@ -187,7 +187,7 @@ func TestAccountDataAccess(t *testing.T) {
err := DeleteOrg(&m.DeleteOrgCommand{Id: ac2.OrgId}) err := DeleteOrg(&m.DeleteOrgCommand{Id: ac2.OrgId})
So(err, ShouldBeNil) So(err, ShouldBeNil)
// remove frome ac2 from ac1 org // remove ac2 user from ac1 org
remCmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac2.Id, ShouldDeleteOrphanedUser: true} remCmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac2.Id, ShouldDeleteOrphanedUser: true}
err = RemoveOrgUser(&remCmd) err = RemoveOrgUser(&remCmd)
So(err, ShouldBeNil) So(err, ShouldBeNil)

View File

@ -99,14 +99,14 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
return inTransaction(func(sess *DBSession) error { return inTransaction(func(sess *DBSession) error {
//Check if quota is already defined in the DB //Check if quota is already defined in the DB
quota := m.Quota{ quota := m.Quota{
Target: cmd.Target, Target: cmd.Target,
OrgId: cmd.OrgId, OrgId: cmd.OrgId,
Updated: time.Now(),
} }
has, err := sess.Get(&quota) has, err := sess.Get(&quota)
if err != nil { if err != nil {
return err return err
} }
quota.Updated = time.Now()
quota.Limit = cmd.Limit quota.Limit = cmd.Limit
if !has { if !has {
quota.Created = time.Now() quota.Created = time.Now()
@ -201,14 +201,14 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
return inTransaction(func(sess *DBSession) error { return inTransaction(func(sess *DBSession) error {
//Check if quota is already defined in the DB //Check if quota is already defined in the DB
quota := m.Quota{ quota := m.Quota{
Target: cmd.Target, Target: cmd.Target,
UserId: cmd.UserId, UserId: cmd.UserId,
Updated: time.Now(),
} }
has, err := sess.Get(&quota) has, err := sess.Get(&quota)
if err != nil { if err != nil {
return err return err
} }
quota.Updated = time.Now()
quota.Limit = cmd.Limit quota.Limit = cmd.Limit
if !has { if !has {
quota.Created = time.Now() quota.Created = time.Now()

View File

@ -2,6 +2,7 @@ package sqlstore
import ( import (
"testing" "testing"
"time"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -168,5 +169,69 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
So(query.Result.Limit, ShouldEqual, 5) So(query.Result.Limit, ShouldEqual, 5)
So(query.Result.Used, ShouldEqual, 1) So(query.Result.Used, ShouldEqual, 1)
}) })
// related: https://github.com/grafana/grafana/issues/14342
Convey("Should org quota updating is successful even if it called multiple time", func() {
orgCmd := m.UpdateOrgQuotaCmd{
OrgId: orgId,
Target: "org_user",
Limit: 5,
}
err := UpdateOrgQuota(&orgCmd)
So(err, ShouldBeNil)
query := m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1}
err = GetOrgQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 5)
// XXX: resolution of `Updated` column is 1sec, so this makes delay
time.Sleep(1 * time.Second)
orgCmd = m.UpdateOrgQuotaCmd{
OrgId: orgId,
Target: "org_user",
Limit: 10,
}
err = UpdateOrgQuota(&orgCmd)
So(err, ShouldBeNil)
query = m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1}
err = GetOrgQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 10)
})
// related: https://github.com/grafana/grafana/issues/14342
Convey("Should user quota updating is successful even if it called multiple time", func() {
userQuotaCmd := m.UpdateUserQuotaCmd{
UserId: userId,
Target: "org_user",
Limit: 5,
}
err := UpdateUserQuota(&userQuotaCmd)
So(err, ShouldBeNil)
query := m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1}
err = GetUserQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 5)
// XXX: resolution of `Updated` column is 1sec, so this makes delay
time.Sleep(1 * time.Second)
userQuotaCmd = m.UpdateUserQuotaCmd{
UserId: userId,
Target: "org_user",
Limit: 10,
}
err = UpdateUserQuota(&userQuotaCmd)
So(err, ShouldBeNil)
query = m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1}
err = GetUserQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 10)
})
}) })
} }

View File

@ -541,7 +541,7 @@ func getErrorFromElasticResponse(response *es.SearchResponse) *tsdb.QueryResult
} else if reason != "" { } else if reason != "" {
result.ErrorString = reason result.ErrorString = reason
} else { } else {
result.ErrorString = "Unkown elasticsearch error response" result.ErrorString = "Unknown elasticsearch error response"
} }
return result return result

View File

@ -32,6 +32,7 @@ func init() {
renders["median"] = QueryDefinition{Renderer: functionRenderer} renders["median"] = QueryDefinition{Renderer: functionRenderer}
renders["sum"] = QueryDefinition{Renderer: functionRenderer} renders["sum"] = QueryDefinition{Renderer: functionRenderer}
renders["mode"] = QueryDefinition{Renderer: functionRenderer} renders["mode"] = QueryDefinition{Renderer: functionRenderer}
renders["cumulative_sum"] = QueryDefinition{Renderer: functionRenderer}
renders["holt_winters"] = QueryDefinition{ renders["holt_winters"] = QueryDefinition{
Renderer: functionRenderer, Renderer: functionRenderer,

View File

@ -23,6 +23,7 @@ func TestInfluxdbQueryPart(t *testing.T) {
{mode: "alias", params: []string{"test"}, input: "mean(value)", expected: `mean(value) AS "test"`}, {mode: "alias", params: []string{"test"}, input: "mean(value)", expected: `mean(value) AS "test"`},
{mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`}, {mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`},
{mode: "mode", params: []string{}, input: "value", expected: `mode(value)`}, {mode: "mode", params: []string{}, input: "value", expected: `mode(value)`},
{mode: "cumulative_sum", params: []string{}, input: "mean(value)", expected: `cumulative_sum(mean(value))`},
} }
queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")} queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")}

View File

@ -84,7 +84,7 @@ func (e *OpenTsdbExecutor) createRequest(dsInfo *models.DataSource, data OpenTsd
postData, err := json.Marshal(data) postData, err := json.Marshal(data)
if err != nil { if err != nil {
plog.Info("Failed marshalling data", "error", err) plog.Info("Failed marshaling data", "error", err)
return nil, fmt.Errorf("Failed to create request. error: %v", err) return nil, fmt.Errorf("Failed to create request. error: %v", err)
} }

View File

@ -84,7 +84,7 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
render() { render() {
const { onCancel } = this.props; const { onCancel } = this.props;
const newItem = this.state; const newItem = this.state;
const pickerClassName = 'width-20'; const pickerClassName = 'min-width-20';
const isValid = this.isValid(); const isValid = this.isValid();
return ( return (
<div className="gf-form-inline cta-form"> <div className="gf-form-inline cta-form">

View File

@ -40,7 +40,7 @@ export class UserPicker extends Component<Props, State> {
.then(result => { .then(result => {
return result.map(user => ({ return result.map(user => ({
id: user.userId, id: user.userId,
label: `${user.login} - ${user.email}`, label: user.login === user.email ? user.login : `${user.login} - ${user.email}`,
avatarUrl: user.avatarUrl, avatarUrl: user.avatarUrl,
login: user.login, login: user.login,
})); }));

View File

@ -0,0 +1,68 @@
import React, { SFC, ReactNode, PureComponent, ReactElement } from 'react';
interface ToggleButtonGroupProps {
onChange: (value) => void;
value?: any;
label?: string;
render: (props) => void;
}
export default class ToggleButtonGroup extends PureComponent<ToggleButtonGroupProps> {
getValues() {
const { children } = this.props;
return React.Children.toArray(children).map((c: ReactElement<any>) => c.props.value);
}
smallChildren() {
const { children } = this.props;
return React.Children.toArray(children).every((c: ReactElement<any>) => c.props.className.includes('small'));
}
handleToggle(toggleValue) {
const { value, onChange } = this.props;
if (value && value === toggleValue) {
return;
}
onChange(toggleValue);
}
render() {
const { value, label } = this.props;
const values = this.getValues();
const selectedValue = value || values[0];
const labelClassName = `gf-form-label ${this.smallChildren() ? 'small' : ''}`;
return (
<div className="gf-form">
<div className="toggle-button-group">
{label && <label className={labelClassName}>{label}</label>}
{this.props.render({ selectedValue, onChange: this.handleToggle.bind(this) })}
</div>
</div>
);
}
}
interface ToggleButtonProps {
onChange?: (value) => void;
selected?: boolean;
value: any;
className?: string;
children: ReactNode;
}
export const ToggleButton: SFC<ToggleButtonProps> = ({ children, selected, className = '', value, onChange }) => {
const handleChange = event => {
event.stopPropagation();
if (onChange) {
onChange(value);
}
};
const btnClassName = `btn ${className} ${selected ? 'active' : ''}`;
return (
<button className={btnClassName} onClick={handleChange}>
<span>{children}</span>
</button>
);
};

View File

@ -54,7 +54,11 @@ export class Settings {
} }
} }
const bootData = (window as any).grafanaBootData || { settings: {} }; const bootData = (window as any).grafanaBootData || {
settings: {},
user: {},
};
const options = bootData.settings; const options = bootData.settings;
options.bootData = bootData; options.bootData = bootData;

View File

@ -1,6 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import { TimeSeries } from 'app/core/core'; import { TimeSeries } from 'app/core/core';
import colors from 'app/core/utils/colors'; import colors, { getThemeColor } from 'app/core/utils/colors';
export enum LogLevel { export enum LogLevel {
crit = 'critical', crit = 'critical',
@ -22,7 +22,7 @@ export const LogLevelColor = {
[LogLevel.info]: colors[0], [LogLevel.info]: colors[0],
[LogLevel.debug]: colors[5], [LogLevel.debug]: colors[5],
[LogLevel.trace]: colors[2], [LogLevel.trace]: colors[2],
[LogLevel.unkown]: '#ddd', [LogLevel.unkown]: getThemeColor('#8e8e8e', '#dde4ed'),
}; };
export interface LogSearchMatch { export interface LogSearchMatch {
@ -95,6 +95,57 @@ export enum LogsDedupStrategy {
signature = 'signature', signature = 'signature',
} }
export interface LogsParser {
/**
* Value-agnostic matcher for a field label.
* Used to filter rows, and first capture group contains the value.
*/
buildMatcher: (label: string) => RegExp;
/**
* Regex to find a field in the log line.
* First capture group contains the label value, second capture group the value.
*/
fieldRegex: RegExp;
/**
* Function to verify if this is a valid parser for the given line.
* The parser accepts the line unless it returns undefined.
*/
test: (line: string) => any;
}
export const LogsParsers: { [name: string]: LogsParser } = {
JSON: {
buildMatcher: label => new RegExp(`(?:{|,)\\s*"${label}"\\s*:\\s*"([^"]*)"`),
fieldRegex: /"(\w+)"\s*:\s*"([^"]*)"/,
test: line => {
try {
return JSON.parse(line);
} catch (error) {}
},
},
logfmt: {
buildMatcher: label => new RegExp(`(?:^|\\s)${label}=("[^"]*"|\\S+)`),
fieldRegex: /(?:^|\s)(\w+)=("[^"]*"|\S+)/,
test: line => LogsParsers.logfmt.fieldRegex.test(line),
},
};
export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabelStat[] {
// Consider only rows that satisfy the matcher
const rowsWithField = rows.filter(row => extractor.test(row.entry));
const rowCount = rowsWithField.length;
// Get field value counts for eligible rows
const countsByValue = _.countBy(rowsWithField, row => (row as LogRow).entry.match(extractor)[1]);
const sortedCounts = _.chain(countsByValue)
.map((count, value) => ({ count, value, proportion: count / rowCount }))
.sortBy('count')
.reverse()
.value();
return sortedCounts;
}
export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabelStat[] { export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabelStat[] {
// Consider only rows that have the given label // Consider only rows that have the given label
const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined); const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined);
@ -151,6 +202,19 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs
}; };
} }
export function getParser(line: string): LogsParser {
let parser;
try {
if (LogsParsers.JSON.test(line)) {
parser = LogsParsers.JSON;
}
} catch (error) {}
if (!parser && LogsParsers.logfmt.test(line)) {
parser = LogsParsers.logfmt;
}
return parser;
}
export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>): LogsModel { export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>): LogsModel {
if (hiddenLogLevels.size === 0) { if (hiddenLogLevels.size === 0) {
return logs; return logs;
@ -170,16 +234,25 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>)
} }
export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] { export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] {
// currently interval is rangeMs / resolution, which is too low for showing series as bars.
// need at least 10px per bucket, so we multiply interval by 10. Should be solved higher up the chain
// when executing queries & interval calculated and not here but this is a temporary fix.
// intervalMs = intervalMs * 10;
// Graph time series by log level // Graph time series by log level
const seriesByLevel = {}; const seriesByLevel = {};
rows.forEach(row => { const bucketSize = intervalMs * 10;
for (const row of rows) {
if (!seriesByLevel[row.logLevel]) { if (!seriesByLevel[row.logLevel]) {
seriesByLevel[row.logLevel] = { lastTs: null, datapoints: [], alias: row.logLevel }; seriesByLevel[row.logLevel] = { lastTs: null, datapoints: [], alias: row.logLevel };
} }
const levelSeries = seriesByLevel[row.logLevel]; const levelSeries = seriesByLevel[row.logLevel];
// Bucket to nearest minute // Bucket to nearest minute
const time = Math.round(row.timeEpochMs / intervalMs / 10) * intervalMs * 10; const time = Math.round(row.timeEpochMs / bucketSize) * bucketSize;
// Entry for time // Entry for time
if (time === levelSeries.lastTs) { if (time === levelSeries.lastTs) {
levelSeries.datapoints[levelSeries.datapoints.length - 1][0]++; levelSeries.datapoints[levelSeries.datapoints.length - 1][0]++;
@ -187,7 +260,7 @@ export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSerie
levelSeries.datapoints.push([1, time]); levelSeries.datapoints.push([1, time]);
levelSeries.lastTs = time; levelSeries.lastTs = time;
} }
}); }
return Object.keys(seriesByLevel).reduce((acc, level) => { return Object.keys(seriesByLevel).reduce((acc, level) => {
if (seriesByLevel[level]) { if (seriesByLevel[level]) {

View File

@ -1,4 +1,12 @@
import { calculateLogsLabelStats, dedupLogRows, LogsDedupStrategy, LogsModel } from '../logs_model'; import {
calculateFieldStats,
calculateLogsLabelStats,
dedupLogRows,
getParser,
LogsDedupStrategy,
LogsModel,
LogsParsers,
} from '../logs_model';
describe('dedupLogRows()', () => { describe('dedupLogRows()', () => {
test('should return rows as is when dedup is set to none', () => { test('should return rows as is when dedup is set to none', () => {
@ -107,6 +115,50 @@ describe('dedupLogRows()', () => {
}); });
}); });
describe('calculateFieldStats()', () => {
test('should return no stats for empty rows', () => {
expect(calculateFieldStats([], /foo=(.*)/)).toEqual([]);
});
test('should return no stats if extractor does not match', () => {
const rows = [
{
entry: 'foo=bar',
},
];
expect(calculateFieldStats(rows as any, /baz=(.*)/)).toEqual([]);
});
test('should return stats for found field', () => {
const rows = [
{
entry: 'foo="42 + 1"',
},
{
entry: 'foo=503 baz=foo',
},
{
entry: 'foo="42 + 1"',
},
{
entry: 't=2018-12-05T07:44:59+0000 foo=503',
},
];
expect(calculateFieldStats(rows as any, /foo=("[^"]*"|\S+)/)).toMatchObject([
{
value: '"42 + 1"',
count: 2,
},
{
value: '503',
count: 2,
},
]);
});
});
describe('calculateLogsLabelStats()', () => { describe('calculateLogsLabelStats()', () => {
test('should return no stats for empty rows', () => { test('should return no stats for empty rows', () => {
expect(calculateLogsLabelStats([], '')).toEqual([]); expect(calculateLogsLabelStats([], '')).toEqual([]);
@ -159,3 +211,70 @@ describe('calculateLogsLabelStats()', () => {
]); ]);
}); });
}); });
describe('getParser()', () => {
test('should return no parser on empty line', () => {
expect(getParser('')).toBeUndefined();
});
test('should return no parser on unknown line pattern', () => {
expect(getParser('To Be or not to be')).toBeUndefined();
});
test('should return logfmt parser on key value patterns', () => {
expect(getParser('foo=bar baz="41 + 1')).toEqual(LogsParsers.logfmt);
});
test('should return JSON parser on JSON log lines', () => {
// TODO implement other JSON value types than string
expect(getParser('{"foo": "bar", "baz": "41 + 1"}')).toEqual(LogsParsers.JSON);
});
});
describe('LogsParsers', () => {
describe('logfmt', () => {
const parser = LogsParsers.logfmt;
test('should detect format', () => {
expect(parser.test('foo')).toBeFalsy();
expect(parser.test('foo=bar')).toBeTruthy();
});
test('should have a valid fieldRegex', () => {
const match = 'foo=bar'.match(parser.fieldRegex);
expect(match).toBeDefined();
expect(match[1]).toBe('foo');
expect(match[2]).toBe('bar');
});
test('should build a valid value matcher', () => {
const matcher = parser.buildMatcher('foo');
const match = 'foo=bar'.match(matcher);
expect(match).toBeDefined();
expect(match[1]).toBe('bar');
});
});
describe('JSON', () => {
const parser = LogsParsers.JSON;
test('should detect format', () => {
expect(parser.test('foo')).toBeFalsy();
expect(parser.test('{"foo":"bar"}')).toBeTruthy();
});
test('should have a valid fieldRegex', () => {
const match = '{"foo":"bar"}'.match(parser.fieldRegex);
expect(match).toBeDefined();
expect(match[1]).toBe('foo');
expect(match[2]).toBe('bar');
});
test('should build a valid value matcher', () => {
const matcher = parser.buildMatcher('foo');
const match = '{"foo":"bar"}'.match(matcher);
expect(match).toBeDefined();
expect(match[1]).toBe('bar');
});
});
});

View File

@ -1,5 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import config from 'app/core/config';
export const PALETTE_ROWS = 4; export const PALETTE_ROWS = 4;
export const PALETTE_COLUMNS = 14; export const PALETTE_COLUMNS = 14;
@ -90,5 +91,9 @@ export function hslToHex(color) {
return tinycolor(color).toHexString(); return tinycolor(color).toHexString();
} }
export function getThemeColor(dark: string, light: string): string {
return config.bootData.user.lightTheme ? light : dark;
}
export let sortedColors = sortColorsByHue(colors); export let sortedColors = sortColorsByHue(colors);
export default colors; export default colors;

View File

@ -1,15 +1,15 @@
import _ from 'lodash'; import _ from 'lodash';
import { renderUrl } from 'app/core/utils/url'; import { renderUrl } from 'app/core/utils/url';
import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
import { DataQuery, RawTimeRange } from 'app/types/series';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import colors from 'app/core/utils/colors';
import TimeSeries from 'app/core/time_series2';
import { parse as parseDate } from 'app/core/utils/datemath';
import store from 'app/core/store'; import store from 'app/core/store';
import colors from 'app/core/utils/colors';
import { parse as parseDate } from 'app/core/utils/datemath';
import TimeSeries from 'app/core/time_series2';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
import { DataQuery, RawTimeRange, IntervalValues, DataSourceApi } from 'app/types/series';
export const DEFAULT_RANGE = { export const DEFAULT_RANGE = {
from: 'now-6h', from: 'now-6h',
@ -170,18 +170,16 @@ export function calculateResultsFromQueryTransactions(
}; };
} }
export function getIntervals( export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, resolution: number): IntervalValues {
range: RawTimeRange,
datasource,
resolution: number
): { interval: string; intervalMs: number } {
if (!datasource || !resolution) { if (!datasource || !resolution) {
return { interval: '1s', intervalMs: 1000 }; return { interval: '1s', intervalMs: 1000 };
} }
const absoluteRange: RawTimeRange = { const absoluteRange: RawTimeRange = {
from: parseDate(range.from, false), from: parseDate(range.from, false),
to: parseDate(range.to, true), to: parseDate(range.to, true),
}; };
return kbn.calculateInterval(absoluteRange, resolution, datasource.interval); return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
} }

View File

@ -223,6 +223,8 @@ export class DashboardModel {
} }
panelInitialized(panel: PanelModel) { panelInitialized(panel: PanelModel) {
panel.initialized();
if (!this.otherPanelInFullscreen(panel)) { if (!this.otherPanelInFullscreen(panel)) {
panel.refresh(); panel.refresh();
} }

View File

@ -44,7 +44,7 @@ export class GeneralTab extends PureComponent<Props> {
render() { render() {
return ( return (
<EditorTabBody heading="Basic Panel Options" toolbarItems={[]}> <EditorTabBody heading="Panel Options" toolbarItems={[]}>
<div ref={element => (this.element = element)} /> <div ref={element => (this.element = element)} />
</EditorTabBody> </EditorTabBody>
); );

View File

@ -77,7 +77,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
const tabs = [ const tabs = [
{ id: 'queries', text: 'Queries' }, { id: 'queries', text: 'Queries' },
{ id: 'visualization', text: 'Visualization' }, { id: 'visualization', text: 'Visualization' },
{ id: 'advanced', text: 'Advanced' }, { id: 'advanced', text: 'Panel Options' },
]; ];
if (config.alertingEnabled && plugin.id === 'graph') { if (config.alertingEnabled && plugin.id === 'graph') {

View File

@ -258,8 +258,8 @@ export class QueriesTab extends PureComponent<Props, State> {
}; };
const options = { const options = {
title: '', title: 'Time Range',
icon: 'fa fa-cog', icon: '',
disabled: false, disabled: false,
render: this.renderOptions, render: this.renderOptions,
}; };

View File

@ -74,6 +74,11 @@ export class DashNavCtrl {
} }
showSearch() { showSearch() {
if (this.dashboard.meta.fullscreen) {
this.close();
return;
}
appEvents.emit('show-dash-search'); appEvents.emit('show-dash-search');
} }

View File

@ -189,7 +189,7 @@ export class PanelModel {
} }
} }
panelInitialized() { initialized() {
this.events.emit('panel-initialized'); this.events.emit('panel-initialized');
} }

View File

@ -666,6 +666,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
...results, ...results,
queryTransactions: nextQueryTransactions, queryTransactions: nextQueryTransactions,
showingStartPage: false, showingStartPage: false,
graphInterval: queryOptions.intervalMs,
}; };
}); });
@ -747,7 +748,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
console.error(response); console.error(response);
let error: string | JSX.Element = response; let error: string | JSX.Element;
if (response.data) { if (response.data) {
if (typeof response.data === 'string') { if (typeof response.data === 'string') {
error = response.data; error = response.data;
@ -764,6 +765,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
} else { } else {
throw new Error('Could not handle error response'); throw new Error('Could not handle error response');
} }
} else if (response.message) {
error = response.message;
} else if (typeof response === 'string') {
error = response;
} else {
error = 'Unknown error during query transaction. Please check JS console logs.';
} }
this.setState(state => { this.setState(state => {

View File

@ -24,7 +24,7 @@ function StatsRow({ active, count, proportion, value }: LogsLabelStat) {
} }
const STATS_ROW_LIMIT = 5; const STATS_ROW_LIMIT = 5;
class Stats extends PureComponent<{ export class Stats extends PureComponent<{
stats: LogsLabelStat[]; stats: LogsLabelStat[];
label: string; label: string;
value: string; value: string;
@ -48,15 +48,21 @@ class Stats extends PureComponent<{
const otherProportion = otherCount / total; const otherProportion = otherCount / total;
return ( return (
<> <div className="logs-stats">
<div className="logs-stats__info"> <div className="logs-stats__header">
{label}: {total} of {rowCount} rows have that label <span className="logs-stats__title">
<span className="logs-stats__icon fa fa-window-close" onClick={onClickClose} /> {label}: {total} of {rowCount} rows have that label
</span>
<span className="logs-stats__close fa fa-remove" onClick={onClickClose} />
</div> </div>
{topRows.map(stat => <StatsRow key={stat.value} {...stat} active={stat.value === value} />)} <div className="logs-stats__body">
{insertActiveRow && <StatsRow key={activeRow.value} {...activeRow} active />} {topRows.map(stat => <StatsRow key={stat.value} {...stat} active={stat.value === value} />)}
{otherCount > 0 && <StatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} />} {insertActiveRow && activeRow && <StatsRow key={activeRow.value} {...activeRow} active />}
</> {otherCount > 0 && (
<StatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} />
)}
</div>
</div>
); );
} }
} }

View File

@ -10,20 +10,26 @@ import {
LogsModel, LogsModel,
dedupLogRows, dedupLogRows,
filterLogLevels, filterLogLevels,
getParser,
LogLevel, LogLevel,
LogsMetaKind, LogsMetaKind,
LogsLabelStat,
LogsParser,
LogRow, LogRow,
calculateFieldStats,
} from 'app/core/logs_model'; } from 'app/core/logs_model';
import { findHighlightChunksInText } from 'app/core/utils/text'; import { findHighlightChunksInText } from 'app/core/utils/text';
import { Switch } from 'app/core/components/Switch/Switch'; import { Switch } from 'app/core/components/Switch/Switch';
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
import Graph from './Graph'; import Graph from './Graph';
import LogLabels from './LogLabels'; import LogLabels, { Stats } from './LogLabels';
const PREVIEW_LIMIT = 100; const PREVIEW_LIMIT = 100;
const graphOptions = { const graphOptions = {
series: { series: {
stack: true,
bars: { bars: {
show: true, show: true,
lineWidth: 5, lineWidth: 5,
@ -36,6 +42,19 @@ const graphOptions = {
}, },
}; };
/**
* Renders a highlighted field.
* When hovering, a stats icon is shown.
*/
const FieldHighlight = onClick => props => {
return (
<span className={props.className} style={props.style}>
{props.children}
<span className="logs-row__field-highlight--icon fa fa-signal" onClick={() => onClick(props.children)} />
</span>
);
};
interface RowProps { interface RowProps {
allRows: LogRow[]; allRows: LogRow[];
highlighterExpressions?: string[]; highlighterExpressions?: string[];
@ -47,63 +66,177 @@ interface RowProps {
onClickLabel?: (label: string, value: string) => void; onClickLabel?: (label: string, value: string) => void;
} }
function Row({ interface RowState {
allRows, fieldCount: number;
highlighterExpressions, fieldLabel: string;
onClickLabel, fieldStats: LogsLabelStat[];
row, fieldValue: string;
showDuplicates, parsed: boolean;
showLabels, parser: LogsParser;
showLocalTime, parsedFieldHighlights: string[];
showUtc, showFieldStats: boolean;
}: RowProps) { }
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
const highlights = previewHighlights ? highlighterExpressions : row.searchWords; /**
const needsHighlighter = highlights && highlights.length > 0; * Renders a log line.
const highlightClassName = classnames('logs-row-match-highlight', { *
'logs-row-match-highlight--preview': previewHighlights, * When user hovers over it for a certain time, it lazily parses the log line.
}); * Once a parser is found, it will determine fields, that will be highlighted.
return ( * When the user requests stats for a field, they will be calculated and rendered below the row.
<> */
{showDuplicates && ( class Row extends PureComponent<RowProps, RowState> {
<div className="logs-row-duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div> mouseMessageTimer: NodeJS.Timer;
)}
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} /> state = {
{showUtc && ( fieldCount: 0,
<div className="logs-row-time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}> fieldLabel: null,
{row.timestamp} fieldStats: null,
</div> fieldValue: null,
)} parsed: false,
{showLocalTime && ( parser: null,
<div className="logs-row-time" title={`${row.timestamp} (${row.timeFromNow})`}> parsedFieldHighlights: [],
{row.timeLocal} showFieldStats: false,
</div> };
)}
{showLabels && ( componentWillUnmount() {
<div className="logs-row-labels"> clearTimeout(this.mouseMessageTimer);
<LogLabels allRows={allRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} /> }
</div>
)} onClickClose = () => {
<div className="logs-row-message"> this.setState({ showFieldStats: false });
{needsHighlighter ? ( };
<Highlighter
textToHighlight={row.entry} onClickHighlight = (fieldText: string) => {
searchWords={highlights} const { allRows } = this.props;
findChunks={findHighlightChunksInText} const { parser } = this.state;
highlightClassName={highlightClassName}
/> const fieldMatch = fieldText.match(parser.fieldRegex);
) : ( if (fieldMatch) {
row.entry // Build value-agnostic row matcher based on the field label
const fieldLabel = fieldMatch[1];
const fieldValue = fieldMatch[2];
const matcher = parser.buildMatcher(fieldLabel);
const fieldStats = calculateFieldStats(allRows, matcher);
const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0);
this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true });
}
};
onMouseOverMessage = () => {
// Don't parse right away, user might move along
this.mouseMessageTimer = setTimeout(this.parseMessage, 500);
};
onMouseOutMessage = () => {
clearTimeout(this.mouseMessageTimer);
this.setState({ parsed: false });
};
parseMessage = () => {
if (!this.state.parsed) {
const { row } = this.props;
const parser = getParser(row.entry);
if (parser) {
// Use parser to highlight detected fields
const parsedFieldHighlights = [];
this.props.row.entry.replace(new RegExp(parser.fieldRegex, 'g'), substring => {
parsedFieldHighlights.push(substring.trim());
return '';
});
this.setState({ parsedFieldHighlights, parsed: true, parser });
}
}
};
render() {
const {
allRows,
highlighterExpressions,
onClickLabel,
row,
showDuplicates,
showLabels,
showLocalTime,
showUtc,
} = this.props;
const {
fieldCount,
fieldLabel,
fieldStats,
fieldValue,
parsed,
parsedFieldHighlights,
showFieldStats,
} = this.state;
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
const needsHighlighter = highlights && highlights.length > 0;
const highlightClassName = classnames('logs-row__match-highlight', {
'logs-row__match-highlight--preview': previewHighlights,
});
return (
<div className="logs-row">
{showDuplicates && (
<div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
)} )}
<div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
{showUtc && (
<div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
{row.timestamp}
</div>
)}
{showLocalTime && (
<div className="logs-row__time" title={`${row.timestamp} (${row.timeFromNow})`}>
{row.timeLocal}
</div>
)}
{showLabels && (
<div className="logs-row__labels">
<LogLabels allRows={allRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
</div>
)}
<div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
{parsed && (
<Highlighter
autoEscape
highlightTag={FieldHighlight(this.onClickHighlight)}
textToHighlight={row.entry}
searchWords={parsedFieldHighlights}
highlightClassName="logs-row__field-highlight"
/>
)}
{!parsed &&
needsHighlighter && (
<Highlighter
textToHighlight={row.entry}
searchWords={highlights}
findChunks={findHighlightChunksInText}
highlightClassName={highlightClassName}
/>
)}
{!parsed && !needsHighlighter && row.entry}
{showFieldStats && (
<div className="logs-row__stats">
<Stats
stats={fieldStats}
label={fieldLabel}
value={fieldValue}
onClickClose={this.onClickClose}
rowCount={fieldCount}
/>
</div>
)}
</div>
</div> </div>
</> );
); }
} }
function renderMetaItem(value: any, kind: LogsMetaKind) { function renderMetaItem(value: any, kind: LogsMetaKind) {
if (kind === LogsMetaKind.LabelsMap) { if (kind === LogsMetaKind.LabelsMap) {
return ( return (
<span className="logs-meta-item__value-labels"> <span className="logs-meta-item__labels">
<LogLabels labels={value} plain /> <LogLabels labels={value} plain />
</span> </span>
); );
@ -112,7 +245,6 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
} }
interface LogsProps { interface LogsProps {
className?: string;
data: LogsModel; data: LogsModel;
highlighterExpressions: string[]; highlighterExpressions: string[];
loading: boolean; loading: boolean;
@ -220,7 +352,6 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
render() { render() {
const { const {
className = '',
data, data,
highlighterExpressions, highlighterExpressions,
loading = false, loading = false,
@ -263,31 +394,31 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
} }
// Grid options // Grid options
const cssColumnSizes = []; // const cssColumnSizes = [];
if (showDuplicates) { // if (showDuplicates) {
cssColumnSizes.push('max-content'); // cssColumnSizes.push('max-content');
} // }
// Log-level indicator line // // Log-level indicator line
cssColumnSizes.push('3px'); // cssColumnSizes.push('3px');
if (showUtc) { // if (showUtc) {
cssColumnSizes.push('minmax(100px, max-content)'); // cssColumnSizes.push('minmax(220px, max-content)');
} // }
if (showLocalTime) { // if (showLocalTime) {
cssColumnSizes.push('minmax(100px, max-content)'); // cssColumnSizes.push('minmax(140px, max-content)');
} // }
if (showLabels) { // if (showLabels) {
cssColumnSizes.push('fit-content(20%)'); // cssColumnSizes.push('fit-content(20%)');
} // }
cssColumnSizes.push('1fr'); // cssColumnSizes.push('1fr');
const logEntriesStyle = { // const logEntriesStyle = {
gridTemplateColumns: cssColumnSizes.join(' '), // gridTemplateColumns: cssColumnSizes.join(' '),
}; // };
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...'; const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
return ( return (
<div className={`${className} logs`}> <div className="logs-panel">
<div className="logs-graph"> <div className="logs-panel-graph">
<Graph <Graph
data={data.series} data={data.series}
height="100px" height="100px"
@ -298,39 +429,36 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
userOptions={graphOptions} userOptions={graphOptions}
/> />
</div> </div>
<div className="logs-panel-options">
<div className="logs-options"> <div className="logs-panel-controls">
<div className="logs-controls">
<Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} /> <Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} />
<Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} /> <Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} />
<Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} /> <Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} />
<Switch <ToggleButtonGroup
label="Dedup: off" label="Dedup"
checked={dedup === LogsDedupStrategy.none} onChange={this.onChangeDedup}
onChange={() => this.onChangeDedup(LogsDedupStrategy.none)} value={dedup}
/> render={({ selectedValue, onChange }) =>
<Switch Object.keys(LogsDedupStrategy).map((dedupType, i) => (
label="Dedup: exact" <ToggleButton
checked={dedup === LogsDedupStrategy.exact} className="btn-small"
onChange={() => this.onChangeDedup(LogsDedupStrategy.exact)} key={i}
/> value={dedupType}
<Switch onChange={onChange}
label="Dedup: numbers" selected={selectedValue === dedupType}
checked={dedup === LogsDedupStrategy.numbers} >
onChange={() => this.onChangeDedup(LogsDedupStrategy.numbers)} {dedupType}
/> </ToggleButton>
<Switch ))
label="Dedup: signature" }
checked={dedup === LogsDedupStrategy.signature}
onChange={() => this.onChangeDedup(LogsDedupStrategy.signature)}
/> />
{hasData && {hasData &&
meta && ( meta && (
<div className="logs-meta"> <div className="logs-panel-meta">
{meta.map(item => ( {meta.map(item => (
<div className="logs-meta-item" key={item.label}> <div className="logs-panel-meta__item" key={item.label}>
<span className="logs-meta-item__label">{item.label}:</span> <span className="logs-panel-meta__label">{item.label}:</span>
<span className="logs-meta-item__value">{renderMetaItem(item.value, item.kind)}</span> <span className="logs-panel-meta__value">{renderMetaItem(item.value, item.kind)}</span>
</div> </div>
))} ))}
</div> </div>
@ -338,7 +466,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
</div> </div>
</div> </div>
<div className="logs-entries" style={logEntriesStyle}> <div className="logs-rows">
{hasData && {hasData &&
!deferLogs && !deferLogs &&
// Only inject highlighterExpression in the first set for performance reasons // Only inject highlighterExpression in the first set for performance reasons
@ -375,7 +503,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
{!loading && {!loading &&
!hasData && !hasData &&
!scanning && ( !scanning && (
<div className="logs-nodata"> <div className="logs-panel-nodata">
No logs found. No logs found.
<a className="link" onClick={this.onClickScan}> <a className="link" onClick={this.onClickScan}>
Scan for older logs Scan for older logs
@ -384,7 +512,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
)} )}
{scanning && ( {scanning && (
<div className="logs-nodata"> <div className="logs-panel-nodata">
<span>{scanText}</span> <span>{scanText}</span>
<a className="link" onClick={this.onClickStopScan}> <a className="link" onClick={this.onClickStopScan}>
Stop scan Stop scan

View File

@ -1,5 +1,4 @@
<div class="panel panel--solo" ng-if="panel" style="width: 100%"> <div class="panel-solo" ng-if="panel">
<plugin-component type="panel"> <plugin-component type="panel">
</plugin-component> </plugin-component>
</div> </div>
<div class="clearfix"></div>

View File

@ -4,7 +4,7 @@ import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/modul
import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module'; import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
import * as grafanaPlugin from 'app/plugins/datasource/grafana/module'; import * as grafanaPlugin from 'app/plugins/datasource/grafana/module';
import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module'; import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module';
import * as loggingPlugin from 'app/plugins/datasource/logging/module'; import * as lokiPlugin from 'app/plugins/datasource/loki/module';
import * as mixedPlugin from 'app/plugins/datasource/mixed/module'; import * as mixedPlugin from 'app/plugins/datasource/mixed/module';
import * as mysqlPlugin from 'app/plugins/datasource/mysql/module'; import * as mysqlPlugin from 'app/plugins/datasource/mysql/module';
import * as postgresPlugin from 'app/plugins/datasource/postgres/module'; import * as postgresPlugin from 'app/plugins/datasource/postgres/module';
@ -33,7 +33,7 @@ const builtInPlugins = {
'app/plugins/datasource/opentsdb/module': opentsdbPlugin, 'app/plugins/datasource/opentsdb/module': opentsdbPlugin,
'app/plugins/datasource/grafana/module': grafanaPlugin, 'app/plugins/datasource/grafana/module': grafanaPlugin,
'app/plugins/datasource/influxdb/module': influxdbPlugin, 'app/plugins/datasource/influxdb/module': influxdbPlugin,
'app/plugins/datasource/logging/module': loggingPlugin, 'app/plugins/datasource/loki/module': lokiPlugin,
'app/plugins/datasource/mixed/module': mixedPlugin, 'app/plugins/datasource/mixed/module': mixedPlugin,
'app/plugins/datasource/mysql/module': mysqlPlugin, 'app/plugins/datasource/mysql/module': mysqlPlugin,
'app/plugins/datasource/postgres/module': postgresPlugin, 'app/plugins/datasource/postgres/module': postgresPlugin,

View File

@ -115,7 +115,7 @@ export class TeamMembers extends PureComponent<Props, State> {
</button> </button>
<h5>Add Team Member</h5> <h5>Add Team Member</h5>
<div className="gf-form-inline"> <div className="gf-form-inline">
<UserPicker onSelected={this.onUserSelected} className="width-30" /> <UserPicker onSelected={this.onUserSelected} className="min-width-30" />
{this.state.newTeamMember && ( {this.state.newTeamMember && (
<button className="btn btn-success gf-form-btn" type="submit" onClick={this.onAddUserToTeam}> <button className="btn btn-success gf-form-btn" type="submit" onClick={this.onAddUserToTeam}>
Add to team Add to team

View File

@ -58,7 +58,7 @@ exports[`Render should render component 1`] = `
className="gf-form-inline" className="gf-form-inline"
> >
<UserPicker <UserPicker
className="width-30" className="min-width-30"
onSelected={[Function]} onSelected={[Function]}
/> />
</div> </div>
@ -152,7 +152,7 @@ exports[`Render should render team members 1`] = `
className="gf-form-inline" className="gf-form-inline"
> >
<UserPicker <UserPicker
className="width-30" className="min-width-30"
onSelected={[Function]} onSelected={[Function]}
/> />
</div> </div>
@ -372,7 +372,7 @@ exports[`Render should render team members when sync enabled 1`] = `
className="gf-form-inline" className="gf-form-inline"
> >
<UserPicker <UserPicker
className="width-30" className="min-width-30"
onSelected={[Function]} onSelected={[Function]}
/> />
</div> </div>

View File

@ -1,3 +0,0 @@
# Grafana Logging Datasource - Native Plugin
This is a **built in** datasource that allows you to connect to Grafana's logging service.

View File

@ -1,15 +0,0 @@
import Datasource from './datasource';
import LoggingStartPage from './components/LoggingStartPage';
import LoggingQueryField from './components/LoggingQueryField';
export class LoggingConfigCtrl {
static templateUrl = 'partials/config.html';
}
export {
Datasource,
LoggingConfigCtrl as ConfigCtrl,
LoggingQueryField as ExploreQueryField,
LoggingStartPage as ExploreStartPage,
};

View File

@ -0,0 +1,3 @@
# Loki Datasource - Native Plugin
This is a **built in** datasource that allows you to connect to the Loki logging service.

View File

@ -15,7 +15,7 @@ const CHEAT_SHEET_ITEMS = [
export default (props: any) => ( export default (props: any) => (
<div> <div>
<h2>Logging Cheat Sheet</h2> <h2>Loki Cheat Sheet</h2>
{CHEAT_SHEET_ITEMS.map(item => ( {CHEAT_SHEET_ITEMS.map(item => (
<div className="cheat-sheet-item" key={item.expression}> <div className="cheat-sheet-item" key={item.expression}>
<div className="cheat-sheet-item__title">{item.title}</div> <div className="cheat-sheet-item__title">{item.title}</div>

View File

@ -49,7 +49,7 @@ interface CascaderOption {
disabled?: boolean; disabled?: boolean;
} }
interface LoggingQueryFieldProps { interface LokiQueryFieldProps {
datasource: any; datasource: any;
error?: string | JSX.Element; error?: string | JSX.Element;
hint?: any; hint?: any;
@ -60,16 +60,16 @@ interface LoggingQueryFieldProps {
onQueryChange?: (value: DataQuery, override?: boolean) => void; onQueryChange?: (value: DataQuery, override?: boolean) => void;
} }
interface LoggingQueryFieldState { interface LokiQueryFieldState {
logLabelOptions: any[]; logLabelOptions: any[];
syntaxLoaded: boolean; syntaxLoaded: boolean;
} }
class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, LoggingQueryFieldState> { class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
plugins: any[]; plugins: any[];
languageProvider: any; languageProvider: any;
constructor(props: LoggingQueryFieldProps, context) { constructor(props: LokiQueryFieldProps, context) {
super(props, context); super(props, context);
if (props.datasource.languageProvider) { if (props.datasource.languageProvider) {
@ -208,8 +208,8 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
onTypeahead={this.onTypeahead} onTypeahead={this.onTypeahead}
onWillApplySuggestion={willApplySuggestion} onWillApplySuggestion={willApplySuggestion}
onValueChanged={this.onChangeQuery} onValueChanged={this.onChangeQuery}
placeholder="Enter a Logging query" placeholder="Enter a Loki Log query"
portalOrigin="logging" portalOrigin="loki"
syntaxLoaded={syntaxLoaded} syntaxLoaded={syntaxLoaded}
/> />
{error ? <div className="prom-query-field-info text-error">{error}</div> : null} {error ? <div className="prom-query-field-info text-error">{error}</div> : null}
@ -229,4 +229,4 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
} }
} }
export default LoggingQueryField; export default LokiQueryField;

View File

@ -1,15 +1,15 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import LoggingCheatSheet from './LoggingCheatSheet'; import LokiCheatSheet from './LokiCheatSheet';
interface Props { interface Props {
onClickExample: () => void; onClickExample: () => void;
} }
export default class LoggingStartPage extends PureComponent<Props> { export default class LokiStartPage extends PureComponent<Props> {
render() { render() {
return ( return (
<div className="grafana-info-box grafana-info-box--max-lg"> <div className="grafana-info-box grafana-info-box--max-lg">
<LoggingCheatSheet onClickExample={this.props.onClickExample} /> <LokiCheatSheet onClickExample={this.props.onClickExample} />
</div> </div>
); );
} }

View File

@ -0,0 +1,98 @@
import LokiDatasource from './datasource';
describe('LokiDatasource', () => {
const instanceSettings = {
url: 'myloggingurl',
};
describe('when performing testDataSource', () => {
let ds;
let result;
describe('and call succeeds', () => {
beforeEach(async () => {
const backendSrv = {
async datasourceRequest() {
return Promise.resolve({
status: 200,
data: {
values: ['avalue'],
},
});
},
};
ds = new LokiDatasource(instanceSettings, backendSrv, {});
result = await ds.testDatasource();
});
it('should return successfully', () => {
expect(result.status).toBe('success');
});
});
describe('and call fails with 401 error', () => {
beforeEach(async () => {
const backendSrv = {
async datasourceRequest() {
return Promise.reject({
statusText: 'Unauthorized',
status: 401,
data: {
message: 'Unauthorized',
},
});
},
};
ds = new LokiDatasource(instanceSettings, backendSrv, {});
result = await ds.testDatasource();
});
it('should return error status and a detailed error message', () => {
expect(result.status).toEqual('error');
expect(result.message).toBe('Loki: Unauthorized. 401. Unauthorized');
});
});
describe('and call fails with 404 error', () => {
beforeEach(async () => {
const backendSrv = {
async datasourceRequest() {
return Promise.reject({
statusText: 'Not found',
status: 404,
data: '404 page not found',
});
},
};
ds = new LokiDatasource(instanceSettings, backendSrv, {});
result = await ds.testDatasource();
});
it('should return error status and a detailed error message', () => {
expect(result.status).toEqual('error');
expect(result.message).toBe('Loki: Not found. 404. 404 page not found');
});
});
describe('and call fails with 502 error', () => {
beforeEach(async () => {
const backendSrv = {
async datasourceRequest() {
return Promise.reject({
statusText: 'Bad Gateway',
status: 502,
data: '',
});
},
};
ds = new LokiDatasource(instanceSettings, backendSrv, {});
result = await ds.testDatasource();
});
it('should return error status and a detailed error message', () => {
expect(result.status).toEqual('error');
expect(result.message).toBe('Loki: Bad Gateway. 502');
});
});
});
});

View File

@ -27,7 +27,7 @@ function serializeParams(data: any) {
.join('&'); .join('&');
} }
export default class LoggingDatasource { export default class LokiDatasource {
languageProvider: LanguageProvider; languageProvider: LanguageProvider;
/** @ngInject */ /** @ngInject */
@ -94,7 +94,7 @@ export default class LoggingDatasource {
} }
metadataRequest(url) { metadataRequest(url) {
// HACK to get label values for {job=|}, will be replaced when implementing LoggingQueryField // HACK to get label values for {job=|}, will be replaced when implementing LokiQueryField
const apiUrl = url.replace('v1', 'prom'); const apiUrl = url.replace('v1', 'prom');
return this._request(apiUrl, { silent: true }).then(res => { return this._request(apiUrl, { silent: true }).then(res => {
const data = { data: { data: res.data.values || [] } }; const data = { data: { data: res.data.values || [] } };
@ -136,11 +136,28 @@ export default class LoggingDatasource {
} }
return { return {
status: 'error', status: 'error',
message: 'Data source connected, but no labels received. Verify that logging is configured properly.', message:
'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
}; };
}) })
.catch(err => { .catch(err => {
return { status: 'error', message: err.message }; let message = 'Loki: ';
if (err.statusText) {
message += err.statusText;
} else {
message += 'Cannot connect to Loki';
}
if (err.status) {
message += `. ${err.status}`;
}
if (err.data && err.data.message) {
message += `. ${err.data.message}`;
} else if (err.data) {
message += `. ${err.data}`;
}
return { status: 'error', message: message };
}); });
} }
} }

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1,216 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="200px" height="200px" viewBox="0 0 200 200" style="enable-background:new 0 0 200 200;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_2_);}
.st2{fill:url(#SVGID_3_);}
.st3{fill:url(#SVGID_4_);}
.st4{fill:url(#SVGID_5_);}
.st5{fill:url(#SVGID_6_);}
.st6{fill:url(#SVGID_7_);}
.st7{fill:url(#SVGID_8_);}
.st8{fill:url(#SVGID_9_);}
.st9{fill:url(#SVGID_10_);}
.st10{fill:url(#SVGID_11_);}
.st11{fill:url(#SVGID_12_);}
.st12{fill:url(#SVGID_13_);}
.st13{fill:url(#SVGID_14_);}
.st14{fill:url(#SVGID_15_);}
.st15{fill:url(#SVGID_16_);}
.st16{fill:url(#SVGID_17_);}
.st17{fill:url(#SVGID_18_);}
.st18{fill:url(#SVGID_19_);}
.st19{fill:url(#SVGID_20_);}
.st20{fill:url(#SVGID_21_);}
.st21{fill:url(#SVGID_22_);}
.st22{fill:url(#SVGID_23_);}
.st23{fill:url(#SVGID_24_);}
.st24{fill:url(#SVGID_25_);}
.st25{fill:url(#SVGID_26_);}
.st26{fill:url(#SVGID_27_);}
.st27{fill:url(#SVGID_28_);}
.st28{fill:url(#SVGID_29_);}
.st29{fill:url(#SVGID_30_);}
.st30{fill:url(#SVGID_31_);}
.st31{fill:url(#SVGID_32_);}
</style>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="135.0285" y1="238.7858" x2="135.0285" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st0" d="M179.5,130c-6.9-9.5-18.6-16.1-30.2-17.9l-38.1-4.6l0,22.4l34.7,4c5.8,0.9,12.3,4.3,15.8,9.1
c3.5,4.7,4.9,10.5,4,16.3c-1.7,10.8-11,18.5-21.6,18.5c-1.1,0-2.3-0.1-3.4-0.3l-37.9-4.7c-5.1,8-12.2,14.7-20.6,19.2l55.2,7.4
c2.3,0.4,4.6,0.5,6.8,0.5c21.3,0,40-15.6,43.4-37.3C189.3,151.1,186.4,139.5,179.5,130z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="56.0866" y1="238.7858" x2="56.0866" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st1" d="M90.5,171c1.3-1.6,2.4-3.3,3.5-5.1c1-1.7,1.9-3.4,2.7-5.2c2.3-5.3,3.5-11.2,3.5-17.3l0-4l0-5.6l0-5.6l0-22.4
l0-5.6l0-5.6L100,43.9C100,19.7,80.2,0,55.9,0S12,19.8,12,44.1l0.1,66.7c5.7-7.6,13.3-13.7,22.1-17.6L34.1,44
c0-12.1,9.8-21.9,21.8-21.9c12.1,0,21.9,9.8,21.9,21.8L78,91.2l0,5.6l0,5.6l0,22.4l0,5.6l0,5.6l0,7.5c0,5.2-1.8,9.9-4.8,13.7
c-1.5,1.9-3.3,3.5-5.3,4.8c-3.4,2.2-7.4,3.5-11.7,3.5c-0.7,0-1.4,0-2,0l-1.4-0.2c-10.8-1.7-18.6-11.1-18.5-21.8
c0-1.1,0.1-2.1,0.2-3.2c0.7-4.3,2.6-8.1,5.3-11.1c1.6-1.8,3.5-3.3,5.5-4.5c3.2-1.8,6.9-2.9,10.8-2.9c1.1,0,2.3,0.1,3.4,0.3l7.5,1.2
l0-22.4l-4-0.6c-2.3-0.4-4.6-0.5-6.8-0.5c-3.7,0-7.3,0.5-10.8,1.4c-1.9,0.5-3.7,1.1-5.5,1.8c-1.9,0.8-3.8,1.7-5.5,2.7
c-11.2,6.4-19.4,17.7-21.6,31.4c-0.4,2.4-0.5,4.9-0.5,7.3c0,1.2,0.1,2.3,0.2,3.5c0,0.3,0.1,0.6,0.1,0.9c0.1,1.1,0.3,2.3,0.5,3.4
c0.1,0.3,0.1,0.7,0.2,1c0.2,1.1,0.5,2.1,0.8,3.2c0.1,0.4,0.2,0.7,0.3,1.1c0.3,1,0.7,2,1.1,3c0.1,0.3,0.3,0.7,0.4,1
c0.4,1,0.9,1.9,1.4,2.9c0.2,0.3,0.3,0.6,0.5,0.9c0.5,1,1.1,1.9,1.7,2.8c0.2,0.2,0.3,0.5,0.5,0.7c0.7,1,1.4,1.9,2.1,2.9
c0.1,0.1,0.2,0.3,0.4,0.4c0.8,1,1.7,1.9,2.6,2.8c0.1,0.1,0.1,0.1,0.2,0.2c1,1,2,1.9,3,2.7c0,0,0.1,0,0.1,0.1
c1.1,0.9,2.2,1.7,3.3,2.5c0,0,0,0,0.1,0.1c1.1,0.8,2.3,1.5,3.5,2.1c0.1,0,0.1,0.1,0.2,0.1c1.1,0.6,2.3,1.2,3.5,1.7
c0.2,0.1,0.3,0.1,0.5,0.2c1.1,0.5,2.2,0.9,3.3,1.2c0.3,0.1,0.6,0.2,0.9,0.3c1,0.3,2.1,0.6,3.2,0.8c0.4,0.1,0.8,0.2,1.2,0.2
c2.7,0.5,5.5,0.8,8.3,0.8C70.1,187.5,82.4,181.1,90.5,171z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="26.7057" y1="238.7858" x2="26.7057" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st2" d="M28.2,177.5c-1-0.9-2-1.8-3-2.7"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="12.7354" y1="238.7858" x2="12.7354" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st3" d="M13,151.9c-0.2-1.1-0.4-2.2-0.5-3.4"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="14.8849" y1="238.7858" x2="14.8849" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st4" d="M15.4,160.1c-0.4-1-0.8-2-1.1-3"/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="18.6023" y1="238.7858" x2="18.6023" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st5" d="M19.5,167.8c-0.6-0.9-1.2-1.9-1.7-2.8"/>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="16.5561" y1="238.7858" x2="16.5561" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st6" d="M17.2,164c-0.5-0.9-1-1.9-1.4-2.9"/>
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="12.2907" y1="238.7858" x2="12.2907" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st7" d="M12.2,144.1c0,1.2,0.1,2.3,0.2,3.5"/>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="23.7117" y1="238.7858" x2="23.7117" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st8" d="M25,174.6c-0.9-0.9-1.8-1.9-2.6-2.8"/>
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="20.9974" y1="238.7858" x2="20.9974" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st9" d="M22.1,171.3c-0.8-0.9-1.5-1.9-2.1-2.9"/>
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="13.6059" y1="238.7858" x2="13.6059" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st10" d="M14,156.1c-0.3-1-0.6-2.1-0.8-3.2"/>
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="37.1094" y1="238.7858" x2="37.1094" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st11" d="M38.8,184c-1.2-0.5-2.3-1.1-3.5-1.7"/>
<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="45.1619" y1="238.7858" x2="45.1619" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st12" d="M46.8,186.5c-1.1-0.2-2.1-0.5-3.2-0.8"/>
<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="29.9487" y1="238.7858" x2="29.9487" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st13" d="M31.6,180c-1.1-0.8-2.2-1.6-3.3-2.5"/>
<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="41.0239" y1="238.7858" x2="41.0239" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st14" d="M42.7,185.4c-1.1-0.4-2.3-0.8-3.3-1.2"/>
<linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="33.4189" y1="238.7858" x2="33.4189" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st15" d="M35.2,182.2c-1.2-0.7-2.4-1.4-3.5-2.1"/>
<linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="53.9595" y1="238.7858" x2="53.9595" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st16" d="M54.1,154.2c-0.1,0-0.1,0-0.2-0.1"/>
<linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="21.1405" y1="238.7858" x2="21.1405" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st17" d="M21.4,178.8c-0.2-0.2-0.3-0.3-0.5-0.5"/>
<linearGradient id="SVGID_19_" gradientUnits="userSpaceOnUse" x1="35.2646" y1="238.7858" x2="35.2646" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st18" d="M35.2,182.2c0.1,0,0.1,0.1,0.2,0.1"/>
<linearGradient id="SVGID_20_" gradientUnits="userSpaceOnUse" x1="39.0979" y1="238.7858" x2="39.0979" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st19" d="M39.3,184.2c-0.2-0.1-0.3-0.1-0.5-0.2"/>
<linearGradient id="SVGID_21_" gradientUnits="userSpaceOnUse" x1="31.6434" y1="238.7858" x2="31.6434" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st20" d="M31.7,180.1C31.7,180.1,31.6,180.1,31.7,180.1"/>
<linearGradient id="SVGID_22_" gradientUnits="userSpaceOnUse" x1="28.2485" y1="238.7858" x2="28.2485" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st21" d="M28.3,177.6C28.3,177.5,28.2,177.5,28.3,177.6"/>
<linearGradient id="SVGID_23_" gradientUnits="userSpaceOnUse" x1="43.1314" y1="238.7858" x2="43.1314" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st22" d="M43.6,185.7c-0.3-0.1-0.6-0.2-0.9-0.3"/>
<linearGradient id="SVGID_24_" gradientUnits="userSpaceOnUse" x1="47.3468" y1="238.7858" x2="47.3468" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st23" d="M46.8,186.5c0.4,0.1,0.8,0.2,1.2,0.2"/>
<linearGradient id="SVGID_25_" gradientUnits="userSpaceOnUse" x1="19.6975" y1="238.7858" x2="19.6975" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st24" d="M19.9,168.4c-0.2-0.2-0.3-0.5-0.5-0.7"/>
<linearGradient id="SVGID_26_" gradientUnits="userSpaceOnUse" x1="17.4923" y1="238.7858" x2="17.4923" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st25" d="M17.7,164.9c-0.2-0.3-0.3-0.6-0.5-0.9"/>
<linearGradient id="SVGID_27_" gradientUnits="userSpaceOnUse" x1="12.4278" y1="238.7858" x2="12.4278" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st26" d="M12.5,148.5c0-0.3-0.1-0.6-0.1-0.9"/>
<linearGradient id="SVGID_28_" gradientUnits="userSpaceOnUse" x1="13.0965" y1="238.7858" x2="13.0965" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st27" d="M13.2,152.9c-0.1-0.3-0.1-0.7-0.2-1"/>
<linearGradient id="SVGID_29_" gradientUnits="userSpaceOnUse" x1="14.1769" y1="238.7858" x2="14.1769" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st28" d="M14.3,157.1c-0.1-0.3-0.2-0.7-0.3-1.1"/>
<linearGradient id="SVGID_30_" gradientUnits="userSpaceOnUse" x1="15.6479" y1="238.7858" x2="15.6479" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st29" d="M15.9,161.1c-0.2-0.3-0.3-0.7-0.4-1"/>
<linearGradient id="SVGID_31_" gradientUnits="userSpaceOnUse" x1="22.2436" y1="238.7858" x2="22.2436" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st30" d="M22.4,171.7c-0.1-0.1-0.2-0.3-0.4-0.4"/>
<linearGradient id="SVGID_32_" gradientUnits="userSpaceOnUse" x1="25.1051" y1="238.7858" x2="25.1051" y2="2.4079">
<stop offset="0" style="stop-color:#FBED1D"/>
<stop offset="1" style="stop-color:#F05A2A"/>
</linearGradient>
<path class="st31" d="M25.2,174.8c-0.1-0.1-0.1-0.1-0.2-0.2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -36,7 +36,7 @@ export function addHistoryMetadata(item: CompletionItem, history: HistoryItem[])
}; };
} }
export default class LoggingLanguageProvider extends LanguageProvider { export default class LokiLanguageProvider extends LanguageProvider {
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...] labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
logLabelOptions: any[]; logLabelOptions: any[];

View File

@ -0,0 +1,15 @@
import Datasource from './datasource';
import LokiStartPage from './components/LokiStartPage';
import LokiQueryField from './components/LokiQueryField';
export class LokiConfigCtrl {
static templateUrl = 'partials/config.html';
}
export {
Datasource,
LokiConfigCtrl as ConfigCtrl,
LokiQueryField as ExploreQueryField,
LokiStartPage as ExploreStartPage,
};

View File

@ -1,7 +1,7 @@
{ {
"type": "datasource", "type": "datasource",
"name": "Grafana Logging", "name": "Loki",
"id": "logging", "id": "loki",
"metrics": false, "metrics": false,
"alerting": false, "alerting": false,
"annotations": false, "annotations": false,
@ -9,19 +9,19 @@
"explore": true, "explore": true,
"tables": true, "tables": true,
"info": { "info": {
"description": "Grafana Logging Data Source for Grafana", "description": "Loki Logging Data Source for Grafana",
"author": { "author": {
"name": "Grafana Project", "name": "Grafana Project",
"url": "https://grafana.com" "url": "https://grafana.com"
}, },
"logos": { "logos": {
"small": "img/grafana_icon.svg", "small": "img/loki_icon.svg",
"large": "img/grafana_icon.svg" "large": "img/loki_icon.svg"
}, },
"links": [ "links": [
{ {
"name": "Grafana Logging", "name": "Loki",
"url": "https://grafana.com/" "url": "https://github.com/grafana/loki"
} }
], ],
"version": "5.3.0" "version": "5.3.0"

View File

@ -151,8 +151,7 @@ table_schema IN (
buildDatatypeQuery(column: string) { buildDatatypeQuery(column: string) {
let query = 'SELECT udt_name FROM information_schema.columns WHERE '; let query = 'SELECT udt_name FROM information_schema.columns WHERE ';
query += this.buildSchemaConstraint(); query += this.buildTableConstraint(this.target.table);
query += ' AND table_name = ' + this.quoteIdentAsLiteral(this.target.table);
query += ' AND column_name = ' + this.quoteIdentAsLiteral(column); query += ' AND column_name = ' + this.quoteIdentAsLiteral(column);
return query; return query;
} }

View File

@ -16,7 +16,7 @@ export class ResultTransformer {
options.valueWithRefId options.valueWithRefId
), ),
]; ];
} else if (options.format === 'heatmap') { } else if (prometheusResult && options.format === 'heatmap') {
let seriesList = []; let seriesList = [];
prometheusResult.sort(sortSeriesByLabel); prometheusResult.sort(sortSeriesByLabel);
for (const metricData of prometheusResult) { for (const metricData of prometheusResult) {
@ -24,7 +24,7 @@ export class ResultTransformer {
} }
seriesList = this.transformToHistogramOverTime(seriesList); seriesList = this.transformToHistogramOverTime(seriesList);
return seriesList; return seriesList;
} else { } else if (prometheusResult) {
const seriesList = []; const seriesList = [];
for (const metricData of prometheusResult) { for (const metricData of prometheusResult) {
if (response.data.data.resultType === 'matrix') { if (response.data.data.resultType === 'matrix') {
@ -82,7 +82,7 @@ export class ResultTransformer {
let i, j; let i, j;
const metricLabels = {}; const metricLabels = {};
if (md.length === 0) { if (!md || md.length === 0) {
return table; return table;
} }

View File

@ -10,6 +10,31 @@ describe('Prometheus Result Transformer', () => {
ctx.resultTransformer = new ResultTransformer(ctx.templateSrv); ctx.resultTransformer = new ResultTransformer(ctx.templateSrv);
}); });
describe('When nothing is returned', () => {
test('should return empty series', () => {
const response = {
status: 'success',
data: {
resultType: '',
result: null,
},
};
const series = ctx.resultTransformer.transform({ data: response }, {});
expect(series).toEqual([]);
});
test('should return empty table', () => {
const response = {
status: 'success',
data: {
resultType: '',
result: null,
},
};
const table = ctx.resultTransformer.transform({ data: response }, { format: 'table' });
expect(table).toMatchObject([{ type: 'table', rows: [] }]);
});
});
describe('When resultFormat is table', () => { describe('When resultFormat is table', () => {
const response = { const response = {
status: 'success', status: 'success',

View File

@ -19,6 +19,7 @@ import {
DataQuery, DataQuery,
DataQueryResponse, DataQueryResponse,
DataQueryOptions, DataQueryOptions,
IntervalValues,
} from './series'; } from './series';
import { PanelProps, PanelOptionsProps, RangeMap, Threshold, ValueMap } from './panel'; import { PanelProps, PanelOptionsProps, RangeMap, Threshold, ValueMap } from './panel';
import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins'; import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
@ -94,6 +95,7 @@ export {
ValidationRule, ValidationRule,
ValueMap, ValueMap,
RangeMap, RangeMap,
IntervalValues,
}; };
export interface StoreState { export interface StoreState {

View File

@ -19,6 +19,11 @@ export interface TimeRange {
raw: RawTimeRange; raw: RawTimeRange;
} }
export interface IntervalValues {
interval: string; // 10s,5m
intervalMs: number;
}
export type TimeSeriesValue = string | number | null; export type TimeSeriesValue = string | number | null;
export type TimeSeriesPoints = TimeSeriesValue[][]; export type TimeSeriesPoints = TimeSeriesValue[][];
@ -90,6 +95,9 @@ export interface DataQueryOptions {
} }
export interface DataSourceApi { export interface DataSourceApi {
/**
* min interval range
*/
interval?: string; interval?: string;
/** /**

View File

@ -59,6 +59,7 @@
@import 'components/panel_text'; @import 'components/panel_text';
@import 'components/panel_heatmap'; @import 'components/panel_heatmap';
@import 'components/panel_add_panel'; @import 'components/panel_add_panel';
@import 'components/panel_logs';
@import 'components/settings_permissions'; @import 'components/settings_permissions';
@import 'components/tagsinput'; @import 'components/tagsinput';
@import 'components/tables_lists'; @import 'components/tables_lists';
@ -104,6 +105,7 @@
@import 'components/page_loader'; @import 'components/page_loader';
@import 'components/unit-picker'; @import 'components/unit-picker';
@import 'components/thresholds'; @import 'components/thresholds';
@import 'components/toggle_button_group';
// PAGES // PAGES
@import 'pages/login'; @import 'pages/login';

View File

@ -237,6 +237,7 @@ $horizontalComponentOffset: 180px;
// ------------------------- // -------------------------
$navbarHeight: 55px; $navbarHeight: 55px;
$navbarBackground: $panel-bg; $navbarBackground: $panel-bg;
$navbarBorder: 1px solid $dark-3; $navbarBorder: 1px solid $dark-3;
$navbarShadow: 0 0 20px black; $navbarShadow: 0 0 20px black;
@ -387,3 +388,6 @@ $panel-editor-viz-item-bg-hover-active: darken($orange, 45%);
$panel-grid-placeholder-bg: darken($blue, 47%); $panel-grid-placeholder-bg: darken($blue, 47%);
$panel-grid-placeholder-shadow: 0 0 4px $blue; $panel-grid-placeholder-shadow: 0 0 4px $blue;
// logs
$logs-color-unkown: $gray-2;

View File

@ -397,3 +397,6 @@ $panel-editor-viz-item-bg-hover-active: lighten($orange, 34%);
$panel-grid-placeholder-bg: lighten($blue, 62%); $panel-grid-placeholder-bg: lighten($blue, 62%);
$panel-grid-placeholder-shadow: 0 0 4px $blue-light; $panel-grid-placeholder-shadow: 0 0 4px $blue-light;
// logs
$logs-color-unkown: $gray-5;

View File

@ -199,7 +199,6 @@ small,
mark, mark,
.mark { .mark {
padding: 0.2em;
background: $alert-warning-bg; background: $alert-warning-bg;
} }

View File

@ -0,0 +1,294 @@
$column-horizontal-spacing: 10px;
.logs-panel-controls {
display: flex;
background-color: $page-bg;
padding: $panel-padding;
padding-top: 10px;
border-radius: $border-radius;
margin: 2*$panel-margin 0;
border: $panel-border;
justify-items: flex-start;
align-items: flex-start;
> * {
margin-right: 1em;
}
}
.logs-panel-nodata {
> * {
margin-left: 0.5em;
}
}
.logs-panel-meta {
flex: 1;
color: $text-color-weak;
// Align first line with controls labels
margin-top: -2px;
}
.logs-panel-meta__item {
margin-right: 1em;
}
.logs-panel-meta__label {
margin-right: 0.5em;
font-size: 0.9em;
font-weight: 500;
}
.logs-panel-meta__value {
font-family: $font-family-monospace;
}
.logs-panel-meta-item__labels {
// compensate for the labels padding
position: relative;
top: 4px;
}
.logs-rows {
font-family: $font-family-monospace;
font-size: 12px;
display: table;
table-layout: fixed;
}
.logs-row {
display: table-row;
> div {
display: table-cell;
padding-right: $column-horizontal-spacing;
vertical-align: middle;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
}
&:hover {
background: $page-bg;
}
}
.logs-row__time {
white-space: nowrap;
}
.logs-row__labels {
max-width: 20%;
line-height: 1.2;
}
.logs-row__message {
word-break: break-all;
min-width: 80%;
}
.logs-row__match-highlight {
// Undoing mark styling
background: inherit;
padding: inherit;
color: $typeahead-selected-color;
border-bottom: 1px solid $typeahead-selected-color;
background-color: rgba($typeahead-selected-color, 0.1);
&--preview {
background-color: rgba($typeahead-selected-color, 0.2);
border-bottom-style: dotted;
}
}
.logs-row__level {
position: relative;
&::after {
content: '';
display: block;
position: absolute;
top: 1px;
bottom: 1px;
width: 3px;
background-color: $logs-color-unkown;
}
&--critical,
&--crit {
&::after {
background-color: #705da0;
}
}
&--error,
&--err {
&::after {
background-color: #e24d42;
}
}
&--warning,
&--warn {
&::after {
background-color: $warn;
}
}
&--info {
&::after {
background-color: #7eb26d;
}
}
&--debug {
&::after {
background-color: #1f78c1;
}
}
&--trace {
&::after {
background-color: #6ed0e0;
}
}
}
.logs-row__duplicates {
text-align: right;
}
.logs-row__field-highlight {
// Undoing mark styling
background: inherit;
padding: inherit;
border-bottom: 1px dotted $typeahead-selected-color;
.logs-row__field-highlight--icon {
margin-left: 0.5em;
cursor: pointer;
display: none;
}
}
.logs-row__stats {
margin: 5px 0;
}
.logs-row__field-highlight:hover {
color: $typeahead-selected-color;
border-bottom-style: solid;
.logs-row__field-highlight--icon {
display: inline;
}
}
.logs-label {
display: inline-block;
padding: 0 2px;
background-color: $btn-inverse-bg;
border-radius: $border-radius;
margin: 0 4px 2px 0;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
}
.logs-label__icon {
border-left: $panel-border;
padding: 0 2px;
cursor: pointer;
margin-left: 2px;
}
.logs-label__stats {
position: absolute;
top: 1.25em;
left: -10px;
z-index: 100;
justify-content: space-between;
box-shadow: $popover-shadow;
}
/*
* Stats popover & message stats box
*/
.logs-stats {
background-color: $popover-bg;
color: $popover-color;
border: 1px solid $popover-border-color;
border-radius: $border-radius;
max-width: 500px;
}
.logs-stats__header {
background-color: $popover-border-color;
padding: 6px 10px;
display: flex;
}
.logs-stats__title {
font-weight: $font-weight-semi-bold;
padding-right: $spacer;
overflow: hidden;
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
flex-grow: 1;
}
.logs-stats__body {
padding: 20px 10px 10px 10px;
}
.logs-stats__close {
cursor: pointer;
}
.logs-stats-row {
margin: $spacer/1.75 0;
&--active {
color: $blue;
position: relative;
}
&--active::after {
display: inline;
content: '*';
position: absolute;
top: 0;
left: -8px;
}
&__label {
display: flex;
margin-bottom: 1px;
}
&__value {
flex: 1;
}
&__count,
&__percent {
text-align: right;
margin-left: 0.5em;
}
&__percent {
width: 3em;
}
&__bar,
&__innerbar {
height: 4px;
overflow: hidden;
background: $text-color-faint;
}
&__innerbar {
background: $blue;
}
}

View File

@ -0,0 +1,37 @@
.toggle-button-group {
display: flex;
.gf-form-label {
background-color: $input-label-bg;
&:first-child {
border-radius: $border-radius 0 0 $border-radius;
margin: 0;
}
&.small {
padding: ($input-padding-y / 2) ($input-padding-x / 2);
font-size: $font-size-xs;
}
}
.btn {
background-color: $typeahead-selected-bg;
border-radius: 0;
color: $text-color;
&.active {
background-color: $input-bg;
&:hover {
cursor: default;
}
}
&:first-child {
border-radius: $border-radius 0 0 $border-radius;
margin: 0;
}
&:last-child {
border-radius: 0 $border-radius $border-radius 0;
margin-left: 0;
}
}
}

View File

@ -15,17 +15,23 @@ div.flot-text {
.panel { .panel {
height: 100%; height: 100%;
}
&--solo { .panel-solo {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
right: 0; right: 0;
margin: 0; margin: 0;
left: 0;
top: 0;
.panel-container { .panel-container {
border: none; border: none;
z-index: $zindex-sidemenu + 1; }
}
.panel-menu-toggle,
.panel-menu {
display: none;
} }
} }

View File

@ -238,212 +238,6 @@
padding-right: 0.25em; padding-right: 0.25em;
} }
.explore {
.logs {
.logs-controls {
display: flex;
background-color: $page-bg;
padding: $panel-padding;
padding-top: 10px;
border-radius: $border-radius;
margin: 2*$panel-margin 0;
border: $panel-border;
justify-items: flex-start;
align-items: flex-start;
> * {
margin-right: 1em;
}
}
.logs-nodata {
> * {
margin-left: 0.5em;
}
}
.logs-meta {
flex: 1;
color: $text-color-weak;
// Align first line with controls labels
margin-top: -2px;
}
.logs-meta-item {
margin-right: 1em;
}
.logs-meta-item__label {
margin-right: 0.5em;
font-size: 0.9em;
font-weight: 500;
}
.logs-meta-item__value {
font-family: $font-family-monospace;
}
.logs-meta-item__value-labels {
// compensate for the labels padding
position: relative;
top: 4px;
}
.logs-entries {
display: grid;
grid-column-gap: 1rem;
grid-row-gap: 0.1rem;
font-family: $font-family-monospace;
font-size: 12px;
}
.logs-row-match-highlight {
// Undoing mark styling
background: inherit;
padding: inherit;
color: $typeahead-selected-color;
border-bottom: 1px solid $typeahead-selected-color;
background-color: rgba($typeahead-selected-color, 0.1);
}
.logs-row-match-highlight--preview {
background-color: rgba($typeahead-selected-color, 0.2);
border-bottom-style: dotted;
}
.logs-row-level {
background-color: transparent;
margin: 2px 0;
position: relative;
opacity: 0.8;
}
.logs-row-level-critical,
.logs-row-level-crit {
background-color: #705da0;
}
.logs-row-level-error,
.logs-row-level-err {
background-color: #e24d42;
}
.logs-row-level-warning,
.logs-row-level-warn {
background-color: #eab839;
}
.logs-row-level-info {
background-color: #7eb26d;
}
.logs-row-level-debug {
background-color: #1f78c1;
}
.logs-row-level-trace {
background-color: #6ed0e0;
}
.logs-row-duplicates {
text-align: right;
}
.logs-label {
display: inline-block;
padding: 0 2px;
background-color: $btn-inverse-bg;
border-radius: $border-radius;
margin: 0 4px 2px 0;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
}
.logs-label__icon {
border-left: $panel-border;
padding: 0 2px;
cursor: pointer;
margin-left: 2px;
}
.logs-label__stats {
position: absolute;
top: 1.25em;
left: -10px;
z-index: 100;
background-color: $popover-bg;
color: $popover-color;
border: 1px solid $popover-border-color;
padding: 10px;
border-radius: $border-radius;
justify-content: space-between;
box-shadow: $popover-shadow;
}
.logs-row-labels {
line-height: 1.2;
}
.logs-stats__info {
margin-bottom: $spacer / 2;
}
.logs-stats__icon {
margin-left: 0.5em;
cursor: pointer;
}
.logs-stats-row {
margin: $spacer/1.75 0;
&--active {
color: $blue;
position: relative;
}
&--active:after {
display: inline;
content: '*';
position: absolute;
top: 0;
left: -8px;
}
&__label {
display: flex;
margin-bottom: 1px;
}
&__value {
flex: 1;
}
&__count,
&__percent {
text-align: right;
margin-left: 0.5em;
}
&__percent {
width: 3em;
}
&__bar,
&__innerbar {
height: 4px;
overflow: hidden;
background: $text-color-faint;
}
&__innerbar {
background-color: $blue;
}
}
}
}
// Prometheus-specifics, to be extracted to datasource soon // Prometheus-specifics, to be extracted to datasource soon
.explore { .explore {

View File

@ -19,6 +19,12 @@
} }
} }
@for $i from 1 through 30 {
.min-width-#{$i} {
min-width: ($spacer * $i) - $gf-form-margin !important;
}
}
@for $i from 1 through 30 { @for $i from 1 through 30 {
.offset-width-#{$i} { .offset-width-#{$i} {
margin-left: ($spacer * $i) !important; margin-left: ($spacer * $i) !important;