mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 08:05:43 -06:00
Prometheus: Run Explore range queries trough backend (#39133)
* Run Explore range queries trough backend * Remove trailing comma * Add timeRange step alignment to backend * Remove creation of instant query on backend as it is not supported ATM * Remove non-related frontend changes * Pass offset to calculate aligned range trough prom query * Update order in query error message * tableRefIds shouldn't contain undefined refIds * Remove cloning of dataframes when processing * Don't mutate response * Remove ordering of processed frames * Remove df because not needed
This commit is contained in:
parent
0606618d4a
commit
81756cd702
@ -51,6 +51,7 @@ type QueryModel struct {
|
|||||||
RangeQuery bool `json:"range"`
|
RangeQuery bool `json:"range"`
|
||||||
InstantQuery bool `json:"instant"`
|
InstantQuery bool `json:"instant"`
|
||||||
IntervalFactor int64 `json:"intervalFactor"`
|
IntervalFactor int64 `json:"intervalFactor"`
|
||||||
|
OffsetSec int64 `json:"offsetSec"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@ -162,13 +163,20 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
|||||||
span.SetTag("stop_unixnano", query.End.UnixNano())
|
span.SetTag("stop_unixnano", query.End.UnixNano())
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
value, _, err := client.QueryRange(ctx, query.Expr, timeRange)
|
var results model.Value
|
||||||
|
|
||||||
|
switch query.QueryType {
|
||||||
|
case Range:
|
||||||
|
results, _, err = client.QueryRange(ctx, query.Expr, timeRange)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &result, err
|
return &result, fmt.Errorf("query: %s failed with: %v", query.Expr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
frame, err := parseResponse(value, query)
|
default:
|
||||||
|
return &result, fmt.Errorf("unknown Query type detected %#v", query.QueryType)
|
||||||
|
}
|
||||||
|
|
||||||
|
frame, err := parseResponse(results, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &result, err
|
return &result, err
|
||||||
}
|
}
|
||||||
@ -283,13 +291,20 @@ func (s *Service) parseQuery(queryContext *backend.QueryDataRequest, dsInfo *Dat
|
|||||||
expr = strings.ReplaceAll(expr, "$__range", strconv.FormatInt(rangeS, 10)+"s")
|
expr = strings.ReplaceAll(expr, "$__range", strconv.FormatInt(rangeS, 10)+"s")
|
||||||
expr = strings.ReplaceAll(expr, "$__rate_interval", intervalv2.FormatDuration(calculateRateInterval(interval, dsInfo.TimeInterval, s.intervalCalculator)))
|
expr = strings.ReplaceAll(expr, "$__rate_interval", intervalv2.FormatDuration(calculateRateInterval(interval, dsInfo.TimeInterval, s.intervalCalculator)))
|
||||||
|
|
||||||
|
queryType := Range
|
||||||
|
|
||||||
|
// Align query range to step. It rounds start and end down to a multiple of step.
|
||||||
|
start := int64(math.Floor((float64(query.TimeRange.From.Unix()+model.OffsetSec)/interval.Seconds()))*interval.Seconds() - float64(model.OffsetSec))
|
||||||
|
end := int64(math.Floor((float64(query.TimeRange.To.Unix()+model.OffsetSec)/interval.Seconds()))*interval.Seconds() - float64(model.OffsetSec))
|
||||||
|
|
||||||
qs = append(qs, &PrometheusQuery{
|
qs = append(qs, &PrometheusQuery{
|
||||||
Expr: expr,
|
Expr: expr,
|
||||||
Step: interval,
|
Step: interval,
|
||||||
LegendFormat: model.LegendFormat,
|
LegendFormat: model.LegendFormat,
|
||||||
Start: query.TimeRange.From,
|
Start: time.Unix(start, 0),
|
||||||
End: query.TimeRange.To,
|
End: time.Unix(end, 0),
|
||||||
RefId: query.RefID,
|
RefId: query.RefID,
|
||||||
|
QueryType: queryType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,6 +275,45 @@ func TestPrometheus_parseQuery(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "rate(ALERTS{job=\"test\" [1m]})", models[0].Expr)
|
require.Equal(t, "rate(ALERTS{job=\"test\" [1m]})", models[0].Expr)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("parsing query model of range query", func(t *testing.T) {
|
||||||
|
timeRange := backend.TimeRange{
|
||||||
|
From: now,
|
||||||
|
To: now.Add(48 * time.Hour),
|
||||||
|
}
|
||||||
|
|
||||||
|
query := queryContext(`{
|
||||||
|
"expr": "go_goroutines",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A",
|
||||||
|
"range": true
|
||||||
|
}`, timeRange)
|
||||||
|
|
||||||
|
dsInfo := &DatasourceInfo{}
|
||||||
|
models, err := service.parseQuery(query, dsInfo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, Range, models[0].QueryType)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parsing query model of with default query type", func(t *testing.T) {
|
||||||
|
timeRange := backend.TimeRange{
|
||||||
|
From: now,
|
||||||
|
To: now.Add(48 * time.Hour),
|
||||||
|
}
|
||||||
|
|
||||||
|
query := queryContext(`{
|
||||||
|
"expr": "go_goroutines",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}`, timeRange)
|
||||||
|
|
||||||
|
dsInfo := &DatasourceInfo{}
|
||||||
|
models, err := service.parseQuery(query, dsInfo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, Range, models[0].QueryType)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryContext(json string, timeRange backend.TimeRange) *backend.QueryDataRequest {
|
func queryContext(json string, timeRange backend.TimeRange) *backend.QueryDataRequest {
|
||||||
|
@ -9,4 +9,13 @@ type PrometheusQuery struct {
|
|||||||
Start time.Time
|
Start time.Time
|
||||||
End time.Time
|
End time.Time
|
||||||
RefId string
|
RefId string
|
||||||
|
QueryType PrometheusQueryType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PrometheusQueryType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Range PrometheusQueryType = "range"
|
||||||
|
//This is currently not used, but we will use it in next iteration
|
||||||
|
Instant PrometheusQueryType = "instant"
|
||||||
|
)
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
DataQueryError,
|
DataQueryError,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
DataSourceApi,
|
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
dateMath,
|
dateMath,
|
||||||
DateTime,
|
DateTime,
|
||||||
@ -17,7 +16,7 @@ import {
|
|||||||
ScopedVars,
|
ScopedVars,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { BackendSrvRequest, FetchError, FetchResponse, getBackendSrv } from '@grafana/runtime';
|
import { BackendSrvRequest, FetchError, FetchResponse, getBackendSrv, DataSourceWithBackend } from '@grafana/runtime';
|
||||||
|
|
||||||
import { safeStringifyValue } from 'app/core/utils/explore';
|
import { safeStringifyValue } from 'app/core/utils/explore';
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
@ -26,7 +25,7 @@ import addLabelToQuery from './add_label_to_query';
|
|||||||
import PrometheusLanguageProvider from './language_provider';
|
import PrometheusLanguageProvider from './language_provider';
|
||||||
import { expandRecordingRules } from './language_utils';
|
import { expandRecordingRules } from './language_utils';
|
||||||
import { getInitHints, getQueryHints } from './query_hints';
|
import { getInitHints, getQueryHints } from './query_hints';
|
||||||
import { getOriginalMetricName, renderTemplate, transform } from './result_transformer';
|
import { getOriginalMetricName, renderTemplate, transform, transformV2 } from './result_transformer';
|
||||||
import {
|
import {
|
||||||
ExemplarTraceIdDestination,
|
ExemplarTraceIdDestination,
|
||||||
isFetchErrorResponse,
|
isFetchErrorResponse,
|
||||||
@ -47,12 +46,13 @@ export const ANNOTATION_QUERY_STEP_DEFAULT = '60s';
|
|||||||
const EXEMPLARS_NOT_AVAILABLE = 'Exemplars for this query are not available.';
|
const EXEMPLARS_NOT_AVAILABLE = 'Exemplars for this query are not available.';
|
||||||
const GET_AND_POST_METADATA_ENDPOINTS = ['api/v1/query', 'api/v1/query_range', 'api/v1/series', 'api/v1/labels'];
|
const GET_AND_POST_METADATA_ENDPOINTS = ['api/v1/query', 'api/v1/query_range', 'api/v1/series', 'api/v1/labels'];
|
||||||
|
|
||||||
export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions> {
|
export class PrometheusDatasource extends DataSourceWithBackend<PromQuery, PromOptions> {
|
||||||
type: string;
|
type: string;
|
||||||
editorSrc: string;
|
editorSrc: string;
|
||||||
ruleMappings: { [index: string]: string };
|
ruleMappings: { [index: string]: string };
|
||||||
url: string;
|
url: string;
|
||||||
directUrl: string;
|
directUrl: string;
|
||||||
|
access: 'direct' | 'proxy';
|
||||||
basicAuth: any;
|
basicAuth: any;
|
||||||
withCredentials: any;
|
withCredentials: any;
|
||||||
metricsNameCache = new LRU<string, string[]>(10);
|
metricsNameCache = new LRU<string, string[]>(10);
|
||||||
@ -75,6 +75,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
|||||||
this.type = 'prometheus';
|
this.type = 'prometheus';
|
||||||
this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
|
this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
|
||||||
this.url = instanceSettings.url!;
|
this.url = instanceSettings.url!;
|
||||||
|
this.access = instanceSettings.access;
|
||||||
this.basicAuth = instanceSettings.basicAuth;
|
this.basicAuth = instanceSettings.basicAuth;
|
||||||
this.withCredentials = instanceSettings.withCredentials;
|
this.withCredentials = instanceSettings.withCredentials;
|
||||||
this.interval = instanceSettings.jsonData.timeInterval || '15s';
|
this.interval = instanceSettings.jsonData.timeInterval || '15s';
|
||||||
@ -288,7 +289,34 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
prepareOptionsV2 = (options: DataQueryRequest<PromQuery>) => {
|
||||||
|
const targets = options.targets.map((target) => {
|
||||||
|
// We want to format Explore + range queries as time_series
|
||||||
|
return {
|
||||||
|
...target,
|
||||||
|
instant: false,
|
||||||
|
range: true,
|
||||||
|
format: 'time_series',
|
||||||
|
offsetSec: this.timeSrv.timeRange().to.utcOffset() * 60,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...options, targets };
|
||||||
|
};
|
||||||
|
|
||||||
query(options: DataQueryRequest<PromQuery>): Observable<DataQueryResponse> {
|
query(options: DataQueryRequest<PromQuery>): Observable<DataQueryResponse> {
|
||||||
|
// WIP - currently we want to run trough backend only if all queries are explore + range queries
|
||||||
|
const shouldRunBackendQuery =
|
||||||
|
this.access === 'proxy' &&
|
||||||
|
options.app === CoreApp.Explore &&
|
||||||
|
!options.targets.some((query) => query.exemplar) &&
|
||||||
|
!options.targets.some((query) => query.instant);
|
||||||
|
|
||||||
|
if (shouldRunBackendQuery) {
|
||||||
|
const newOptions = this.prepareOptionsV2(options);
|
||||||
|
return super.query(newOptions).pipe(map((response) => transformV2(response, newOptions)));
|
||||||
|
// Run queries trough browser/proxy
|
||||||
|
} else {
|
||||||
const start = this.getPrometheusTime(options.range.from, false);
|
const start = this.getPrometheusTime(options.range.from, false);
|
||||||
const end = this.getPrometheusTime(options.range.to, true);
|
const end = this.getPrometheusTime(options.range.to, true);
|
||||||
const { queries, activeTargets } = this.prepareTargets(options, start, end);
|
const { queries, activeTargets } = this.prepareTargets(options, start, end);
|
||||||
@ -307,6 +335,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
|||||||
|
|
||||||
return this.panelsQuery(queries, activeTargets, end, options.requestId, options.scopedVars);
|
return this.panelsQuery(queries, activeTargets, end, options.requestId, options.scopedVars);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private exploreQuery(queries: PromQueryRequest[], activeTargets: PromQuery[], end: number) {
|
private exploreQuery(queries: PromQueryRequest[], activeTargets: PromQuery[], end: number) {
|
||||||
let runningQueriesCount = queries.length;
|
let runningQueriesCount = queries.length;
|
||||||
@ -824,6 +853,27 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
|||||||
getOriginalMetricName(labelData: { [key: string]: string }) {
|
getOriginalMetricName(labelData: { [key: string]: string }) {
|
||||||
return getOriginalMetricName(labelData);
|
return getOriginalMetricName(labelData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used when running queries trough backend
|
||||||
|
filterQuery(query: PromQuery): boolean {
|
||||||
|
if (query.hide || !query.expr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used when running queries trough backend
|
||||||
|
applyTemplateVariables(target: PromQuery, scopedVars: ScopedVars): Record<string, any> {
|
||||||
|
const variables = cloneDeep(scopedVars);
|
||||||
|
// We want to interpolate these variables on backend
|
||||||
|
delete variables.__interval;
|
||||||
|
delete variables.__interval_ms;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...target,
|
||||||
|
expr: this.templateSrv.replace(target.expr, variables, this.interpolateQueryExpr),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { DataFrame, FieldType } from '@grafana/data';
|
import { DataFrame, FieldType, DataQueryRequest, DataQueryResponse, MutableDataFrame } from '@grafana/data';
|
||||||
import { transform } from './result_transformer';
|
import { transform, transformV2, transformDFoTable } from './result_transformer';
|
||||||
|
import { PromQuery } from './types';
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
getTemplateSrv: () => ({
|
getTemplateSrv: () => ({
|
||||||
@ -31,6 +32,169 @@ const matrixResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('Prometheus Result Transformer', () => {
|
describe('Prometheus Result Transformer', () => {
|
||||||
|
describe('transformV2', () => {
|
||||||
|
it('results with time_series format should be enriched with preferredVisualisationType', () => {
|
||||||
|
const options = ({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
format: 'time_series',
|
||||||
|
refId: 'A',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown) as DataQueryRequest<PromQuery>;
|
||||||
|
const response = ({
|
||||||
|
state: 'Done',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
fields: [],
|
||||||
|
length: 2,
|
||||||
|
name: 'ALERTS',
|
||||||
|
refId: 'A',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown) as DataQueryResponse;
|
||||||
|
const series = transformV2(response, options);
|
||||||
|
expect(series).toEqual({
|
||||||
|
data: [{ fields: [], length: 2, meta: { preferredVisualisationType: 'graph' }, name: 'ALERTS', refId: 'A' }],
|
||||||
|
state: 'Done',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('results with table format should be transformed to table dataFrames', () => {
|
||||||
|
const options = ({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
format: 'table',
|
||||||
|
refId: 'A',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown) as DataQueryRequest<PromQuery>;
|
||||||
|
const response = ({
|
||||||
|
state: 'Done',
|
||||||
|
data: [
|
||||||
|
new MutableDataFrame({
|
||||||
|
refId: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [6, 5, 4] },
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: [6, 5, 4],
|
||||||
|
labels: { label1: 'value1', label2: 'value2' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} as unknown) as DataQueryResponse;
|
||||||
|
const series = transformV2(response, options);
|
||||||
|
// expect(series.data[0]).toBe({});
|
||||||
|
expect(series.data[0].fields[0].name).toEqual('time');
|
||||||
|
expect(series.data[0].fields[1].name).toEqual('label1');
|
||||||
|
expect(series.data[0].fields[2].name).toEqual('label2');
|
||||||
|
expect(series.data[0].fields[3].name).toEqual('Value');
|
||||||
|
expect(series.data[0].meta?.preferredVisualisationType).toEqual('table');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('results with table and time_series format should be correctly transformed', () => {
|
||||||
|
const options = ({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
format: 'table',
|
||||||
|
refId: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: 'time_series',
|
||||||
|
refId: 'B',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown) as DataQueryRequest<PromQuery>;
|
||||||
|
const response = ({
|
||||||
|
state: 'Done',
|
||||||
|
data: [
|
||||||
|
new MutableDataFrame({
|
||||||
|
refId: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [6, 5, 4] },
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: [6, 5, 4],
|
||||||
|
labels: { label1: 'value1', label2: 'value2' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
new MutableDataFrame({
|
||||||
|
refId: 'B',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [6, 5, 4] },
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: [6, 5, 4],
|
||||||
|
labels: { label1: 'value1', label2: 'value2' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} as unknown) as DataQueryResponse;
|
||||||
|
const series = transformV2(response, options);
|
||||||
|
expect(series.data[0].fields.length).toEqual(2);
|
||||||
|
expect(series.data[0].meta?.preferredVisualisationType).toEqual('graph');
|
||||||
|
expect(series.data[1].fields.length).toEqual(4);
|
||||||
|
expect(series.data[1].meta?.preferredVisualisationType).toEqual('table');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('transformDFoTable', () => {
|
||||||
|
it('transforms dataFrame with response length 1 to table dataFrame', () => {
|
||||||
|
const df = new MutableDataFrame({
|
||||||
|
refId: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [6, 5, 4] },
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: [6, 5, 4],
|
||||||
|
labels: { label1: 'value1', label2: 'value2' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableDf = transformDFoTable(df, 1);
|
||||||
|
expect(tableDf.fields.length).toBe(4);
|
||||||
|
expect(tableDf.fields[0].name).toBe('time');
|
||||||
|
expect(tableDf.fields[1].name).toBe('label1');
|
||||||
|
expect(tableDf.fields[1].values.get(0)).toBe('value1');
|
||||||
|
expect(tableDf.fields[2].name).toBe('label2');
|
||||||
|
expect(tableDf.fields[2].values.get(0)).toBe('value2');
|
||||||
|
expect(tableDf.fields[3].name).toBe('Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('transforms dataFrame with response length 2 to table dataFrame', () => {
|
||||||
|
const df = new MutableDataFrame({
|
||||||
|
refId: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [6, 5, 4] },
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: [6, 5, 4],
|
||||||
|
labels: { label1: 'value1', label2: 'value2' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableDf = transformDFoTable(df, 3);
|
||||||
|
expect(tableDf.fields.length).toBe(4);
|
||||||
|
expect(tableDf.fields[0].name).toBe('time');
|
||||||
|
expect(tableDf.fields[1].name).toBe('label1');
|
||||||
|
expect(tableDf.fields[1].values.get(0)).toBe('value1');
|
||||||
|
expect(tableDf.fields[2].name).toBe('label2');
|
||||||
|
expect(tableDf.fields[2].values.get(0)).toBe('value2');
|
||||||
|
expect(tableDf.fields[3].name).toBe('Value #A');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('transform', () => {
|
||||||
const options: any = { target: {}, query: {} };
|
const options: any = { target: {}, query: {} };
|
||||||
describe('When nothing is returned', () => {
|
describe('When nothing is returned', () => {
|
||||||
it('should return empty array', () => {
|
it('should return empty array', () => {
|
||||||
@ -341,7 +505,10 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should fill null values', () => {
|
it('should fill null values', () => {
|
||||||
const result = transform({ data: matrixResponse } as any, { ...options, query: { step: 1, start: 0, end: 2 } });
|
const result = transform({ data: matrixResponse } as any, {
|
||||||
|
...options,
|
||||||
|
query: { step: 1, start: 0, end: 2 },
|
||||||
|
});
|
||||||
|
|
||||||
expect(result[0].fields[0].values.toArray()).toEqual([0, 1000, 2000]);
|
expect(result[0].fields[0].values.toArray()).toEqual([0, 1000, 2000]);
|
||||||
expect(result[0].fields[1].values.toArray()).toEqual([null, 10, 0]);
|
expect(result[0].fields[1].values.toArray()).toEqual([null, 10, 0]);
|
||||||
@ -453,7 +620,10 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it('should correctly parse values', () => {
|
it('should correctly parse values', () => {
|
||||||
const result: DataFrame[] = transform({ data: response } as any, { ...options, target: { format: 'table' } });
|
const result: DataFrame[] = transform({ data: response } as any, {
|
||||||
|
...options,
|
||||||
|
target: { format: 'table' },
|
||||||
|
});
|
||||||
expect(result[0].fields[1].values.toArray()).toEqual([Number.POSITIVE_INFINITY]);
|
expect(result[0].fields[1].values.toArray()).toEqual([Number.POSITIVE_INFINITY]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -478,7 +648,10 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
|
|
||||||
describe('When format is table', () => {
|
describe('When format is table', () => {
|
||||||
it('should correctly parse values', () => {
|
it('should correctly parse values', () => {
|
||||||
const result: DataFrame[] = transform({ data: response } as any, { ...options, target: { format: 'table' } });
|
const result: DataFrame[] = transform({ data: response } as any, {
|
||||||
|
...options,
|
||||||
|
target: { format: 'table' },
|
||||||
|
});
|
||||||
expect(result[0].fields[3].values.toArray()).toEqual([Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]);
|
expect(result[0].fields[3].values.toArray()).toEqual([Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -585,3 +758,4 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
@ -13,8 +13,12 @@ import {
|
|||||||
ScopedVars,
|
ScopedVars,
|
||||||
TIME_SERIES_TIME_FIELD_NAME,
|
TIME_SERIES_TIME_FIELD_NAME,
|
||||||
TIME_SERIES_VALUE_FIELD_NAME,
|
TIME_SERIES_VALUE_FIELD_NAME,
|
||||||
|
DataQueryResponse,
|
||||||
|
DataQueryRequest,
|
||||||
|
PreferredVisualisationType,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { FetchResponse, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime';
|
import { FetchResponse, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime';
|
||||||
|
import { partition } from 'lodash';
|
||||||
import { descending, deviation } from 'd3';
|
import { descending, deviation } from 'd3';
|
||||||
import {
|
import {
|
||||||
ExemplarTraceIdDestination,
|
ExemplarTraceIdDestination,
|
||||||
@ -37,6 +41,84 @@ interface TimeAndValue {
|
|||||||
[TIME_SERIES_VALUE_FIELD_NAME]: number;
|
[TIME_SERIES_VALUE_FIELD_NAME]: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// V2 result trasnformer used to transform query results from queries that were run trough prometheus backend
|
||||||
|
export function transformV2(response: DataQueryResponse, options: DataQueryRequest<PromQuery>) {
|
||||||
|
// Get refIds that have table format as we need to process those to table reuslts
|
||||||
|
const tableRefIds = options.targets.filter((target) => target.format === 'table').map((target) => target.refId);
|
||||||
|
const [tableResults, otherResults]: [DataFrame[], DataFrame[]] = partition(response.data, (dataFrame) =>
|
||||||
|
dataFrame.refId ? tableRefIds.includes(dataFrame.refId) : false
|
||||||
|
);
|
||||||
|
|
||||||
|
// For table results, we need to transform data frames to table data frames
|
||||||
|
const tableFrames = tableResults.map((dataFrame) => {
|
||||||
|
const df = transformDFoTable(dataFrame, options.targets.length);
|
||||||
|
return df;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Everything else is processed as time_series result and graph preferredVisualisationType
|
||||||
|
const otherFrames = otherResults.map((dataFrame) => {
|
||||||
|
const df = {
|
||||||
|
...dataFrame,
|
||||||
|
meta: {
|
||||||
|
...dataFrame.meta,
|
||||||
|
preferredVisualisationType: 'graph',
|
||||||
|
},
|
||||||
|
} as DataFrame;
|
||||||
|
return df;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...response, data: [...otherFrames, ...tableFrames] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformDFoTable(df: DataFrame, responseLength: number): DataFrame {
|
||||||
|
if (df.length === 0) {
|
||||||
|
return df;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeField = df.fields[0];
|
||||||
|
const valueField = df.fields[1];
|
||||||
|
|
||||||
|
// Create label fields
|
||||||
|
const promLabels: PromMetric = valueField.labels ?? {};
|
||||||
|
const labelFields = Object.keys(promLabels)
|
||||||
|
.sort()
|
||||||
|
.map((label) => {
|
||||||
|
const numberField = label === 'le';
|
||||||
|
return {
|
||||||
|
name: label,
|
||||||
|
config: { filterable: true },
|
||||||
|
type: numberField ? FieldType.number : FieldType.string,
|
||||||
|
values: new ArrayVector(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fill labelFields with label values
|
||||||
|
labelFields.forEach((field) => field.values.add(getLabelValue(promLabels, field.name)));
|
||||||
|
|
||||||
|
const tableDataFrame = {
|
||||||
|
...df,
|
||||||
|
name: undefined,
|
||||||
|
meta: { ...df.meta, preferredVisualisationType: 'table' as PreferredVisualisationType },
|
||||||
|
fields: [
|
||||||
|
timeField,
|
||||||
|
...labelFields,
|
||||||
|
{
|
||||||
|
...valueField,
|
||||||
|
name: getValueText(responseLength, df.refId),
|
||||||
|
labels: undefined,
|
||||||
|
config: { ...valueField.config, displayNameFromDS: undefined },
|
||||||
|
state: { ...valueField.state, displayName: undefined },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return tableDataFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValueText(responseLength: number, refId = '') {
|
||||||
|
return responseLength > 1 ? `Value #${refId}` : 'Value';
|
||||||
|
}
|
||||||
|
|
||||||
export function transform(
|
export function transform(
|
||||||
response: FetchResponse<PromDataSuccessResponse>,
|
response: FetchResponse<PromDataSuccessResponse>,
|
||||||
transformOptions: {
|
transformOptions: {
|
||||||
|
@ -10,6 +10,8 @@ export interface PromQuery extends DataQuery {
|
|||||||
hinting?: boolean;
|
hinting?: boolean;
|
||||||
interval?: string;
|
interval?: string;
|
||||||
intervalFactor?: number;
|
intervalFactor?: number;
|
||||||
|
// Timezone offset to align start & end time on backend
|
||||||
|
offsetSec?: number;
|
||||||
legendFormat?: string;
|
legendFormat?: string;
|
||||||
valueWithRefId?: boolean;
|
valueWithRefId?: boolean;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user