mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into azure-monitor-refactor-#15087
This commit is contained in:
@@ -232,6 +232,14 @@ export default class CloudWatchDatasource {
|
||||
});
|
||||
}
|
||||
|
||||
getResourceARNs(region, resourceType, tags) {
|
||||
return this.doMetricQueryRequest('resource_arns', {
|
||||
region: this.templateSrv.replace(this.getActualRegion(region)),
|
||||
resourceType: this.templateSrv.replace(resourceType),
|
||||
tags: tags,
|
||||
});
|
||||
}
|
||||
|
||||
metricFindQuery(query) {
|
||||
let region;
|
||||
let namespace;
|
||||
@@ -293,6 +301,15 @@ export default class CloudWatchDatasource {
|
||||
return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson);
|
||||
}
|
||||
|
||||
|
||||
const resourceARNsQuery = query.match(/^resource_arns\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
|
||||
if (resourceARNsQuery) {
|
||||
region = resourceARNsQuery[1];
|
||||
const resourceType = resourceARNsQuery[2];
|
||||
const tagsJSON = JSON.parse(this.templateSrv.replace(resourceARNsQuery[3]));
|
||||
return this.getResourceARNs(region, resourceType, tagsJSON);
|
||||
}
|
||||
|
||||
return this.$q.when([]);
|
||||
}
|
||||
|
||||
|
||||
@@ -380,6 +380,29 @@ describe('CloudWatchDatasource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeMetricFindQuery('resource_arns(default,ec2:instance,{"environment":["production"]})', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = {
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [{
|
||||
rows: [[
|
||||
'arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567',
|
||||
'arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321'
|
||||
]]
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __ListMetrics and return result', () => {
|
||||
expect(scenario.result[0].text).toContain('arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567');
|
||||
expect(scenario.request.queries[0].type).toBe('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).toBe('resource_arns');
|
||||
});
|
||||
});
|
||||
|
||||
it('should caclculate the correct period', () => {
|
||||
const hourSec = 60 * 60;
|
||||
const daySec = hourSec * 24;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { Select, SelectOptionItem } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { QueryEditorProps } from '@grafana/ui/src/types';
|
||||
import { LokiDatasource } from '../datasource';
|
||||
import { LokiQuery } from '../types';
|
||||
import { LokiQueryField } from './LokiQueryField';
|
||||
|
||||
type Props = QueryEditorProps<LokiDatasource, LokiQuery>;
|
||||
|
||||
interface State {
|
||||
query: LokiQuery;
|
||||
}
|
||||
|
||||
export class LokiQueryEditor extends PureComponent<Props> {
|
||||
state: State = {
|
||||
query: this.props.query,
|
||||
};
|
||||
|
||||
onRunQuery = () => {
|
||||
const { query } = this.state;
|
||||
|
||||
this.props.onChange(query);
|
||||
this.props.onRunQuery();
|
||||
};
|
||||
|
||||
onFieldChange = (query: LokiQuery, override?) => {
|
||||
this.setState({
|
||||
query: {
|
||||
...this.state.query,
|
||||
expr: query.expr,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onFormatChanged = (option: SelectOptionItem) => {
|
||||
this.props.onChange({
|
||||
...this.state.query,
|
||||
resultFormat: option.value,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { query } = this.state;
|
||||
const { datasource } = this.props;
|
||||
const formatOptions: SelectOptionItem[] = [
|
||||
{ label: 'Time Series', value: 'time_series' },
|
||||
{ label: 'Table', value: 'table' },
|
||||
];
|
||||
|
||||
query.resultFormat = query.resultFormat || 'time_series';
|
||||
const currentFormat = formatOptions.find(item => item.value === query.resultFormat);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LokiQueryField
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onQueryChange={this.onFieldChange}
|
||||
onExecuteQuery={this.onRunQuery}
|
||||
history={[]}
|
||||
/>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-label">Format as</div>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
options={formatOptions}
|
||||
onChange={this.onFormatChanged}
|
||||
value={currentFormat}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form gf-form--grow">
|
||||
<div className="gf-form-label gf-form-label--grow" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LokiQueryEditor;
|
||||
@@ -15,8 +15,9 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
||||
|
||||
// Types
|
||||
import { LokiQuery } from '../types';
|
||||
import { TypeaheadOutput } from 'app/types/explore';
|
||||
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
||||
import { makePromiseCancelable, CancelablePromise } from 'app/core/utils/CancelablePromise';
|
||||
import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui';
|
||||
|
||||
const PRISM_SYNTAX = 'promql';
|
||||
|
||||
@@ -64,15 +65,8 @@ interface CascaderOption {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface LokiQueryFieldProps {
|
||||
datasource: any;
|
||||
error?: string | JSX.Element;
|
||||
hint?: any;
|
||||
history?: any[];
|
||||
initialQuery?: LokiQuery;
|
||||
onClickHintFix?: (action: any) => void;
|
||||
onPressEnter?: () => void;
|
||||
onQueryChange?: (value: LokiQuery, override?: boolean) => void;
|
||||
interface LokiQueryFieldProps extends ExploreQueryFieldProps<ExploreDataSourceApi, LokiQuery> {
|
||||
history: HistoryItem[];
|
||||
}
|
||||
|
||||
interface LokiQueryFieldState {
|
||||
@@ -80,7 +74,7 @@ interface LokiQueryFieldState {
|
||||
syntaxLoaded: boolean;
|
||||
}
|
||||
|
||||
class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
|
||||
export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
|
||||
plugins: any[];
|
||||
pluginsSearch: any[];
|
||||
languageProvider: any;
|
||||
@@ -97,14 +91,14 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
|
||||
|
||||
this.plugins = [
|
||||
BracesPlugin(),
|
||||
RunnerPlugin({ handler: props.onPressEnter }),
|
||||
RunnerPlugin({ handler: props.onExecuteQuery }),
|
||||
PluginPrism({
|
||||
onlyIn: node => node.type === 'code_block',
|
||||
getSyntax: node => 'promql',
|
||||
}),
|
||||
];
|
||||
|
||||
this.pluginsSearch = [RunnerPlugin({ handler: props.onPressEnter })];
|
||||
this.pluginsSearch = [RunnerPlugin({ handler: props.onExecuteQuery })];
|
||||
|
||||
this.state = {
|
||||
logLabelOptions: [],
|
||||
@@ -168,20 +162,21 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
|
||||
|
||||
onChangeQuery = (value: string, override?: boolean) => {
|
||||
// Send text change to parent
|
||||
const { initialQuery, onQueryChange } = this.props;
|
||||
const { query, onQueryChange, onExecuteQuery } = this.props;
|
||||
if (onQueryChange) {
|
||||
const query = {
|
||||
...initialQuery,
|
||||
expr: value,
|
||||
};
|
||||
onQueryChange(query, override);
|
||||
const nextQuery = { ...query, expr: value };
|
||||
onQueryChange(nextQuery);
|
||||
|
||||
if (override && onExecuteQuery) {
|
||||
onExecuteQuery();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClickHintFix = () => {
|
||||
const { hint, onClickHintFix } = this.props;
|
||||
if (onClickHintFix && hint && hint.fix) {
|
||||
onClickHintFix(hint.fix.action);
|
||||
const { hint, onExecuteHint } = this.props;
|
||||
if (onExecuteHint && hint && hint.fix) {
|
||||
onExecuteHint(hint.fix.action);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -219,7 +214,7 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
|
||||
};
|
||||
|
||||
render() {
|
||||
const { error, hint, initialQuery } = this.props;
|
||||
const { error, hint, query } = this.props;
|
||||
const { logLabelOptions, syntaxLoaded } = this.state;
|
||||
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
||||
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
|
||||
@@ -239,10 +234,11 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
|
||||
<QueryField
|
||||
additionalPlugins={this.plugins}
|
||||
cleanText={cleanText}
|
||||
initialQuery={initialQuery.expr}
|
||||
initialQuery={query.expr}
|
||||
onTypeahead={this.onTypeahead}
|
||||
onWillApplySuggestion={willApplySuggestion}
|
||||
onValueChanged={this.onChangeQuery}
|
||||
onQueryChange={this.onChangeQuery}
|
||||
onExecuteQuery={this.props.onExecuteQuery}
|
||||
placeholder="Enter a Loki query"
|
||||
portalOrigin="loki"
|
||||
syntaxLoaded={syntaxLoaded}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import LokiCheatSheet from './LokiCheatSheet';
|
||||
import { ExploreStartPageProps } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
onClickExample: () => void;
|
||||
}
|
||||
|
||||
export default class LokiStartPage extends PureComponent<Props> {
|
||||
export default class LokiStartPage extends PureComponent<ExploreStartPageProps> {
|
||||
render() {
|
||||
return (
|
||||
<div className="grafana-info-box grafana-info-box--max-lg">
|
||||
|
||||
@@ -7,6 +7,17 @@ describe('LokiDatasource', () => {
|
||||
url: 'myloggingurl',
|
||||
};
|
||||
|
||||
const testResp = {
|
||||
data: {
|
||||
streams: [
|
||||
{
|
||||
entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }],
|
||||
labels: '{}',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('when querying', () => {
|
||||
const backendSrvMock = { datasourceRequest: jest.fn() };
|
||||
|
||||
@@ -17,7 +28,7 @@ describe('LokiDatasource', () => {
|
||||
|
||||
test('should use default max lines when no limit given', () => {
|
||||
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
|
||||
backendSrvMock.datasourceRequest = jest.fn();
|
||||
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
|
||||
const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
|
||||
|
||||
ds.query(options);
|
||||
@@ -30,7 +41,7 @@ describe('LokiDatasource', () => {
|
||||
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
|
||||
const customSettings = { ...instanceSettings, jsonData: customData };
|
||||
const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock);
|
||||
backendSrvMock.datasourceRequest = jest.fn();
|
||||
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
|
||||
|
||||
const options = getQueryOptions<LokiQuery>({ targets: [{ expr: 'foo', refId: 'B' }] });
|
||||
ds.query(options);
|
||||
@@ -38,6 +49,34 @@ describe('LokiDatasource', () => {
|
||||
expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1);
|
||||
expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20');
|
||||
});
|
||||
|
||||
test('should return log streams when resultFormat is undefined', async done => {
|
||||
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
|
||||
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
|
||||
|
||||
const options = getQueryOptions<LokiQuery>({
|
||||
targets: [{ expr: 'foo', refId: 'B' }],
|
||||
});
|
||||
|
||||
const res = await ds.query(options);
|
||||
|
||||
expect(res.data[0].entries[0].line).toBe('hello');
|
||||
done();
|
||||
});
|
||||
|
||||
test('should return time series when resultFormat is time_series', async done => {
|
||||
const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock);
|
||||
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
|
||||
|
||||
const options = getQueryOptions<LokiQuery>({
|
||||
targets: [{ expr: 'foo', refId: 'B', resultFormat: 'time_series' }],
|
||||
});
|
||||
|
||||
const res = await ds.query(options);
|
||||
|
||||
expect(res.data[0].datapoints).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when performing testDataSource', () => {
|
||||
|
||||
@@ -32,7 +32,7 @@ function serializeParams(data: any) {
|
||||
.join('&');
|
||||
}
|
||||
|
||||
export default class LokiDatasource {
|
||||
export class LokiDatasource {
|
||||
languageProvider: LanguageProvider;
|
||||
maxLines: number;
|
||||
|
||||
@@ -73,10 +73,11 @@ export default class LokiDatasource {
|
||||
};
|
||||
}
|
||||
|
||||
query(options: DataQueryOptions<LokiQuery>): Promise<{ data: LogsStream[] }> {
|
||||
async query(options: DataQueryOptions<LokiQuery>) {
|
||||
const queryTargets = options.targets
|
||||
.filter(target => target.expr)
|
||||
.filter(target => target.expr && !target.hide)
|
||||
.map(target => this.prepareQueryTarget(target, options));
|
||||
|
||||
if (queryTargets.length === 0) {
|
||||
return Promise.resolve({ data: [] });
|
||||
}
|
||||
@@ -84,20 +85,29 @@ export default class LokiDatasource {
|
||||
const queries = queryTargets.map(target => this._request('/api/prom/query', target));
|
||||
|
||||
return Promise.all(queries).then((results: any[]) => {
|
||||
// Flatten streams from multiple queries
|
||||
const allStreams: LogsStream[] = results.reduce((acc, response, i) => {
|
||||
if (!response) {
|
||||
return acc;
|
||||
const allStreams: LogsStream[] = [];
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
const query = queryTargets[i];
|
||||
|
||||
// add search term to stream & add to array
|
||||
if (result.data) {
|
||||
for (const stream of (result.data.streams || [])) {
|
||||
stream.search = query.regexp;
|
||||
allStreams.push(stream);
|
||||
}
|
||||
}
|
||||
const streams: LogsStream[] = response.data.streams || [];
|
||||
// Inject search for match highlighting
|
||||
const search: string = queryTargets[i].regexp;
|
||||
streams.forEach(s => {
|
||||
s.search = search;
|
||||
});
|
||||
return [...acc, ...streams];
|
||||
}, []);
|
||||
return { data: allStreams };
|
||||
}
|
||||
|
||||
// check resultType
|
||||
if (options.targets[0].resultFormat === 'time_series') {
|
||||
const logs = mergeStreamsToLogs(allStreams, this.maxLines);
|
||||
logs.series = makeSeriesForLogs(logs.rows, options.intervalMs);
|
||||
return { data: logs.series };
|
||||
} else {
|
||||
return { data: allStreams };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,34 +152,36 @@ export default class LokiDatasource {
|
||||
|
||||
testDatasource() {
|
||||
return this._request('/api/prom/label')
|
||||
.then(res => {
|
||||
if (res && res.data && res.data.values && res.data.values.length > 0) {
|
||||
return { status: 'success', message: 'Data source connected and labels found.' };
|
||||
}
|
||||
return {
|
||||
status: 'error',
|
||||
message:
|
||||
'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
let message = 'Loki: ';
|
||||
if (err.statusText) {
|
||||
message += err.statusText;
|
||||
} else {
|
||||
message += 'Cannot connect to Loki';
|
||||
}
|
||||
.then(res => {
|
||||
if (res && res.data && res.data.values && res.data.values.length > 0) {
|
||||
return { status: 'success', message: 'Data source connected and labels found.' };
|
||||
}
|
||||
return {
|
||||
status: 'error',
|
||||
message:
|
||||
'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
let message = 'Loki: ';
|
||||
if (err.statusText) {
|
||||
message += err.statusText;
|
||||
} else {
|
||||
message += 'Cannot connect to Loki';
|
||||
}
|
||||
|
||||
if (err.status) {
|
||||
message += `. ${err.status}`;
|
||||
}
|
||||
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 };
|
||||
});
|
||||
if (err.data && err.data.message) {
|
||||
message += `. ${err.data.message}`;
|
||||
} else if (err.data) {
|
||||
message += `. ${err.data}`;
|
||||
}
|
||||
return { status: 'error', message: message };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default LokiDatasource;
|
||||
|
||||
@@ -2,6 +2,7 @@ import Datasource from './datasource';
|
||||
|
||||
import LokiStartPage from './components/LokiStartPage';
|
||||
import LokiQueryField from './components/LokiQueryField';
|
||||
import LokiQueryEditor from './components/LokiQueryEditor';
|
||||
|
||||
export class LokiConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
@@ -9,6 +10,7 @@ export class LokiConfigCtrl {
|
||||
|
||||
export {
|
||||
Datasource,
|
||||
LokiQueryEditor as QueryEditor,
|
||||
LokiConfigCtrl as ConfigCtrl,
|
||||
LokiQueryField as ExploreQueryField,
|
||||
LokiStartPage as ExploreStartPage,
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
"type": "datasource",
|
||||
"name": "Loki",
|
||||
"id": "loki",
|
||||
"metrics": false,
|
||||
|
||||
"metrics": true,
|
||||
"alerting": false,
|
||||
"annotations": false,
|
||||
"logs": true,
|
||||
"explore": true,
|
||||
"tables": false,
|
||||
|
||||
"info": {
|
||||
"description": "Loki Logging Data Source for Grafana",
|
||||
"author": {
|
||||
|
||||
@@ -2,5 +2,8 @@ import { DataQuery } from '@grafana/ui/src/types';
|
||||
|
||||
export interface LokiQuery extends DataQuery {
|
||||
expr: string;
|
||||
resultFormat?: LokiQueryResultFormats;
|
||||
}
|
||||
|
||||
export type LokiQueryResultFormats = 'time_series' | 'logs';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import Cascader from 'rc-cascader';
|
||||
import PluginPrism from 'slate-prism';
|
||||
import Prism from 'prismjs';
|
||||
|
||||
import { TypeaheadOutput } from 'app/types/explore';
|
||||
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
|
||||
|
||||
// dom also includes Element polyfills
|
||||
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
|
||||
@@ -13,6 +13,7 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
||||
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
||||
import { PromQuery } from '../types';
|
||||
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
|
||||
import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui';
|
||||
|
||||
const HISTOGRAM_GROUP = '__histograms__';
|
||||
const METRIC_MARK = 'metric';
|
||||
@@ -86,15 +87,8 @@ interface CascaderOption {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface PromQueryFieldProps {
|
||||
datasource: any;
|
||||
error?: string | JSX.Element;
|
||||
initialQuery: PromQuery;
|
||||
hint?: any;
|
||||
history?: any[];
|
||||
onClickHintFix?: (action: any) => void;
|
||||
onPressEnter?: () => void;
|
||||
onQueryChange?: (value: PromQuery, override?: boolean) => void;
|
||||
interface PromQueryFieldProps extends ExploreQueryFieldProps<ExploreDataSourceApi, PromQuery> {
|
||||
history: HistoryItem[];
|
||||
}
|
||||
|
||||
interface PromQueryFieldState {
|
||||
@@ -116,7 +110,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
|
||||
this.plugins = [
|
||||
BracesPlugin(),
|
||||
RunnerPlugin({ handler: props.onPressEnter }),
|
||||
RunnerPlugin({ handler: props.onExecuteQuery }),
|
||||
PluginPrism({
|
||||
onlyIn: node => node.type === 'code_block',
|
||||
getSyntax: node => 'promql',
|
||||
@@ -174,20 +168,21 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
|
||||
onChangeQuery = (value: string, override?: boolean) => {
|
||||
// Send text change to parent
|
||||
const { initialQuery, onQueryChange } = this.props;
|
||||
const { query, onQueryChange, onExecuteQuery } = this.props;
|
||||
if (onQueryChange) {
|
||||
const query: PromQuery = {
|
||||
...initialQuery,
|
||||
expr: value,
|
||||
};
|
||||
onQueryChange(query, override);
|
||||
const nextQuery: PromQuery = { ...query, expr: value };
|
||||
onQueryChange(nextQuery);
|
||||
|
||||
if (override && onExecuteQuery) {
|
||||
onExecuteQuery();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClickHintFix = () => {
|
||||
const { hint, onClickHintFix } = this.props;
|
||||
if (onClickHintFix && hint && hint.fix) {
|
||||
onClickHintFix(hint.fix.action);
|
||||
const { hint, onExecuteHint } = this.props;
|
||||
if (onExecuteHint && hint && hint.fix) {
|
||||
onExecuteHint(hint.fix.action);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -242,29 +237,30 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
};
|
||||
|
||||
render() {
|
||||
const { error, hint, initialQuery } = this.props;
|
||||
const { error, hint, query } = this.props;
|
||||
const { metricsOptions, syntaxLoaded } = this.state;
|
||||
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
||||
const chooserText = syntaxLoaded ? 'Metrics' : 'Loading metrics...';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-inline gf-form-inline--nowrap">
|
||||
<div className="gf-form flex-shrink-0">
|
||||
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
||||
<button className="gf-form-label gf-form-label--btn" disabled={!syntaxLoaded}>
|
||||
{chooserText} <i className="fa fa-caret-down" />
|
||||
</button>
|
||||
</Cascader>
|
||||
</div>
|
||||
<div className="gf-form gf-form--grow">
|
||||
<div className="gf-form gf-form--grow flex-shrink-1">
|
||||
<QueryField
|
||||
additionalPlugins={this.plugins}
|
||||
cleanText={cleanText}
|
||||
initialQuery={initialQuery.expr}
|
||||
initialQuery={query.expr}
|
||||
onTypeahead={this.onTypeahead}
|
||||
onWillApplySuggestion={willApplySuggestion}
|
||||
onValueChanged={this.onChangeQuery}
|
||||
onQueryChange={this.onChangeQuery}
|
||||
onExecuteQuery={this.props.onExecuteQuery}
|
||||
placeholder="Enter a PromQL query"
|
||||
portalOrigin="prometheus"
|
||||
syntaxLoaded={syntaxLoaded}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PromCheatSheet from './PromCheatSheet';
|
||||
import { ExploreStartPageProps } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
onClickExample: () => void;
|
||||
}
|
||||
|
||||
export default class PromStart extends PureComponent<Props> {
|
||||
export default class PromStart extends PureComponent<ExploreStartPageProps> {
|
||||
render() {
|
||||
return (
|
||||
<div className="grafana-info-box grafana-info-box--max-lg">
|
||||
|
||||
@@ -10,21 +10,21 @@ import { Alignments } from './Alignments';
|
||||
import { AlignmentPeriods } from './AlignmentPeriods';
|
||||
import { AliasBy } from './AliasBy';
|
||||
import { Help } from './Help';
|
||||
import { Target, MetricDescriptor } from '../types';
|
||||
import { StackdriverQuery, MetricDescriptor } from '../types';
|
||||
import { getAlignmentPickerData } from '../functions';
|
||||
import StackdriverDatasource from '../datasource';
|
||||
import { SelectOptionItem } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
onQueryChange: (target: Target) => void;
|
||||
onQueryChange: (target: StackdriverQuery) => void;
|
||||
onExecuteQuery: () => void;
|
||||
target: Target;
|
||||
target: StackdriverQuery;
|
||||
events: any;
|
||||
datasource: StackdriverDatasource;
|
||||
templateSrv: TemplateSrv;
|
||||
}
|
||||
|
||||
interface State extends Target {
|
||||
interface State extends StackdriverQuery {
|
||||
alignOptions: SelectOptionItem[];
|
||||
lastQuery: string;
|
||||
lastQueryError: string;
|
||||
|
||||
@@ -2,9 +2,10 @@ import { stackdriverUnitMappings } from './constants';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import _ from 'lodash';
|
||||
import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
|
||||
import { MetricDescriptor } from './types';
|
||||
import { StackdriverQuery, MetricDescriptor } from './types';
|
||||
import { DataSourceApi, DataQueryOptions } from '@grafana/ui/src/types';
|
||||
|
||||
export default class StackdriverDatasource {
|
||||
export default class StackdriverDatasource implements DataSourceApi<StackdriverQuery> {
|
||||
id: number;
|
||||
url: string;
|
||||
baseUrl: string;
|
||||
@@ -39,9 +40,7 @@ export default class StackdriverDatasource {
|
||||
alignmentPeriod: this.templateSrv.replace(t.alignmentPeriod, options.scopedVars || {}),
|
||||
groupBys: this.interpolateGroupBys(t.groupBys, options.scopedVars),
|
||||
view: t.view || 'FULL',
|
||||
filters: (t.filters || []).map(f => {
|
||||
return this.templateSrv.replace(f, options.scopedVars || {});
|
||||
}),
|
||||
filters: this.interpolateFilters(t.filters, options.scopedVars),
|
||||
aliasBy: this.templateSrv.replace(t.aliasBy, options.scopedVars || {}),
|
||||
type: 'timeSeriesQuery',
|
||||
};
|
||||
@@ -63,7 +62,13 @@ export default class StackdriverDatasource {
|
||||
}
|
||||
}
|
||||
|
||||
async getLabels(metricType, refId) {
|
||||
interpolateFilters(filters: string[], scopedVars: object) {
|
||||
return (filters || []).map(f => {
|
||||
return this.templateSrv.replace(f, scopedVars || {}, 'regex');
|
||||
});
|
||||
}
|
||||
|
||||
async getLabels(metricType: string, refId: string) {
|
||||
const response = await this.getTimeSeries({
|
||||
targets: [
|
||||
{
|
||||
@@ -103,7 +108,7 @@ export default class StackdriverDatasource {
|
||||
return unit;
|
||||
}
|
||||
|
||||
async query(options) {
|
||||
async query(options: DataQueryOptions<StackdriverQuery>) {
|
||||
const result = [];
|
||||
const data = await this.getTimeSeries(options);
|
||||
if (data.results) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="100" height="90" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M-1-1h102v92H-1z"/><g><path d="M151.637 29.785c-1.659.621-3.32 1.241-4.783 2.055-1.548-7.686-18.278-8.18-18.117 1.021.148 8.228 18.35 8.414 22.9 16.065 3.456 5.808 1.064 14.28-3.417 17.433-1.805 1.271-4.625 3.234-10.936 3.076-7.568-.19-13.65-5.277-16.065-12.305 1.474-1.151 3.464-1.777 5.468-2.393.087 9.334 18.304 12.687 20.509 3.418 3.661-15.375-24.686-9.097-24.267-25.636.375-14.998 25.388-16.197 28.708-2.734zM207.347 68.413h-5.466v-4.444c-2.872 2.517-5.263 5.222-10.254 5.467-10.316.51-17.038-10.377-10.256-17.773 4.38-4.774 13.169-5.41 20.512-2.05 1.548-10.171-13.626-11.842-16.407-4.44-1.698-.697-3.195-1.592-5.126-2.054 2.832-10.246 20.01-9.729 24.949-2.392 4.608 6.837.757 17.618 2.048 27.686zm-22.216-7.866c4.483 6.856 17.435 2.377 16.751-6.154-5.161-3.469-18.501-3.389-16.751 6.154zM416.873 53.029c-7.868.794-17.201.117-25.638.343-1.48 10.76 16.123 14.618 19.144 5.127 1.631.754 3.326 1.457 5.127 2.048-2.477 9.824-18.37 11.251-25.294 4.445-9.549-9.386-4.276-31.335 12.987-29.735 8.89.826 13.149 7.176 13.674 17.772zm-25.295-4.444h18.801c-.04-11.168-18.433-9.957-18.801 0zM347.486 36.283v32.13h-5.81v-32.13h5.81zM352.273 36.283h6.153c3.048 8.342 6.48 16.303 9.224 24.949 4.33-7.408 6.575-16.895 10.251-24.949h6.155c-4.39 10.646-8.865 21.217-12.988 32.13h-6.152c-3.907-11.019-8.635-21.217-12.643-32.13zM427.354 36.225h-5.525v32.111h5.982V48.885s1.845-9.322 11.396-7.021l2.417-5.867s-8.978-2.532-14.155 5.407l-.115-5.179zM322.434 36.225h-5.522v32.111h5.987V48.885s1.84-9.322 11.395-7.021l2.417-5.867s-8.976-2.532-14.159 5.407l-.118-5.179zM304.139 51.998c0 6.579-4.645 11.919-10.372 11.919-5.725 0-10.366-5.34-10.366-11.919 0-6.586 4.642-11.92 10.366-11.92 5.727 0 10.372 5.334 10.372 11.92zm-.107 11.916v4.19h5.742V21.038h-5.812v19.325c-2.789-3.472-6.805-5.649-11.269-5.649-8.424 0-15.253 7.768-15.253 17.341 0 9.576 6.829 17.344 15.253 17.344 4.496 0 8.536-2.21 11.33-5.724l.009.239z" fill="#6F6F6F"/><circle r="4.185" cy="25.306" cx="344.584" fill="#6F6F6F"/><path fill="#6F6F6F" d="M253.751 50.332l13.835-14.078h7.603l-12.337 12.711 13.21 19.321h-7.354l-10.346-14.959-4.738 4.861v10.225h-5.856V21.422h5.856v28.443zM236.855 46.471c-1.762-3.644-5.163-6.109-9.065-6.109-5.713 0-10.348 5.282-10.348 11.799 0 6.524 4.635 11.806 10.348 11.806 3.93 0 7.347-2.496 9.101-6.183l5.394 2.556c-2.779 5.419-8.227 9.097-14.497 9.097-9.083 0-16.451-7.733-16.451-17.275 0-9.537 7.368-17.269 16.451-17.269 6.247 0 11.683 3.653 14.467 9.041l-5.4 2.537zM160.884 26.693v9.747h-5.849v5.479h5.727v17.052s-.37 13.157 15.103 9.383l-1.947-4.995s-7.065 2.434-7.065-3.896V41.919h7.796V36.56h-7.674v-9.866h-6.091v-.001z"/><path fill="#009245" d="M50.794 41.715L27.708 84.812l46.207-.008z"/><path fill="#006837" d="M27.699 84.804L4.833 44.994h45.958z"/><path fill="#39B54A" d="M50.913 45.008H4.833L27.898 5.12H74.031z"/><path fill="#8CC63F" d="M74.031 5.12l23.236 39.84-23.352 39.844-23.002-39.796z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -14,8 +14,8 @@
|
||||
"description": "Google Stackdriver Datasource for Grafana",
|
||||
"version": "1.0.0",
|
||||
"logos": {
|
||||
"small": "img/stackdriver_logo.png",
|
||||
"large": "img/stackdriver_logo.png"
|
||||
"small": "img/stackdriver_logo.svg",
|
||||
"large": "img/stackdriver_logo.svg"
|
||||
},
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
import { Target } from './types';
|
||||
import { StackdriverQuery } from './types';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
export class StackdriverQueryCtrl extends QueryCtrl {
|
||||
@@ -16,7 +16,7 @@ export class StackdriverQueryCtrl extends QueryCtrl {
|
||||
this.onExecuteQuery = this.onExecuteQuery.bind(this);
|
||||
}
|
||||
|
||||
onQueryChange(target: Target) {
|
||||
onQueryChange(target: StackdriverQuery) {
|
||||
Object.assign(this.target, target);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import StackdriverDataSource from '../datasource';
|
||||
import { metricDescriptors } from './testData';
|
||||
import moment from 'moment';
|
||||
import { TemplateSrvStub } from 'test/specs/helpers';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { CustomVariable } from 'app/features/templating/all';
|
||||
|
||||
describe('StackdriverDataSource', () => {
|
||||
const instanceSettings = {
|
||||
@@ -9,7 +10,7 @@ describe('StackdriverDataSource', () => {
|
||||
defaultProject: 'testproject',
|
||||
},
|
||||
};
|
||||
const templateSrv = new TemplateSrvStub();
|
||||
const templateSrv = new TemplateSrv();
|
||||
const timeSrv = {};
|
||||
|
||||
describe('when performing testDataSource', () => {
|
||||
@@ -154,15 +155,41 @@ describe('StackdriverDataSource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when interpolating a template variable for the filter', () => {
|
||||
let interpolated;
|
||||
describe('and is single value variable', () => {
|
||||
beforeEach(() => {
|
||||
const filterTemplateSrv = initTemplateSrv('filtervalue1');
|
||||
const ds = new StackdriverDataSource(instanceSettings, {}, filterTemplateSrv, timeSrv);
|
||||
interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '${test}'], {});
|
||||
});
|
||||
|
||||
it('should replace the variable with the value', () => {
|
||||
expect(interpolated.length).toBe(3);
|
||||
expect(interpolated[2]).toBe('filtervalue1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and is multi value variable', () => {
|
||||
beforeEach(() => {
|
||||
const filterTemplateSrv = initTemplateSrv(['filtervalue1', 'filtervalue2'], true);
|
||||
const ds = new StackdriverDataSource(instanceSettings, {}, filterTemplateSrv, timeSrv);
|
||||
interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '[[test]]'], {});
|
||||
});
|
||||
|
||||
it('should replace the variable with a regex expression', () => {
|
||||
expect(interpolated[2]).toBe('(filtervalue1|filtervalue2)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when interpolating a template variable for group bys', () => {
|
||||
let interpolated;
|
||||
|
||||
describe('and is single value variable', () => {
|
||||
beforeEach(() => {
|
||||
templateSrv.data = {
|
||||
test: 'groupby1',
|
||||
};
|
||||
const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv, timeSrv);
|
||||
const groupByTemplateSrv = initTemplateSrv('groupby1');
|
||||
const ds = new StackdriverDataSource(instanceSettings, {}, groupByTemplateSrv, timeSrv);
|
||||
interpolated = ds.interpolateGroupBys(['[[test]]'], {});
|
||||
});
|
||||
|
||||
@@ -174,10 +201,8 @@ describe('StackdriverDataSource', () => {
|
||||
|
||||
describe('and is multi value variable', () => {
|
||||
beforeEach(() => {
|
||||
templateSrv.data = {
|
||||
test: 'groupby1,groupby2',
|
||||
};
|
||||
const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv, timeSrv);
|
||||
const groupByTemplateSrv = initTemplateSrv(['groupby1', 'groupby2'], true);
|
||||
const ds = new StackdriverDataSource(instanceSettings, {}, groupByTemplateSrv, timeSrv);
|
||||
interpolated = ds.interpolateGroupBys(['[[test]]'], {});
|
||||
});
|
||||
|
||||
@@ -241,3 +266,19 @@ describe('StackdriverDataSource', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
function initTemplateSrv(values: any, multi = false) {
|
||||
const templateSrv = new TemplateSrv();
|
||||
templateSrv.init([
|
||||
new CustomVariable(
|
||||
{
|
||||
name: 'test',
|
||||
current: {
|
||||
value: values,
|
||||
},
|
||||
multi: multi,
|
||||
},
|
||||
{}
|
||||
),
|
||||
]);
|
||||
return templateSrv;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DataQuery } from '@grafana/ui/src/types';
|
||||
|
||||
export enum MetricFindQueryTypes {
|
||||
Services = 'services',
|
||||
MetricTypes = 'metricTypes',
|
||||
@@ -20,20 +22,22 @@ export interface VariableQueryData {
|
||||
services: Array<{ value: string; name: string }>;
|
||||
}
|
||||
|
||||
export interface Target {
|
||||
defaultProject: string;
|
||||
unit: string;
|
||||
export interface StackdriverQuery extends DataQuery {
|
||||
defaultProject?: string;
|
||||
unit?: string;
|
||||
metricType: string;
|
||||
service: string;
|
||||
service?: string;
|
||||
refId: string;
|
||||
crossSeriesReducer: string;
|
||||
alignmentPeriod: string;
|
||||
alignmentPeriod?: string;
|
||||
perSeriesAligner: string;
|
||||
groupBys: string[];
|
||||
filters: string[];
|
||||
aliasBy: string;
|
||||
groupBys?: string[];
|
||||
filters?: string[];
|
||||
aliasBy?: string;
|
||||
metricKind: string;
|
||||
valueType: string;
|
||||
datasourceId?: number;
|
||||
view?: string;
|
||||
}
|
||||
|
||||
export interface AnnotationTarget {
|
||||
|
||||
@@ -41,9 +41,9 @@ export class QueryEditor extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
onScenarioChange = (item: SelectOptionItem) => {
|
||||
this.props.onQueryChange({
|
||||
this.props.onChange({
|
||||
...this.props.query,
|
||||
scenarioId: item.value,
|
||||
...this.props.query
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user