mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Query hints for prometheus (#12833)
* Explore: Query hints for prometheus - time series are analyzed on response - hints are shown per query - some hints have fixes - fix rendered as link after hint - click on fix executes the fix action * Added tests for determineQueryHints() * Fix index for rate hints in explore
This commit is contained in:
parent
817179c097
commit
c1b9bbc2cf
@ -19,6 +19,16 @@ import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
|
|||||||
|
|
||||||
const MAX_HISTORY_ITEMS = 100;
|
const MAX_HISTORY_ITEMS = 100;
|
||||||
|
|
||||||
|
function makeHints(hints) {
|
||||||
|
const hintsByIndex = [];
|
||||||
|
hints.forEach(hint => {
|
||||||
|
if (hint) {
|
||||||
|
hintsByIndex[hint.index] = hint;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return hintsByIndex;
|
||||||
|
}
|
||||||
|
|
||||||
function makeTimeSeriesList(dataList, options) {
|
function makeTimeSeriesList(dataList, options) {
|
||||||
return dataList.map((seriesData, index) => {
|
return dataList.map((seriesData, index) => {
|
||||||
const datapoints = seriesData.datapoints || [];
|
const datapoints = seriesData.datapoints || [];
|
||||||
@ -37,7 +47,7 @@ function makeTimeSeriesList(dataList, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseInitialState(initial: string | undefined) {
|
function parseUrlState(initial: string | undefined) {
|
||||||
if (initial) {
|
if (initial) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(decodePathComponent(initial));
|
const parsed = JSON.parse(decodePathComponent(initial));
|
||||||
@ -64,8 +74,9 @@ interface IExploreState {
|
|||||||
latency: number;
|
latency: number;
|
||||||
loading: any;
|
loading: any;
|
||||||
logsResult: any;
|
logsResult: any;
|
||||||
queries: any;
|
queries: any[];
|
||||||
queryError: any;
|
queryErrors: any[];
|
||||||
|
queryHints: any[];
|
||||||
range: any;
|
range: any;
|
||||||
requestOptions: any;
|
requestOptions: any;
|
||||||
showingGraph: boolean;
|
showingGraph: boolean;
|
||||||
@ -82,7 +93,8 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { datasource, queries, range } = parseInitialState(props.routeParams.state);
|
const initialState: IExploreState = props.initialState;
|
||||||
|
const { datasource, queries, range } = parseUrlState(props.routeParams.state);
|
||||||
this.state = {
|
this.state = {
|
||||||
datasource: null,
|
datasource: null,
|
||||||
datasourceError: null,
|
datasourceError: null,
|
||||||
@ -95,7 +107,8 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
loading: false,
|
loading: false,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
queries: ensureQueries(queries),
|
queries: ensureQueries(queries),
|
||||||
queryError: null,
|
queryErrors: [],
|
||||||
|
queryHints: [],
|
||||||
range: range || { ...DEFAULT_RANGE },
|
range: range || { ...DEFAULT_RANGE },
|
||||||
requestOptions: null,
|
requestOptions: null,
|
||||||
showingGraph: true,
|
showingGraph: true,
|
||||||
@ -105,7 +118,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
supportsLogs: null,
|
supportsLogs: null,
|
||||||
supportsTable: null,
|
supportsTable: null,
|
||||||
tableResult: null,
|
tableResult: null,
|
||||||
...props.initialState,
|
...initialState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,6 +204,8 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
datasourceLoading: true,
|
datasourceLoading: true,
|
||||||
graphResult: null,
|
graphResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
|
queryErrors: [],
|
||||||
|
queryHints: [],
|
||||||
tableResult: null,
|
tableResult: null,
|
||||||
});
|
});
|
||||||
const datasource = await this.props.datasourceSrv.get(option.value);
|
const datasource = await this.props.datasourceSrv.get(option.value);
|
||||||
@ -199,6 +214,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
|
|
||||||
onChangeQuery = (value: string, index: number, override?: boolean) => {
|
onChangeQuery = (value: string, index: number, override?: boolean) => {
|
||||||
const { queries } = this.state;
|
const { queries } = this.state;
|
||||||
|
let { queryErrors, queryHints } = this.state;
|
||||||
const prevQuery = queries[index];
|
const prevQuery = queries[index];
|
||||||
const edited = override ? false : prevQuery.query !== value;
|
const edited = override ? false : prevQuery.query !== value;
|
||||||
const nextQuery = {
|
const nextQuery = {
|
||||||
@ -208,7 +224,18 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
};
|
};
|
||||||
const nextQueries = [...queries];
|
const nextQueries = [...queries];
|
||||||
nextQueries[index] = nextQuery;
|
nextQueries[index] = nextQuery;
|
||||||
this.setState({ queries: nextQueries }, override ? () => this.onSubmit() : undefined);
|
if (override) {
|
||||||
|
queryErrors = [];
|
||||||
|
queryHints = [];
|
||||||
|
}
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
queryErrors,
|
||||||
|
queryHints,
|
||||||
|
queries: nextQueries,
|
||||||
|
},
|
||||||
|
override ? () => this.onSubmit() : undefined
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeTime = nextRange => {
|
onChangeTime = nextRange => {
|
||||||
@ -255,13 +282,32 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onClickTableCell = (columnKey: string, rowValue: string) => {
|
onClickTableCell = (columnKey: string, rowValue: string) => {
|
||||||
|
this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue });
|
||||||
|
};
|
||||||
|
|
||||||
|
onModifyQueries = (action: object, index?: number) => {
|
||||||
const { datasource, queries } = this.state;
|
const { datasource, queries } = this.state;
|
||||||
if (datasource && datasource.modifyQuery) {
|
if (datasource && datasource.modifyQuery) {
|
||||||
const nextQueries = queries.map(q => ({
|
let nextQueries;
|
||||||
...q,
|
if (index === undefined) {
|
||||||
edited: false,
|
// Modify all queries
|
||||||
query: datasource.modifyQuery(q.query, { addFilter: { key: columnKey, value: rowValue } }),
|
nextQueries = queries.map(q => ({
|
||||||
}));
|
...q,
|
||||||
|
edited: false,
|
||||||
|
query: datasource.modifyQuery(q.query, action),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// Modify query only at index
|
||||||
|
nextQueries = [
|
||||||
|
...queries.slice(0, index),
|
||||||
|
{
|
||||||
|
...queries[index],
|
||||||
|
edited: false,
|
||||||
|
query: datasource.modifyQuery(queries[index].query, action),
|
||||||
|
},
|
||||||
|
...queries.slice(index + 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
this.setState({ queries: nextQueries }, () => this.onSubmit());
|
this.setState({ queries: nextQueries }, () => this.onSubmit());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -309,7 +355,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
this.setState({ history });
|
this.setState({ history });
|
||||||
}
|
}
|
||||||
|
|
||||||
buildQueryOptions(targetOptions: { format: string; instant?: boolean }) {
|
buildQueryOptions(targetOptions: { format: string; hinting?: boolean; instant?: boolean }) {
|
||||||
const { datasource, queries, range } = this.state;
|
const { datasource, queries, range } = this.state;
|
||||||
const resolution = this.el.offsetWidth;
|
const resolution = this.el.offsetWidth;
|
||||||
const absoluteRange = {
|
const absoluteRange = {
|
||||||
@ -333,19 +379,20 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
if (!hasQuery(queries)) {
|
if (!hasQuery(queries)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ latency: 0, loading: true, graphResult: null, queryError: null });
|
this.setState({ latency: 0, loading: true, graphResult: null, queryErrors: [], queryHints: [] });
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const options = this.buildQueryOptions({ format: 'time_series', instant: false });
|
const options = this.buildQueryOptions({ format: 'time_series', instant: false, hinting: true });
|
||||||
try {
|
try {
|
||||||
const res = await datasource.query(options);
|
const res = await datasource.query(options);
|
||||||
const result = makeTimeSeriesList(res.data, options);
|
const result = makeTimeSeriesList(res.data, options);
|
||||||
|
const queryHints = res.hints ? makeHints(res.hints) : [];
|
||||||
const latency = Date.now() - now;
|
const latency = Date.now() - now;
|
||||||
this.setState({ latency, loading: false, graphResult: result, requestOptions: options });
|
this.setState({ latency, loading: false, graphResult: result, queryHints, requestOptions: options });
|
||||||
this.onQuerySuccess(datasource.meta.id, queries);
|
this.onQuerySuccess(datasource.meta.id, queries);
|
||||||
} catch (response) {
|
} catch (response) {
|
||||||
console.error(response);
|
console.error(response);
|
||||||
const queryError = response.data ? response.data.error : response;
|
const queryError = response.data ? response.data.error : response;
|
||||||
this.setState({ loading: false, queryError });
|
this.setState({ loading: false, queryErrors: [queryError] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,7 +401,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
if (!hasQuery(queries)) {
|
if (!hasQuery(queries)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ latency: 0, loading: true, queryError: null, tableResult: null });
|
this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], tableResult: null });
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const options = this.buildQueryOptions({
|
const options = this.buildQueryOptions({
|
||||||
format: 'table',
|
format: 'table',
|
||||||
@ -369,7 +416,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
} catch (response) {
|
} catch (response) {
|
||||||
console.error(response);
|
console.error(response);
|
||||||
const queryError = response.data ? response.data.error : response;
|
const queryError = response.data ? response.data.error : response;
|
||||||
this.setState({ loading: false, queryError });
|
this.setState({ loading: false, queryErrors: [queryError] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +425,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
if (!hasQuery(queries)) {
|
if (!hasQuery(queries)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ latency: 0, loading: true, queryError: null, logsResult: null });
|
this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], logsResult: null });
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const options = this.buildQueryOptions({
|
const options = this.buildQueryOptions({
|
||||||
format: 'logs',
|
format: 'logs',
|
||||||
@ -393,7 +440,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
} catch (response) {
|
} catch (response) {
|
||||||
console.error(response);
|
console.error(response);
|
||||||
const queryError = response.data ? response.data.error : response;
|
const queryError = response.data ? response.data.error : response;
|
||||||
this.setState({ loading: false, queryError });
|
this.setState({ loading: false, queryErrors: [queryError] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,7 +462,8 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
loading,
|
loading,
|
||||||
logsResult,
|
logsResult,
|
||||||
queries,
|
queries,
|
||||||
queryError,
|
queryErrors,
|
||||||
|
queryHints,
|
||||||
range,
|
range,
|
||||||
requestOptions,
|
requestOptions,
|
||||||
showingGraph,
|
showingGraph,
|
||||||
@ -449,12 +497,12 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="navbar-buttons explore-first-button">
|
<div className="navbar-buttons explore-first-button">
|
||||||
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
|
||||||
Close Split
|
Close Split
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!datasourceMissing ? (
|
{!datasourceMissing ? (
|
||||||
<div className="navbar-buttons">
|
<div className="navbar-buttons">
|
||||||
<Select
|
<Select
|
||||||
@ -504,14 +552,15 @@ export class Explore extends React.Component<any, IExploreState> {
|
|||||||
<QueryRows
|
<QueryRows
|
||||||
history={history}
|
history={history}
|
||||||
queries={queries}
|
queries={queries}
|
||||||
|
queryErrors={queryErrors}
|
||||||
|
queryHints={queryHints}
|
||||||
request={this.request}
|
request={this.request}
|
||||||
onAddQueryRow={this.onAddQueryRow}
|
onAddQueryRow={this.onAddQueryRow}
|
||||||
onChangeQuery={this.onChangeQuery}
|
onChangeQuery={this.onChangeQuery}
|
||||||
|
onClickHintFix={this.onModifyQueries}
|
||||||
onExecuteQuery={this.onSubmit}
|
onExecuteQuery={this.onSubmit}
|
||||||
onRemoveQueryRow={this.onRemoveQueryRow}
|
onRemoveQueryRow={this.onRemoveQueryRow}
|
||||||
/>
|
/>
|
||||||
{queryError && !loading ? <div className="text-warning m-a-2">{queryError}</div> : null}
|
|
||||||
|
|
||||||
<div className="result-options">
|
<div className="result-options">
|
||||||
{supportsGraph ? (
|
{supportsGraph ? (
|
||||||
<button className={`btn navbar-button ${graphButtonActive}`} onClick={this.onClickGraphButton}>
|
<button className={`btn navbar-button ${graphButtonActive}`} onClick={this.onClickGraphButton}>
|
||||||
|
@ -105,13 +105,16 @@ interface CascaderOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PromQueryFieldProps {
|
interface PromQueryFieldProps {
|
||||||
history?: any[];
|
error?: string;
|
||||||
|
hint?: any;
|
||||||
histogramMetrics?: string[];
|
histogramMetrics?: string[];
|
||||||
|
history?: any[];
|
||||||
initialQuery?: string | null;
|
initialQuery?: string | null;
|
||||||
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,...]
|
||||||
metrics?: string[];
|
metrics?: string[];
|
||||||
metricsByPrefix?: CascaderOption[];
|
metricsByPrefix?: CascaderOption[];
|
||||||
|
onClickHintFix?: (action: any) => void;
|
||||||
onPressEnter?: () => void;
|
onPressEnter?: () => void;
|
||||||
onQueryChange?: (value: string, override?: boolean) => void;
|
onQueryChange?: (value: string, override?: boolean) => void;
|
||||||
portalPrefix?: string;
|
portalPrefix?: string;
|
||||||
@ -189,6 +192,13 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onClickHintFix = () => {
|
||||||
|
const { hint, onClickHintFix } = this.props;
|
||||||
|
if (onClickHintFix && hint && hint.fix) {
|
||||||
|
onClickHintFix(hint.fix.action);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onReceiveMetrics = () => {
|
onReceiveMetrics = () => {
|
||||||
if (!this.state.metrics) {
|
if (!this.state.metrics) {
|
||||||
return;
|
return;
|
||||||
@ -435,6 +445,7 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { error, hint } = this.props;
|
||||||
const { histogramMetrics, metricsByPrefix } = this.state;
|
const { histogramMetrics, metricsByPrefix } = this.state;
|
||||||
const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
|
const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
|
||||||
const metricsOptions = [
|
const metricsOptions = [
|
||||||
@ -449,16 +460,29 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
|
|||||||
<button className="btn navbar-button navbar-button--tight">Metrics</button>
|
<button className="btn navbar-button navbar-button--tight">Metrics</button>
|
||||||
</Cascader>
|
</Cascader>
|
||||||
</div>
|
</div>
|
||||||
<div className="slate-query-field-wrapper">
|
<div className="prom-query-field-wrapper">
|
||||||
<TypeaheadField
|
<div className="slate-query-field-wrapper">
|
||||||
additionalPlugins={this.plugins}
|
<TypeaheadField
|
||||||
cleanText={cleanText}
|
additionalPlugins={this.plugins}
|
||||||
initialValue={this.props.initialQuery}
|
cleanText={cleanText}
|
||||||
onTypeahead={this.onTypeahead}
|
initialValue={this.props.initialQuery}
|
||||||
onWillApplySuggestion={willApplySuggestion}
|
onTypeahead={this.onTypeahead}
|
||||||
onValueChanged={this.onChangeQuery}
|
onWillApplySuggestion={willApplySuggestion}
|
||||||
placeholder="Enter a PromQL query"
|
onValueChanged={this.onChangeQuery}
|
||||||
/>
|
placeholder="Enter a PromQL query"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error ? <div className="prom-query-field-info text-error">{error}</div> : null}
|
||||||
|
{hint ? (
|
||||||
|
<div className="prom-query-field-info text-warning">
|
||||||
|
{hint.label}{' '}
|
||||||
|
{hint.fix ? (
|
||||||
|
<a className="text-link muted" onClick={this.onClickHintFix}>
|
||||||
|
{hint.fix.label}
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
// TODO make this datasource-plugin-dependent
|
||||||
import QueryField from './PromQueryField';
|
import QueryField from './PromQueryField';
|
||||||
|
|
||||||
class QueryRow extends PureComponent<any, {}> {
|
class QueryRow extends PureComponent<any, {}> {
|
||||||
@ -21,6 +22,13 @@ class QueryRow extends PureComponent<any, {}> {
|
|||||||
this.onChangeQuery('', true);
|
this.onChangeQuery('', true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onClickHintFix = action => {
|
||||||
|
const { index, onClickHintFix } = this.props;
|
||||||
|
if (onClickHintFix) {
|
||||||
|
onClickHintFix(action, index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onClickRemoveButton = () => {
|
onClickRemoveButton = () => {
|
||||||
const { index, onRemoveQueryRow } = this.props;
|
const { index, onRemoveQueryRow } = this.props;
|
||||||
if (onRemoveQueryRow) {
|
if (onRemoveQueryRow) {
|
||||||
@ -36,14 +44,17 @@ class QueryRow extends PureComponent<any, {}> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { edited, history, query, request } = this.props;
|
const { edited, history, query, queryError, queryHint, request } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="query-row">
|
<div className="query-row">
|
||||||
<div className="query-row-field">
|
<div className="query-row-field">
|
||||||
<QueryField
|
<QueryField
|
||||||
|
error={queryError}
|
||||||
|
hint={queryHint}
|
||||||
initialQuery={edited ? null : query}
|
initialQuery={edited ? null : query}
|
||||||
history={history}
|
history={history}
|
||||||
portalPrefix="explore"
|
portalPrefix="explore"
|
||||||
|
onClickHintFix={this.onClickHintFix}
|
||||||
onPressEnter={this.onPressEnter}
|
onPressEnter={this.onPressEnter}
|
||||||
onQueryChange={this.onChangeQuery}
|
onQueryChange={this.onChangeQuery}
|
||||||
request={request}
|
request={request}
|
||||||
@ -67,11 +78,19 @@ class QueryRow extends PureComponent<any, {}> {
|
|||||||
|
|
||||||
export default class QueryRows extends PureComponent<any, {}> {
|
export default class QueryRows extends PureComponent<any, {}> {
|
||||||
render() {
|
render() {
|
||||||
const { className = '', queries, ...handlers } = this.props;
|
const { className = '', queries, queryErrors = [], queryHints = [], ...handlers } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{queries.map((q, index) => (
|
{queries.map((q, index) => (
|
||||||
<QueryRow key={q.key} index={index} query={q.query} edited={q.edited} {...handlers} />
|
<QueryRow
|
||||||
|
key={q.key}
|
||||||
|
index={index}
|
||||||
|
query={q.query}
|
||||||
|
queryError={queryErrors[index]}
|
||||||
|
queryHint={queryHints[index]}
|
||||||
|
edited={q.edited}
|
||||||
|
{...handlers}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -82,6 +82,68 @@ export function addLabelToQuery(query: string, key: string, value: string): stri
|
|||||||
return parts.join('');
|
return parts.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function determineQueryHints(series: any[]): any[] {
|
||||||
|
const hints = series.map((s, i) => {
|
||||||
|
const query: string = s.query;
|
||||||
|
const index: number = s.responseIndex;
|
||||||
|
if (query === undefined || index === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..._bucket metric needs a histogram_quantile()
|
||||||
|
const histogramMetric = query.trim().match(/^\w+_bucket$/);
|
||||||
|
if (histogramMetric) {
|
||||||
|
const label = 'Time series has buckets, you probably wanted a histogram.';
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
label,
|
||||||
|
fix: {
|
||||||
|
label: 'Fix by adding histogram_quantile().',
|
||||||
|
action: {
|
||||||
|
type: 'ADD_HISTOGRAM_QUANTILE',
|
||||||
|
query,
|
||||||
|
index,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for monotony
|
||||||
|
const datapoints: [number, number][] = s.datapoints;
|
||||||
|
const simpleMetric = query.trim().match(/^\w+$/);
|
||||||
|
if (simpleMetric && datapoints.length > 1) {
|
||||||
|
let increasing = false;
|
||||||
|
const monotonic = datapoints.every((dp, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
increasing = increasing || dp[0] > datapoints[index - 1][0];
|
||||||
|
// monotonic?
|
||||||
|
return dp[0] >= datapoints[index - 1][0];
|
||||||
|
});
|
||||||
|
if (increasing && monotonic) {
|
||||||
|
const label = 'Time series is monotonously increasing.';
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
index,
|
||||||
|
fix: {
|
||||||
|
label: 'Fix by adding rate().',
|
||||||
|
action: {
|
||||||
|
type: 'ADD_RATE',
|
||||||
|
query,
|
||||||
|
index,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No hint found
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return hints;
|
||||||
|
}
|
||||||
|
|
||||||
export function prometheusRegularEscape(value) {
|
export function prometheusRegularEscape(value) {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return value.replace(/'/g, "\\\\'");
|
return value.replace(/'/g, "\\\\'");
|
||||||
@ -223,10 +285,15 @@ export class PrometheusDatasource {
|
|||||||
|
|
||||||
return this.$q.all(allQueryPromise).then(responseList => {
|
return this.$q.all(allQueryPromise).then(responseList => {
|
||||||
let result = [];
|
let result = [];
|
||||||
|
let hints = [];
|
||||||
|
|
||||||
_.each(responseList, (response, index) => {
|
_.each(responseList, (response, index) => {
|
||||||
if (response.status === 'error') {
|
if (response.status === 'error') {
|
||||||
throw response.error;
|
const error = {
|
||||||
|
index,
|
||||||
|
...response.error,
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keeping original start/end for transformers
|
// Keeping original start/end for transformers
|
||||||
@ -241,16 +308,24 @@ export class PrometheusDatasource {
|
|||||||
responseIndex: index,
|
responseIndex: index,
|
||||||
refId: activeTargets[index].refId,
|
refId: activeTargets[index].refId,
|
||||||
};
|
};
|
||||||
this.resultTransformer.transform(result, response, transformerOptions);
|
const series = this.resultTransformer.transform(response, transformerOptions);
|
||||||
|
result = [...result, ...series];
|
||||||
|
|
||||||
|
if (queries[index].hinting) {
|
||||||
|
const queryHints = determineQueryHints(series);
|
||||||
|
hints = [...hints, ...queryHints];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: result };
|
return { data: result, hints };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createQuery(target, options, start, end) {
|
createQuery(target, options, start, end) {
|
||||||
var query: any = {};
|
const query: any = {
|
||||||
query.instant = target.instant;
|
hinting: target.hinting,
|
||||||
|
instant: target.instant,
|
||||||
|
};
|
||||||
var range = Math.ceil(end - start);
|
var range = Math.ceil(end - start);
|
||||||
|
|
||||||
var interval = kbn.interval_to_seconds(options.interval);
|
var interval = kbn.interval_to_seconds(options.interval);
|
||||||
@ -450,12 +525,20 @@ export class PrometheusDatasource {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyQuery(query: string, options: any): string {
|
modifyQuery(query: string, action: any): string {
|
||||||
const { addFilter } = options;
|
switch (action.type) {
|
||||||
if (addFilter) {
|
case 'ADD_FILTER': {
|
||||||
return addLabelToQuery(query, addFilter.key, addFilter.value);
|
return addLabelToQuery(query, action.key, action.value);
|
||||||
|
}
|
||||||
|
case 'ADD_HISTOGRAM_QUANTILE': {
|
||||||
|
return `histogram_quantile(0.95, sum(rate(${query}[5m])) by (le))`;
|
||||||
|
}
|
||||||
|
case 'ADD_RATE': {
|
||||||
|
return `rate(${query}[5m])`;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
return query;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPrometheusTime(date, roundUp) {
|
getPrometheusTime(date, roundUp) {
|
||||||
|
@ -4,11 +4,11 @@ import TableModel from 'app/core/table_model';
|
|||||||
export class ResultTransformer {
|
export class ResultTransformer {
|
||||||
constructor(private templateSrv) {}
|
constructor(private templateSrv) {}
|
||||||
|
|
||||||
transform(result: any, response: any, options: any) {
|
transform(response: any, options: any): any[] {
|
||||||
let prometheusResult = response.data.data.result;
|
let prometheusResult = response.data.data.result;
|
||||||
|
|
||||||
if (options.format === 'table') {
|
if (options.format === 'table') {
|
||||||
result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId));
|
return [this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId)];
|
||||||
} else if (options.format === 'heatmap') {
|
} else if (options.format === 'heatmap') {
|
||||||
let seriesList = [];
|
let seriesList = [];
|
||||||
prometheusResult.sort(sortSeriesByLabel);
|
prometheusResult.sort(sortSeriesByLabel);
|
||||||
@ -16,16 +16,19 @@ export class ResultTransformer {
|
|||||||
seriesList.push(this.transformMetricData(metricData, options, options.start, options.end));
|
seriesList.push(this.transformMetricData(metricData, options, options.start, options.end));
|
||||||
}
|
}
|
||||||
seriesList = this.transformToHistogramOverTime(seriesList);
|
seriesList = this.transformToHistogramOverTime(seriesList);
|
||||||
result.push(...seriesList);
|
return seriesList;
|
||||||
} else {
|
} else {
|
||||||
|
let seriesList = [];
|
||||||
for (let metricData of prometheusResult) {
|
for (let metricData of prometheusResult) {
|
||||||
if (response.data.data.resultType === 'matrix') {
|
if (response.data.data.resultType === 'matrix') {
|
||||||
result.push(this.transformMetricData(metricData, options, options.start, options.end));
|
seriesList.push(this.transformMetricData(metricData, options, options.start, options.end));
|
||||||
} else if (response.data.data.resultType === 'vector') {
|
} else if (response.data.data.resultType === 'vector') {
|
||||||
result.push(this.transformInstantMetricData(metricData, options));
|
seriesList.push(this.transformInstantMetricData(metricData, options));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return seriesList;
|
||||||
}
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
transformMetricData(metricData, options, start, end) {
|
transformMetricData(metricData, options, start, end) {
|
||||||
@ -60,7 +63,12 @@ export class ResultTransformer {
|
|||||||
dps.push([null, t]);
|
dps.push([null, t]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { target: metricLabel, datapoints: dps };
|
return {
|
||||||
|
datapoints: dps,
|
||||||
|
query: options.query,
|
||||||
|
responseIndex: options.responseIndex,
|
||||||
|
target: metricLabel,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
transformMetricDataToTable(md, resultCount: number, refId: string) {
|
transformMetricDataToTable(md, resultCount: number, refId: string) {
|
||||||
@ -124,7 +132,7 @@ export class ResultTransformer {
|
|||||||
metricLabel = null;
|
metricLabel = null;
|
||||||
metricLabel = this.createMetricLabel(md.metric, options);
|
metricLabel = this.createMetricLabel(md.metric, options);
|
||||||
dps.push([parseFloat(md.value[1]), md.value[0] * 1000]);
|
dps.push([parseFloat(md.value[1]), md.value[0] * 1000]);
|
||||||
return { target: metricLabel, datapoints: dps };
|
return { target: metricLabel, datapoints: dps, labels: md.metric };
|
||||||
}
|
}
|
||||||
|
|
||||||
createMetricLabel(labelData, options) {
|
createMetricLabel(labelData, options) {
|
||||||
|
@ -3,6 +3,7 @@ import moment from 'moment';
|
|||||||
import q from 'q';
|
import q from 'q';
|
||||||
import {
|
import {
|
||||||
alignRange,
|
alignRange,
|
||||||
|
determineQueryHints,
|
||||||
PrometheusDatasource,
|
PrometheusDatasource,
|
||||||
prometheusSpecialRegexEscape,
|
prometheusSpecialRegexEscape,
|
||||||
prometheusRegularEscape,
|
prometheusRegularEscape,
|
||||||
@ -122,7 +123,7 @@ describe('PrometheusDatasource', () => {
|
|||||||
ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock);
|
ctx.ds.performTimeSeriesQuery = jest.fn().mockReturnValue(responseMock);
|
||||||
return ctx.ds.query(ctx.query).then(result => {
|
return ctx.ds.query(ctx.query).then(result => {
|
||||||
let results = result.data;
|
let results = result.data;
|
||||||
return expect(results).toEqual(expected);
|
return expect(results).toMatchObject(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -180,6 +181,54 @@ describe('PrometheusDatasource', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('determineQueryHints()', () => {
|
||||||
|
it('returns no hints for no series', () => {
|
||||||
|
expect(determineQueryHints([])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns no hints for empty series', () => {
|
||||||
|
expect(determineQueryHints([{ datapoints: [], query: '' }])).toEqual([null]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns no hint for a monotonously decreasing series', () => {
|
||||||
|
const series = [{ datapoints: [[23, 1000], [22, 1001]], query: 'metric', responseIndex: 0 }];
|
||||||
|
const hints = determineQueryHints(series);
|
||||||
|
expect(hints).toEqual([null]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a rate hint for a monotonously increasing series', () => {
|
||||||
|
const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'metric', responseIndex: 0 }];
|
||||||
|
const hints = determineQueryHints(series);
|
||||||
|
expect(hints.length).toBe(1);
|
||||||
|
expect(hints[0]).toMatchObject({
|
||||||
|
label: 'Time series is monotonously increasing.',
|
||||||
|
index: 0,
|
||||||
|
fix: {
|
||||||
|
action: {
|
||||||
|
type: 'ADD_RATE',
|
||||||
|
query: 'metric',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a histogram hint for a bucket series', () => {
|
||||||
|
const series = [{ datapoints: [[23, 1000]], query: 'metric_bucket', responseIndex: 0 }];
|
||||||
|
const hints = determineQueryHints(series);
|
||||||
|
expect(hints.length).toBe(1);
|
||||||
|
expect(hints[0]).toMatchObject({
|
||||||
|
label: 'Time series has buckets, you probably wanted a histogram.',
|
||||||
|
index: 0,
|
||||||
|
fix: {
|
||||||
|
action: {
|
||||||
|
type: 'ADD_HISTOGRAM_QUANTILE',
|
||||||
|
query: 'metric_bucket',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Prometheus regular escaping', () => {
|
describe('Prometheus regular escaping', () => {
|
||||||
it('should not escape non-string', () => {
|
it('should not escape non-string', () => {
|
||||||
expect(prometheusRegularEscape(12)).toEqual(12);
|
expect(prometheusRegularEscape(12)).toEqual(12);
|
||||||
|
@ -111,7 +111,6 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it('should convert cumulative histogram to regular', () => {
|
it('should convert cumulative histogram to regular', () => {
|
||||||
let result = [];
|
|
||||||
let options = {
|
let options = {
|
||||||
format: 'heatmap',
|
format: 'heatmap',
|
||||||
start: 1445000010,
|
start: 1445000010,
|
||||||
@ -119,7 +118,7 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
legendFormat: '{{le}}',
|
legendFormat: '{{le}}',
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.resultTransformer.transform(result, { data: response }, options);
|
const result = ctx.resultTransformer.transform({ data: response }, options);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ target: '1', datapoints: [[10, 1445000010000], [10, 1445000020000], [0, 1445000030000]] },
|
{ target: '1', datapoints: [[10, 1445000010000], [10, 1445000020000], [0, 1445000030000]] },
|
||||||
{ target: '2', datapoints: [[10, 1445000010000], [0, 1445000020000], [30, 1445000030000]] },
|
{ target: '2', datapoints: [[10, 1445000010000], [0, 1445000020000], [30, 1445000030000]] },
|
||||||
@ -172,14 +171,13 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let result = [];
|
|
||||||
let options = {
|
let options = {
|
||||||
format: 'timeseries',
|
format: 'timeseries',
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 2,
|
end: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.resultTransformer.transform(result, { data: response }, options);
|
const result = ctx.resultTransformer.transform({ data: response }, options);
|
||||||
expect(result).toEqual([{ target: 'test{job="testjob"}', datapoints: [[10, 0], [10, 1000], [0, 2000]] }]);
|
expect(result).toEqual([{ target: 'test{job="testjob"}', datapoints: [[10, 0], [10, 1000], [0, 2000]] }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -196,7 +194,6 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let result = [];
|
|
||||||
let options = {
|
let options = {
|
||||||
format: 'timeseries',
|
format: 'timeseries',
|
||||||
step: 1,
|
step: 1,
|
||||||
@ -204,7 +201,7 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
end: 2,
|
end: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.resultTransformer.transform(result, { data: response }, options);
|
const result = ctx.resultTransformer.transform({ data: response }, options);
|
||||||
expect(result).toEqual([{ target: 'test{job="testjob"}', datapoints: [[null, 0], [10, 1000], [0, 2000]] }]);
|
expect(result).toEqual([{ target: 'test{job="testjob"}', datapoints: [[null, 0], [10, 1000], [0, 2000]] }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -221,7 +218,6 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let result = [];
|
|
||||||
let options = {
|
let options = {
|
||||||
format: 'timeseries',
|
format: 'timeseries',
|
||||||
step: 2,
|
step: 2,
|
||||||
@ -229,7 +225,7 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
end: 8,
|
end: 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.resultTransformer.transform(result, { data: response }, options);
|
const result = ctx.resultTransformer.transform({ data: response }, options);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ target: 'test{job="testjob"}', datapoints: [[null, 0], [null, 2000], [10, 4000], [null, 6000], [10, 8000]] },
|
{ target: 'test{job="testjob"}', datapoints: [[null, 0], [null, 2000], [10, 4000], [null, 6000], [10, 8000]] },
|
||||||
]);
|
]);
|
||||||
|
@ -158,4 +158,12 @@
|
|||||||
.prom-query-field {
|
.prom-query-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prom-query-field-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prom-query-field-info {
|
||||||
|
margin: 0.25em 0.5em 0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user