Commit c1b9bbc2 by David Committed by Alexander Zobnin

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
parent 817179c0
...@@ -19,6 +19,16 @@ import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; ...@@ -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) { ...@@ -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 { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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>
); );
......
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, {}> { ...@@ -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, {}> { ...@@ -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, {}> { ...@@ -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 ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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'; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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'; ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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 @@ ...@@ -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;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment