mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: Remove unsupported browser access mode related code (#77316)
* Remove unused code * More cleaning * Delete more * betterer * Small import fixes * Fix unit test * Fix unit test * uncomment * Remove PromLink component since it was removed from Prometheus data source * Clean the direct/browser access code * Remove directUrl * Big cleaning * unit test fixing * cleaning * fix metric_find_query tests * clean result_transformer tests * betterer * Remove unused type * betterer
This commit is contained in:
parent
164a412d3a
commit
73f25f7ccc
@ -6406,9 +6406,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/components/PromLink.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@ -6487,16 +6484,7 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/datasource/prometheus/datasource.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/datasource.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@ -6511,14 +6499,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "19"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/language_provider.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@ -6547,7 +6528,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/query_hints.ts:5381": [
|
||||
@ -6686,44 +6667,12 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/result_transformer.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "19"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "24"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "28"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "29"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "30"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "31"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/prometheus/types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/tempo/LokiSearch.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
|
@ -1,123 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { dateTime, PanelData, TimeRange } from '@grafana/data';
|
||||
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { PromQuery } from '../types';
|
||||
|
||||
import PromLink from './PromLink';
|
||||
|
||||
jest.mock('@grafana/data', () => ({
|
||||
...jest.requireActual('@grafana/data'),
|
||||
rangeUtil: {
|
||||
intervalToSeconds: jest.fn(() => 15),
|
||||
},
|
||||
}));
|
||||
|
||||
const now = dateTime().valueOf();
|
||||
const intervalInSeconds = 60 * 5;
|
||||
const endInput = encodeURIComponent(dateTime(now).add(5, 'hours').format('Y-MM-DD HH:mm'));
|
||||
|
||||
const getPanelData = (panelDataOverrides?: Partial<PanelData>) => {
|
||||
const panelData = {
|
||||
request: {
|
||||
scopedVars: [{ __interval: { text: '15s', value: '15s' } }],
|
||||
targets: [
|
||||
{ refId: 'A', datasource: 'prom1' },
|
||||
{ refId: 'B', datasource: 'prom2' },
|
||||
],
|
||||
range: {
|
||||
raw: {},
|
||||
to: dateTime(now), // "now"
|
||||
from: dateTime(now - 1000 * intervalInSeconds), // 5 minutes ago from "now"
|
||||
} as TimeRange,
|
||||
},
|
||||
};
|
||||
|
||||
return Object.assign(panelData, panelDataOverrides) as PanelData;
|
||||
};
|
||||
|
||||
const getDataSource = (datasourceOverrides?: Partial<PrometheusDatasource>) => {
|
||||
const datasource = {
|
||||
createQuery: () => ({ expr: 'up', step: 15 }),
|
||||
directUrl: 'prom1',
|
||||
getRateIntervalScopedVariable: jest.fn(() => ({ __rate_interval: { text: '60s', value: '60s' } })),
|
||||
};
|
||||
|
||||
return Object.assign(datasource, datasourceOverrides) as unknown as PrometheusDatasource;
|
||||
};
|
||||
|
||||
const getDataSourceWithCustomQueryParameters = (datasourceOverrides?: Partial<PrometheusDatasource>) => {
|
||||
const datasource = {
|
||||
getPrometheusTime: () => 1677870470,
|
||||
createQuery: () => ({ expr: 'up', step: 20 }),
|
||||
directUrl: 'prom3',
|
||||
getRateIntervalScopedVariable: jest.fn(() => ({ __rate_interval: { text: '60s', value: '60s' } })),
|
||||
customQueryParameters: new URLSearchParams('g0.foo=1'),
|
||||
};
|
||||
|
||||
return Object.assign(datasource, datasourceOverrides) as unknown as PrometheusDatasource;
|
||||
};
|
||||
|
||||
describe('PromLink', () => {
|
||||
it('should show correct link for 1 component', async () => {
|
||||
render(
|
||||
<div>
|
||||
<PromLink datasource={getDataSource()} panelData={getPanelData()} query={{} as PromQuery} />
|
||||
</div>
|
||||
);
|
||||
expect(screen.getByText('Prometheus')).toHaveAttribute(
|
||||
'href',
|
||||
`prom1/graph?g0.expr=up&g0.range_input=${intervalInSeconds}s&g0.end_input=${endInput}&g0.step_input=15&g0.tab=0`
|
||||
);
|
||||
});
|
||||
it('should show different link when there are 2 components with the same panel data', () => {
|
||||
render(
|
||||
<div>
|
||||
<PromLink datasource={getDataSource()} panelData={getPanelData()} query={{} as PromQuery} />
|
||||
<PromLink
|
||||
datasource={getDataSource({ directUrl: 'prom2' })}
|
||||
panelData={getPanelData()}
|
||||
query={{} as PromQuery}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const promLinkButtons = screen.getAllByText('Prometheus');
|
||||
expect(promLinkButtons[0]).toHaveAttribute(
|
||||
'href',
|
||||
`prom1/graph?g0.expr=up&g0.range_input=${intervalInSeconds}s&g0.end_input=${endInput}&g0.step_input=15&g0.tab=0`
|
||||
);
|
||||
expect(promLinkButtons[1]).toHaveAttribute(
|
||||
'href',
|
||||
`prom2/graph?g0.expr=up&g0.range_input=${intervalInSeconds}s&g0.end_input=${endInput}&g0.step_input=15&g0.tab=0`
|
||||
);
|
||||
});
|
||||
it('should create sanitized link', async () => {
|
||||
render(
|
||||
<div>
|
||||
<PromLink
|
||||
datasource={getDataSource({ directUrl: "javascript:300?1:2;alert('Hello');//" })}
|
||||
panelData={getPanelData()}
|
||||
query={{} as PromQuery}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
expect(screen.getByText('Prometheus')).toHaveAttribute('href', 'about:blank');
|
||||
});
|
||||
it('should add custom query parameters when it is configured', async () => {
|
||||
render(
|
||||
<div>
|
||||
<PromLink
|
||||
datasource={getDataSourceWithCustomQueryParameters()}
|
||||
panelData={getPanelData()}
|
||||
query={{} as PromQuery}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
expect(screen.getByText('Prometheus')).toHaveAttribute(
|
||||
'href',
|
||||
`prom3/graph?g0.foo=1&g0.expr=up&g0.range_input=${intervalInSeconds}s&g0.end_input=${endInput}&g0.step_input=20&g0.tab=0`
|
||||
);
|
||||
});
|
||||
});
|
@ -1,84 +0,0 @@
|
||||
import { map } from 'lodash';
|
||||
import React, { useEffect, useState, memo } from 'react';
|
||||
|
||||
import { DataQueryRequest, PanelData, ScopedVars, textUtil, rangeUtil } from '@grafana/data';
|
||||
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { getPrometheusTime } from '../language_utils';
|
||||
import { PromQuery } from '../types';
|
||||
|
||||
interface Props {
|
||||
datasource: PrometheusDatasource;
|
||||
query: PromQuery;
|
||||
panelData?: PanelData;
|
||||
}
|
||||
|
||||
const PromLink = ({ panelData, query, datasource }: Props) => {
|
||||
const [href, setHref] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (panelData) {
|
||||
const getExternalLink = () => {
|
||||
if (!panelData.request) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const {
|
||||
request: { range, interval, scopedVars },
|
||||
} = panelData;
|
||||
|
||||
const start = getPrometheusTime(range.from, false);
|
||||
const end = getPrometheusTime(range.to, true);
|
||||
const rangeDiff = Math.ceil(end - start);
|
||||
const endTime = range.to.utc().format('YYYY-MM-DD HH:mm');
|
||||
|
||||
const enrichedScopedVars: ScopedVars = {
|
||||
...scopedVars,
|
||||
// As we support $__rate_interval variable in min step, we need add it to scopedVars
|
||||
...datasource.getRateIntervalScopedVariable(
|
||||
rangeUtil.intervalToSeconds(interval),
|
||||
rangeUtil.intervalToSeconds(datasource.interval)
|
||||
),
|
||||
};
|
||||
|
||||
const options = {
|
||||
interval,
|
||||
scopedVars: enrichedScopedVars,
|
||||
} as DataQueryRequest<PromQuery>;
|
||||
|
||||
const customQueryParameters: { [key: string]: string } = {};
|
||||
if (datasource.customQueryParameters) {
|
||||
for (const [k, v] of datasource.customQueryParameters) {
|
||||
customQueryParameters[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
const queryOptions = datasource.createQuery(query, options, start, end);
|
||||
|
||||
const expr = {
|
||||
...customQueryParameters,
|
||||
'g0.expr': queryOptions.expr,
|
||||
'g0.range_input': rangeDiff + 's',
|
||||
'g0.end_input': endTime,
|
||||
'g0.step_input': queryOptions.step,
|
||||
'g0.tab': 0,
|
||||
};
|
||||
|
||||
const args = map(expr, (v: string, k: string) => {
|
||||
return k + '=' + encodeURIComponent(v);
|
||||
}).join('&');
|
||||
return `${datasource.directUrl}/graph?${args}`;
|
||||
};
|
||||
|
||||
setHref(getExternalLink());
|
||||
}
|
||||
}, [datasource, panelData, query]);
|
||||
|
||||
return (
|
||||
<a href={textUtil.sanitizeUrl(href)} target="_blank" rel="noopener noreferrer">
|
||||
Prometheus
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PromLink);
|
@ -9,7 +9,6 @@ export function createDefaultConfigOptions(): DataSourceSettings<PromOptions> {
|
||||
timeInterval: '1m',
|
||||
queryTimeout: '1m',
|
||||
httpMethod: 'GET',
|
||||
directUrl: 'url',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
import { cloneDeep, defaults } from 'lodash';
|
||||
import { forkJoin, lastValueFrom, merge, Observable, of, OperatorFunction, pipe, throwError } from 'rxjs';
|
||||
import { catchError, filter, map, tap } from 'rxjs/operators';
|
||||
import { lastValueFrom, Observable, throwError } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import semver from 'semver/preload';
|
||||
|
||||
import {
|
||||
@ -10,7 +10,6 @@ import {
|
||||
AnnotationQueryRequest,
|
||||
CoreApp,
|
||||
DataFrame,
|
||||
DataQueryError,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataSourceGetTagKeysOptions,
|
||||
@ -21,7 +20,6 @@ import {
|
||||
dateTime,
|
||||
getDefaultTimeRange,
|
||||
LegacyMetricFindQueryOptions,
|
||||
LoadingState,
|
||||
MetricFindValue,
|
||||
QueryFixAction,
|
||||
rangeUtil,
|
||||
@ -33,15 +31,13 @@ import {
|
||||
BackendDataSourceResponse,
|
||||
BackendSrvRequest,
|
||||
DataSourceWithBackend,
|
||||
FetchError,
|
||||
FetchResponse,
|
||||
getBackendSrv,
|
||||
isFetchError,
|
||||
toDataQueryResponse,
|
||||
getTemplateSrv,
|
||||
isFetchError,
|
||||
TemplateSrv,
|
||||
toDataQueryResponse,
|
||||
} from '@grafana/runtime';
|
||||
import { safeStringifyValue } from 'app/core/utils/explore';
|
||||
|
||||
import { addLabelToQuery } from './add_label_to_query';
|
||||
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
|
||||
@ -57,21 +53,15 @@ import { getInitHints, getQueryHints } from './query_hints';
|
||||
import { promQueryModeller } from './querybuilder/PromQueryModeller';
|
||||
import { QueryBuilderLabelFilter, QueryEditorMode } from './querybuilder/shared/types';
|
||||
import { CacheRequestInfo, defaultPrometheusQueryOverlapWindow, QueryCache } from './querycache/QueryCache';
|
||||
import { getOriginalMetricName, transform, transformV2 } from './result_transformer';
|
||||
import { getOriginalMetricName, transformV2 } from './result_transformer';
|
||||
import { trackQuery } from './tracking';
|
||||
import {
|
||||
ExemplarTraceIdDestination,
|
||||
PromApplication,
|
||||
PromDataErrorResponse,
|
||||
PromDataSuccessResponse,
|
||||
PrometheusCacheLevel,
|
||||
PromExemplarData,
|
||||
PromMatrixData,
|
||||
PromOptions,
|
||||
PromQuery,
|
||||
PromQueryRequest,
|
||||
PromScalarData,
|
||||
PromVectorData,
|
||||
} from './types';
|
||||
import { PrometheusVariableSupport } from './variables';
|
||||
|
||||
@ -89,7 +79,6 @@ export class PrometheusDatasource
|
||||
hasIncrementalQuery: boolean;
|
||||
url: string;
|
||||
id: number;
|
||||
directUrl: string;
|
||||
access: 'direct' | 'proxy';
|
||||
basicAuth: any;
|
||||
withCredentials: any;
|
||||
@ -124,9 +113,6 @@ export class PrometheusDatasource
|
||||
this.interval = instanceSettings.jsonData.timeInterval || '15s';
|
||||
this.queryTimeout = instanceSettings.jsonData.queryTimeout;
|
||||
this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET';
|
||||
// `directUrl` is never undefined, we set it at https://github.com/grafana/grafana/blob/main/pkg/api/frontendsettings.go#L108
|
||||
// here we "fall back" to this.url to make typescript happy, but it should never happen
|
||||
this.directUrl = instanceSettings.jsonData.directUrl ?? this.url;
|
||||
this.exemplarTraceIdDestinations = instanceSettings.jsonData.exemplarTraceIdDestinations;
|
||||
this.hasIncrementalQuery = instanceSettings.jsonData.incrementalQuerying ?? false;
|
||||
this.ruleMappings = {};
|
||||
@ -222,6 +208,15 @@ export class PrometheusDatasource
|
||||
}
|
||||
}
|
||||
|
||||
directAccessError() {
|
||||
return throwError(
|
||||
() =>
|
||||
new Error(
|
||||
'Browser access mode in the Prometheus datasource is no longer available. Switch to server access mode.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Any request done from this data source should go through here as it contains some common processing for the
|
||||
* request. Any processing done here needs to be also copied on the backend as this goes through data source proxy
|
||||
@ -233,10 +228,7 @@ export class PrometheusDatasource
|
||||
overrides: Partial<BackendSrvRequest> = {}
|
||||
): Observable<FetchResponse<T>> {
|
||||
if (this.access === 'direct') {
|
||||
const error = new Error(
|
||||
'Browser access mode in the Prometheus datasource is no longer available. Switch to server access mode.'
|
||||
);
|
||||
return throwError(() => error);
|
||||
return this.directAccessError();
|
||||
}
|
||||
|
||||
data = data || {};
|
||||
@ -346,86 +338,6 @@ export class PrometheusDatasource
|
||||
return this.templateSrv.containsTemplate(target.expr);
|
||||
}
|
||||
|
||||
prepareTargets = (options: DataQueryRequest<PromQuery>, start: number, end: number) => {
|
||||
const queries: PromQueryRequest[] = [];
|
||||
const activeTargets: PromQuery[] = [];
|
||||
const clonedTargets = cloneDeep(options.targets);
|
||||
|
||||
for (const target of clonedTargets) {
|
||||
if (!target.expr || target.hide) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const metricName = this.languageProvider.histogramMetrics.find((m) => target.expr.includes(m));
|
||||
|
||||
// In Explore, we run both (instant and range) queries if both are true (selected) or both are undefined (legacy Explore queries)
|
||||
if (options.app === CoreApp.Explore && target.range === target.instant) {
|
||||
// Create instant target
|
||||
const instantTarget: any = cloneDeep(target);
|
||||
instantTarget.format = 'table';
|
||||
instantTarget.instant = true;
|
||||
instantTarget.range = false;
|
||||
instantTarget.valueWithRefId = true;
|
||||
delete instantTarget.maxDataPoints;
|
||||
|
||||
// Create range target
|
||||
const rangeTarget = cloneDeep(target);
|
||||
rangeTarget.format = 'time_series';
|
||||
rangeTarget.instant = false;
|
||||
instantTarget.range = true;
|
||||
|
||||
// Create exemplar query
|
||||
if (target.exemplar) {
|
||||
// Only create exemplar target for different metric names
|
||||
if (
|
||||
!metricName ||
|
||||
(metricName && !activeTargets.some((activeTarget) => activeTarget.expr.includes(metricName)))
|
||||
) {
|
||||
const exemplarTarget = cloneDeep(target);
|
||||
exemplarTarget.instant = false;
|
||||
queries.push(this.createQuery(exemplarTarget, options, start, end));
|
||||
activeTargets.push(exemplarTarget);
|
||||
}
|
||||
instantTarget.exemplar = false;
|
||||
rangeTarget.exemplar = false;
|
||||
}
|
||||
|
||||
// Add both targets to activeTargets and queries arrays
|
||||
activeTargets.push(instantTarget, rangeTarget);
|
||||
queries.push(
|
||||
this.createQuery(instantTarget, options, start, end),
|
||||
this.createQuery(rangeTarget, options, start, end)
|
||||
);
|
||||
// If running only instant query in Explore, format as table
|
||||
} else if (target.instant && options.app === CoreApp.Explore) {
|
||||
const instantTarget = cloneDeep(target);
|
||||
instantTarget.format = 'table';
|
||||
queries.push(this.createQuery(instantTarget, options, start, end));
|
||||
activeTargets.push(instantTarget);
|
||||
} else {
|
||||
// It doesn't make sense to query for exemplars in dashboard if only instant is selected
|
||||
if (target.exemplar && !target.instant) {
|
||||
if (
|
||||
!metricName ||
|
||||
(metricName && !activeTargets.some((activeTarget) => activeTarget.expr.includes(metricName)))
|
||||
) {
|
||||
const exemplarTarget = cloneDeep(target);
|
||||
queries.push(this.createQuery(exemplarTarget, options, start, end));
|
||||
activeTargets.push(exemplarTarget);
|
||||
}
|
||||
target.exemplar = false;
|
||||
}
|
||||
queries.push(this.createQuery(target, options, start, end));
|
||||
activeTargets.push(target);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
queries,
|
||||
activeTargets,
|
||||
};
|
||||
};
|
||||
|
||||
shouldRunExemplarQuery(target: PromQuery, request: DataQueryRequest<PromQuery>): boolean {
|
||||
if (target.exemplar) {
|
||||
// We check all already processed targets and only create exemplar target for not used metric names
|
||||
@ -474,151 +386,40 @@ export class PrometheusDatasource
|
||||
}
|
||||
|
||||
query(request: DataQueryRequest<PromQuery>): Observable<DataQueryResponse> {
|
||||
if (this.access === 'proxy') {
|
||||
let fullOrPartialRequest: DataQueryRequest<PromQuery>;
|
||||
let requestInfo: CacheRequestInfo<PromQuery> | undefined = undefined;
|
||||
const hasInstantQuery = request.targets.some((target) => target.instant);
|
||||
|
||||
// Don't cache instant queries
|
||||
if (this.hasIncrementalQuery && !hasInstantQuery) {
|
||||
requestInfo = this.cache.requestInfo(request);
|
||||
fullOrPartialRequest = requestInfo.requests[0];
|
||||
} else {
|
||||
fullOrPartialRequest = request;
|
||||
}
|
||||
|
||||
const targets = fullOrPartialRequest.targets.map((target) => this.processTargetV2(target, fullOrPartialRequest));
|
||||
const startTime = new Date();
|
||||
return super.query({ ...fullOrPartialRequest, targets: targets.flat() }).pipe(
|
||||
map((response) => {
|
||||
const amendedResponse = {
|
||||
...response,
|
||||
data: this.cache.procFrames(request, requestInfo, response.data),
|
||||
};
|
||||
return transformV2(amendedResponse, request, {
|
||||
exemplarTraceIdDestinations: this.exemplarTraceIdDestinations,
|
||||
});
|
||||
}),
|
||||
tap((response: DataQueryResponse) => {
|
||||
trackQuery(response, request, startTime);
|
||||
})
|
||||
);
|
||||
// Run queries through browser/proxy
|
||||
} else {
|
||||
const start = getPrometheusTime(request.range.from, false);
|
||||
const end = getPrometheusTime(request.range.to, true);
|
||||
const { queries, activeTargets } = this.prepareTargets(request, start, end);
|
||||
|
||||
// No valid targets, return the empty result to save a round trip.
|
||||
if (!queries || !queries.length) {
|
||||
return of({
|
||||
data: [],
|
||||
state: LoadingState.Done,
|
||||
});
|
||||
}
|
||||
|
||||
if (request.app === CoreApp.Explore) {
|
||||
return this.exploreQuery(queries, activeTargets, end);
|
||||
}
|
||||
|
||||
return this.panelsQuery(queries, activeTargets, end, request.requestId, request.scopedVars);
|
||||
if (this.access === 'direct') {
|
||||
return this.directAccessError();
|
||||
}
|
||||
}
|
||||
|
||||
private exploreQuery(queries: PromQueryRequest[], activeTargets: PromQuery[], end: number) {
|
||||
let runningQueriesCount = queries.length;
|
||||
let fullOrPartialRequest: DataQueryRequest<PromQuery>;
|
||||
let requestInfo: CacheRequestInfo<PromQuery> | undefined = undefined;
|
||||
const hasInstantQuery = request.targets.some((target) => target.instant);
|
||||
|
||||
const subQueries = queries.map((query, index) => {
|
||||
const target = activeTargets[index];
|
||||
// Don't cache instant queries
|
||||
if (this.hasIncrementalQuery && !hasInstantQuery) {
|
||||
requestInfo = this.cache.requestInfo(request);
|
||||
fullOrPartialRequest = requestInfo.requests[0];
|
||||
} else {
|
||||
fullOrPartialRequest = request;
|
||||
}
|
||||
|
||||
const filterAndMapResponse = pipe(
|
||||
// Decrease the counter here. We assume that each request returns only single value and then completes
|
||||
// (should hold until there is some streaming requests involved).
|
||||
tap(() => runningQueriesCount--),
|
||||
filter((response: any) => (response.cancelled ? false : true)),
|
||||
map((response) => {
|
||||
const data = transform(response, {
|
||||
query,
|
||||
target,
|
||||
responseListLength: queries.length,
|
||||
exemplarTraceIdDestinations: this.exemplarTraceIdDestinations,
|
||||
});
|
||||
const result: DataQueryResponse = {
|
||||
data,
|
||||
key: query.requestId,
|
||||
state: runningQueriesCount === 0 ? LoadingState.Done : LoadingState.Loading,
|
||||
};
|
||||
return result;
|
||||
})
|
||||
);
|
||||
|
||||
return this.runQuery(query, end, filterAndMapResponse);
|
||||
});
|
||||
|
||||
return merge(...subQueries);
|
||||
}
|
||||
|
||||
private panelsQuery(
|
||||
queries: PromQueryRequest[],
|
||||
activeTargets: PromQuery[],
|
||||
end: number,
|
||||
requestId: string,
|
||||
scopedVars: ScopedVars
|
||||
) {
|
||||
const observables = queries.map((query, index) => {
|
||||
const target = activeTargets[index];
|
||||
|
||||
const filterAndMapResponse = pipe(
|
||||
filter((response: any) => (response.cancelled ? false : true)),
|
||||
map((response) => {
|
||||
const data = transform(response, {
|
||||
query,
|
||||
target,
|
||||
responseListLength: queries.length,
|
||||
scopedVars,
|
||||
exemplarTraceIdDestinations: this.exemplarTraceIdDestinations,
|
||||
});
|
||||
return data;
|
||||
})
|
||||
);
|
||||
|
||||
return this.runQuery(query, end, filterAndMapResponse);
|
||||
});
|
||||
|
||||
return forkJoin(observables).pipe(
|
||||
map((results) => {
|
||||
const data = results.reduce((result, current) => {
|
||||
return [...result, ...current];
|
||||
}, []);
|
||||
return {
|
||||
data,
|
||||
key: requestId,
|
||||
state: LoadingState.Done,
|
||||
const targets = fullOrPartialRequest.targets.map((target) => this.processTargetV2(target, fullOrPartialRequest));
|
||||
const startTime = new Date();
|
||||
return super.query({ ...fullOrPartialRequest, targets: targets.flat() }).pipe(
|
||||
map((response) => {
|
||||
const amendedResponse = {
|
||||
...response,
|
||||
data: this.cache.procFrames(request, requestInfo, response.data),
|
||||
};
|
||||
return transformV2(amendedResponse, request, {
|
||||
exemplarTraceIdDestinations: this.exemplarTraceIdDestinations,
|
||||
});
|
||||
}),
|
||||
tap((response: DataQueryResponse) => {
|
||||
trackQuery(response, request, startTime);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private runQuery<T>(query: PromQueryRequest, end: number, filter: OperatorFunction<any, T>): Observable<T> {
|
||||
if (query.instant) {
|
||||
return this.performInstantQuery(query, end).pipe(filter);
|
||||
}
|
||||
|
||||
if (query.exemplar) {
|
||||
return this.getExemplars(query).pipe(
|
||||
catchError(() => {
|
||||
return of({
|
||||
data: [],
|
||||
state: LoadingState.Done,
|
||||
});
|
||||
}),
|
||||
filter
|
||||
);
|
||||
}
|
||||
|
||||
return this.performTimeSeriesQuery(query, query.start, query.end).pipe(filter);
|
||||
}
|
||||
|
||||
createQuery(target: PromQuery, options: DataQueryRequest<PromQuery>, start: number, end: number) {
|
||||
const query: PromQueryRequest = {
|
||||
hinting: target.hinting,
|
||||
@ -704,93 +505,6 @@ export class PrometheusDatasource
|
||||
return Math.max(interval * intervalFactor, minInterval, safeInterval);
|
||||
}
|
||||
|
||||
performTimeSeriesQuery(query: PromQueryRequest, start: number, end: number) {
|
||||
if (start > end) {
|
||||
throw { message: 'Invalid time range' };
|
||||
}
|
||||
|
||||
const url = '/api/v1/query_range';
|
||||
const data: any = {
|
||||
query: query.expr,
|
||||
start,
|
||||
end,
|
||||
step: query.step,
|
||||
};
|
||||
|
||||
if (this.queryTimeout) {
|
||||
data['timeout'] = this.queryTimeout;
|
||||
}
|
||||
|
||||
return this._request<PromDataSuccessResponse<PromMatrixData>>(url, data, {
|
||||
requestId: query.requestId,
|
||||
headers: query.headers,
|
||||
}).pipe(
|
||||
catchError((err: FetchError<PromDataErrorResponse<PromMatrixData>>) => {
|
||||
if (err.cancelled) {
|
||||
return of(err);
|
||||
}
|
||||
|
||||
return throwError(this.handleErrors(err, query));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
performInstantQuery(
|
||||
query: PromQueryRequest,
|
||||
time: number
|
||||
): Observable<FetchResponse<PromDataSuccessResponse<PromVectorData | PromScalarData>> | FetchError> {
|
||||
const url = '/api/v1/query';
|
||||
const data: any = {
|
||||
query: query.expr,
|
||||
time,
|
||||
};
|
||||
|
||||
if (this.queryTimeout) {
|
||||
data['timeout'] = this.queryTimeout;
|
||||
}
|
||||
|
||||
return this._request<PromDataSuccessResponse<PromVectorData | PromScalarData>>(
|
||||
`/api/datasources/uid/${this.uid}/resources${url}`,
|
||||
data,
|
||||
{
|
||||
requestId: query.requestId,
|
||||
headers: query.headers,
|
||||
}
|
||||
).pipe(
|
||||
catchError((err: FetchError<PromDataErrorResponse<PromVectorData | PromScalarData>>) => {
|
||||
if (err.cancelled) {
|
||||
return of(err);
|
||||
}
|
||||
|
||||
return throwError(this.handleErrors(err, query));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
handleErrors = (err: any, target: PromQuery) => {
|
||||
const error: DataQueryError = {
|
||||
message: (err && err.statusText) || 'Unknown error during query transaction. Please check JS console logs.',
|
||||
refId: target.refId,
|
||||
};
|
||||
|
||||
if (err.data) {
|
||||
if (typeof err.data === 'string') {
|
||||
error.message = err.data;
|
||||
} else if (err.data.error) {
|
||||
error.message = safeStringifyValue(err.data.error);
|
||||
}
|
||||
} else if (err.message) {
|
||||
error.message = err.message;
|
||||
} else if (typeof err === 'string') {
|
||||
error.message = err;
|
||||
}
|
||||
|
||||
error.status = err.status;
|
||||
error.statusText = err.statusText;
|
||||
|
||||
return error;
|
||||
};
|
||||
|
||||
metricFindQuery(query: string, options?: LegacyMetricFindQueryOptions) {
|
||||
if (!query) {
|
||||
return Promise.resolve([]);
|
||||
@ -950,15 +664,6 @@ export class PrometheusDatasource
|
||||
return eventList;
|
||||
};
|
||||
|
||||
getExemplars(query: PromQueryRequest) {
|
||||
const url = '/api/v1/query_exemplars';
|
||||
return this._request<PromDataSuccessResponse<PromExemplarData>>(
|
||||
url,
|
||||
{ query: query.expr, start: query.start.toString(), end: query.end.toString() },
|
||||
{ requestId: query.requestId, headers: query.headers }
|
||||
);
|
||||
}
|
||||
|
||||
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
|
||||
// this is used to get label keys, a.k.a label names
|
||||
// it is used in metric_find_query.ts
|
||||
|
@ -20,7 +20,6 @@ const instanceSettings = {
|
||||
url: 'proxied',
|
||||
id: 1,
|
||||
uid: 'ABCDEF',
|
||||
directUrl: 'direct',
|
||||
user: 'test',
|
||||
password: 'mupp',
|
||||
jsonData: { httpMethod: 'GET' },
|
||||
@ -226,9 +225,10 @@ describe('PrometheusMetricFindQuery', () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith({
|
||||
method: 'GET',
|
||||
url: `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=metric&time=${raw.to.unix()}`,
|
||||
requestId: undefined,
|
||||
url: `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=metric`,
|
||||
headers: {},
|
||||
hideFromInspector: true,
|
||||
showErrorAlert: false,
|
||||
});
|
||||
});
|
||||
|
||||
@ -248,9 +248,10 @@ describe('PrometheusMetricFindQuery', () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith({
|
||||
method: 'GET',
|
||||
url: `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=1%2B1&time=${raw.to.unix()}`,
|
||||
requestId: undefined,
|
||||
url: `/api/datasources/uid/ABCDEF/resources/api/v1/query?query=1%2B1`,
|
||||
headers: {},
|
||||
hideFromInspector: true,
|
||||
showErrorAlert: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { chain, map as _map, uniq } from 'lodash';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { getDefaultTimeRange, MetricFindValue, TimeRange } from '@grafana/data';
|
||||
|
||||
@ -12,7 +10,6 @@ import {
|
||||
PrometheusMetricNamesRegex,
|
||||
PrometheusQueryResultRegex,
|
||||
} from './migrations/variableMigration';
|
||||
import { PromQueryRequest } from './types';
|
||||
|
||||
export default class PrometheusMetricFindQuery {
|
||||
range: TimeRange;
|
||||
@ -65,7 +62,7 @@ export default class PrometheusMetricFindQuery {
|
||||
|
||||
const queryResultQuery = this.query.match(queryResultRegex);
|
||||
if (queryResultQuery) {
|
||||
return lastValueFrom(this.queryResultQuery(queryResultQuery[1]));
|
||||
return this.queryResultQuery(queryResultQuery[1]);
|
||||
}
|
||||
|
||||
// if query contains full metric name, return metric name and label list
|
||||
@ -136,41 +133,41 @@ export default class PrometheusMetricFindQuery {
|
||||
}
|
||||
|
||||
queryResultQuery(query: string) {
|
||||
const end = getPrometheusTime(this.range.to, true);
|
||||
const instantQuery: PromQueryRequest = { expr: query } as PromQueryRequest;
|
||||
return this.datasource.performInstantQuery(instantQuery, end).pipe(
|
||||
map((result) => {
|
||||
switch (result.data.data.resultType) {
|
||||
case 'scalar': // [ <unix_time>, "<scalar_value>" ]
|
||||
case 'string': // [ <unix_time>, "<string_value>" ]
|
||||
return [
|
||||
{
|
||||
text: result.data.data.result[1] || '',
|
||||
expandable: false,
|
||||
},
|
||||
];
|
||||
case 'vector':
|
||||
return _map(result.data.data.result, (metricData) => {
|
||||
let text = metricData.metric.__name__ || '';
|
||||
delete metricData.metric.__name__;
|
||||
text +=
|
||||
'{' +
|
||||
_map(metricData.metric, (v, k) => {
|
||||
return k + '="' + v + '"';
|
||||
}).join(',') +
|
||||
'}';
|
||||
text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000;
|
||||
const url = '/api/v1/query';
|
||||
const params = {
|
||||
query,
|
||||
};
|
||||
return this.datasource.metadataRequest(url, params).then((result: any) => {
|
||||
switch (result.data.data.resultType) {
|
||||
case 'scalar': // [ <unix_time>, "<scalar_value>" ]
|
||||
case 'string': // [ <unix_time>, "<string_value>" ]
|
||||
return [
|
||||
{
|
||||
text: result.data.data.result[1] || '',
|
||||
expandable: false,
|
||||
},
|
||||
];
|
||||
case 'vector':
|
||||
return _map(result.data.data.result, (metricData) => {
|
||||
let text = metricData.metric.__name__ || '';
|
||||
delete metricData.metric.__name__;
|
||||
text +=
|
||||
'{' +
|
||||
_map(metricData.metric, (v, k) => {
|
||||
return k + '="' + v + '"';
|
||||
}).join(',') +
|
||||
'}';
|
||||
text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000;
|
||||
|
||||
return {
|
||||
text: text,
|
||||
expandable: true,
|
||||
};
|
||||
});
|
||||
default:
|
||||
throw Error(`Unknown/Unhandled result type: [${result.data.data.resultType}]`);
|
||||
}
|
||||
})
|
||||
);
|
||||
return {
|
||||
text: text,
|
||||
expandable: true,
|
||||
};
|
||||
});
|
||||
default:
|
||||
throw Error(`Unknown/Unhandled result type: [${result.data.data.resultType}]`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
metricNameAndLabelsQuery(query: string): Promise<MetricFindValue[]> {
|
||||
|
@ -138,13 +138,6 @@ export function getQueryHints(query: string, series?: any[], datasource?: Promet
|
||||
|
||||
export function getInitHints(datasource: PrometheusDatasource): QueryHint[] {
|
||||
const hints = [];
|
||||
// Hint if using Loki as Prometheus data source
|
||||
if (datasource.directUrl.includes('/loki') && !datasource.languageProvider.metrics.length) {
|
||||
hints.push({
|
||||
label: `Using Loki as a Prometheus data source is no longer supported. You must use the Loki data source for your Loki instance.`,
|
||||
type: 'INFO',
|
||||
});
|
||||
}
|
||||
|
||||
// Hint for big disabled lookups
|
||||
if (datasource.lookupsDisabled) {
|
||||
|
@ -17,7 +17,6 @@ import {
|
||||
const instanceSettings = {
|
||||
url: 'proxied',
|
||||
id: 1,
|
||||
directUrl: 'direct',
|
||||
user: 'test',
|
||||
password: 'mupp',
|
||||
jsonData: { httpMethod: 'GET' },
|
||||
|
@ -1,14 +1,13 @@
|
||||
import {
|
||||
cacheFieldDisplayNames,
|
||||
createDataFrame,
|
||||
DataFrame,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
FieldType,
|
||||
PreferredVisualisationType,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { parseSampleValue, sortSeriesByLabel, transform, transformDFToTable, transformV2 } from './result_transformer';
|
||||
import { parseSampleValue, sortSeriesByLabel, transformDFToTable, transformV2 } from './result_transformer';
|
||||
import { PromQuery } from './types';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
@ -30,22 +29,6 @@ jest.mock('@grafana/runtime', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const matrixResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'matrix',
|
||||
result: [
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob' },
|
||||
values: [
|
||||
[1, '10'],
|
||||
[2, '0'],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('Prometheus Result Transformer', () => {
|
||||
describe('parse variants of "+Inf" and "-Inf" strings', () => {
|
||||
it('+Inf', () => {
|
||||
@ -1088,580 +1071,4 @@ describe('Prometheus Result Transformer', () => {
|
||||
expect(transformedTableDataFrames[1].meta?.executedQueryString).toEqual(executedQueryForRefB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transform', () => {
|
||||
const options: any = { target: {}, query: {} };
|
||||
describe('When nothing is returned', () => {
|
||||
it('should return empty array', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: '',
|
||||
result: null,
|
||||
},
|
||||
};
|
||||
const series = transform({ data: response } as any, options);
|
||||
expect(series).toEqual([]);
|
||||
});
|
||||
it('should return empty array', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: '',
|
||||
result: null,
|
||||
},
|
||||
};
|
||||
const result = transform({ data: response } as any, { ...options, target: { format: 'table' } });
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When resultFormat is table', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'matrix',
|
||||
result: [
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob' },
|
||||
values: [
|
||||
[1443454528, '3846'],
|
||||
[1443454530, '3848'],
|
||||
],
|
||||
},
|
||||
{
|
||||
metric: {
|
||||
__name__: 'test2',
|
||||
instance: 'localhost:8080',
|
||||
job: 'otherjob',
|
||||
},
|
||||
values: [
|
||||
[1443454529, '3847'],
|
||||
[1443454531, '3849'],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it('should return data frame', () => {
|
||||
const result = transform({ data: response } as any, {
|
||||
...options,
|
||||
target: {
|
||||
responseListLength: 0,
|
||||
refId: 'A',
|
||||
format: 'table',
|
||||
},
|
||||
});
|
||||
expect(result[0].fields[0].values).toEqual([1443454528000, 1443454530000, 1443454529000, 1443454531000]);
|
||||
expect(result[0].fields[0].name).toBe('Time');
|
||||
expect(result[0].fields[0].type).toBe(FieldType.time);
|
||||
expect(result[0].fields[1].values).toEqual(['test', 'test', 'test2', 'test2']);
|
||||
expect(result[0].fields[1].name).toBe('__name__');
|
||||
expect(result[0].fields[1].config.filterable).toBe(true);
|
||||
expect(result[0].fields[1].type).toBe(FieldType.string);
|
||||
expect(result[0].fields[2].values).toEqual(['', '', 'localhost:8080', 'localhost:8080']);
|
||||
expect(result[0].fields[2].name).toBe('instance');
|
||||
expect(result[0].fields[2].type).toBe(FieldType.string);
|
||||
expect(result[0].fields[3].values).toEqual(['testjob', 'testjob', 'otherjob', 'otherjob']);
|
||||
expect(result[0].fields[3].name).toBe('job');
|
||||
expect(result[0].fields[3].type).toBe(FieldType.string);
|
||||
expect(result[0].fields[4].values).toEqual([3846, 3848, 3847, 3849]);
|
||||
expect(result[0].fields[4].name).toEqual('Value');
|
||||
expect(result[0].fields[4].type).toBe(FieldType.number);
|
||||
expect(result[0].refId).toBe('A');
|
||||
});
|
||||
|
||||
it('should include refId if response count is more than 2', () => {
|
||||
const result = transform({ data: response } as any, {
|
||||
...options,
|
||||
target: {
|
||||
refId: 'B',
|
||||
format: 'table',
|
||||
},
|
||||
responseListLength: 2,
|
||||
});
|
||||
|
||||
expect(result[0].fields[4].name).toEqual('Value #B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When resultFormat is table and instant = true', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'vector',
|
||||
result: [
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob' },
|
||||
value: [1443454528, '3846'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it('should return data frame', () => {
|
||||
const result = transform({ data: response } as any, { ...options, target: { format: 'table' } });
|
||||
expect(result[0].fields[0].values).toEqual([1443454528000]);
|
||||
expect(result[0].fields[0].name).toBe('Time');
|
||||
expect(result[0].fields[1].values).toEqual(['test']);
|
||||
expect(result[0].fields[1].name).toBe('__name__');
|
||||
expect(result[0].fields[2].values).toEqual(['testjob']);
|
||||
expect(result[0].fields[2].name).toBe('job');
|
||||
expect(result[0].fields[3].values).toEqual([3846]);
|
||||
expect(result[0].fields[3].name).toEqual('Value');
|
||||
});
|
||||
|
||||
it('should return le label values parsed as numbers', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'vector',
|
||||
result: [
|
||||
{
|
||||
metric: { le: '102' },
|
||||
value: [1594908838, '0'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const result = transform({ data: response } as any, { ...options, target: { format: 'table' } });
|
||||
expect(result[0].fields[1].values).toEqual([102]);
|
||||
expect(result[0].fields[1].type).toEqual(FieldType.number);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When instant = true', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'vector',
|
||||
result: [
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob' },
|
||||
value: [1443454528, '3846'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it('should return data frame', () => {
|
||||
const result: DataFrame[] = transform({ data: response } as any, { ...options, query: { instant: true } });
|
||||
expect(result[0].name).toBe('test{job="testjob"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When resultFormat is heatmap', () => {
|
||||
const getResponse = (result: any) => ({
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'matrix',
|
||||
result,
|
||||
},
|
||||
});
|
||||
|
||||
const options = {
|
||||
format: 'heatmap',
|
||||
start: 1445000010,
|
||||
end: 1445000030,
|
||||
legendFormat: '{{le}}',
|
||||
};
|
||||
|
||||
it('should convert cumulative histogram to regular', () => {
|
||||
const response = getResponse([
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob', le: '1' },
|
||||
values: [
|
||||
[1445000010, '10'],
|
||||
[1445000020, '10'],
|
||||
[1445000030, '0'],
|
||||
],
|
||||
},
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob', le: '2' },
|
||||
values: [
|
||||
[1445000010, '20'],
|
||||
[1445000020, '10'],
|
||||
[1445000030, '30'],
|
||||
],
|
||||
},
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob', le: '3' },
|
||||
values: [
|
||||
[1445000010, '30'],
|
||||
[1445000020, '10'],
|
||||
[1445000030, '40'],
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = transform({ data: response } as any, { query: options, target: options } as any);
|
||||
expect(result[0].fields[0].values).toEqual([1445000010000, 1445000020000, 1445000030000]);
|
||||
expect(result[0].fields[1].values).toEqual([10, 10, 0]);
|
||||
expect(result[0].fields[2].values).toEqual([10, 0, 30]);
|
||||
expect(result[0].fields[3].values).toEqual([10, 0, 10]);
|
||||
});
|
||||
|
||||
it('should handle missing datapoints', () => {
|
||||
const response = getResponse([
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob', le: '1' },
|
||||
values: [
|
||||
[1445000010, '1'],
|
||||
[1445000020, '2'],
|
||||
],
|
||||
},
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob', le: '2' },
|
||||
values: [
|
||||
[1445000010, '2'],
|
||||
[1445000020, '5'],
|
||||
[1445000030, '1'],
|
||||
],
|
||||
},
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob', le: '3' },
|
||||
values: [
|
||||
[1445000010, '3'],
|
||||
[1445000020, '7'],
|
||||
],
|
||||
},
|
||||
]);
|
||||
const result = transform({ data: response } as any, { query: options, target: options } as any);
|
||||
expect(result[0].fields[1].values).toEqual([1, 2]);
|
||||
expect(result[0].fields[2].values).toEqual([1, 3, 1]);
|
||||
expect(result[0].fields[3].values).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the response is a matrix', () => {
|
||||
it('should have labels with the value field', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'matrix',
|
||||
result: [
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob', instance: 'testinstance' },
|
||||
values: [
|
||||
[0, '10'],
|
||||
[1, '10'],
|
||||
[2, '0'],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result: DataFrame[] = transform({ data: response } as any, {
|
||||
...options,
|
||||
});
|
||||
|
||||
expect(result[0].fields[1].labels).toBeDefined();
|
||||
expect(result[0].fields[1].labels?.instance).toBe('testinstance');
|
||||
expect(result[0].fields[1].labels?.job).toBe('testjob');
|
||||
});
|
||||
|
||||
it('should transform into a data frame', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'matrix',
|
||||
result: [
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob' },
|
||||
values: [
|
||||
[0, '10'],
|
||||
[1, '10'],
|
||||
[2, '0'],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result: DataFrame[] = transform({ data: response } as any, {
|
||||
...options,
|
||||
query: {
|
||||
start: 0,
|
||||
end: 2,
|
||||
},
|
||||
});
|
||||
expect(result[0].fields[0].values).toEqual([0, 1000, 2000]);
|
||||
expect(result[0].fields[1].values).toEqual([10, 10, 0]);
|
||||
expect(result[0].name).toBe('test{job="testjob"}');
|
||||
});
|
||||
|
||||
it('should fill null values', () => {
|
||||
const result = transform({ data: matrixResponse } as any, {
|
||||
...options,
|
||||
query: { step: 1, start: 0, end: 2 },
|
||||
});
|
||||
|
||||
expect(result[0].fields[0].values).toEqual([0, 1000, 2000]);
|
||||
expect(result[0].fields[1].values).toEqual([null, 10, 0]);
|
||||
});
|
||||
|
||||
it('should use __name__ label as series name', () => {
|
||||
const result = transform({ data: matrixResponse } as any, {
|
||||
...options,
|
||||
query: {
|
||||
step: 1,
|
||||
start: 0,
|
||||
end: 2,
|
||||
},
|
||||
});
|
||||
expect(result[0].name).toEqual('test{job="testjob"}');
|
||||
});
|
||||
|
||||
it('should use query as series name when __name__ is not available and metric is empty', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'matrix',
|
||||
result: [
|
||||
{
|
||||
metric: {},
|
||||
values: [[0, '10']],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const expr = 'histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))';
|
||||
const result = transform({ data: response } as any, {
|
||||
...options,
|
||||
query: {
|
||||
step: 1,
|
||||
start: 0,
|
||||
end: 2,
|
||||
expr,
|
||||
},
|
||||
});
|
||||
expect(result[0].name).toEqual(expr);
|
||||
});
|
||||
|
||||
it('should set frame name to undefined if no __name__ label but there are other labels', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'matrix',
|
||||
result: [
|
||||
{
|
||||
metric: { job: 'testjob' },
|
||||
values: [
|
||||
[1, '10'],
|
||||
[2, '0'],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = transform({ data: response } as any, {
|
||||
...options,
|
||||
query: {
|
||||
step: 1,
|
||||
start: 0,
|
||||
end: 2,
|
||||
},
|
||||
});
|
||||
expect(result[0].name).toBe('{job="testjob"}');
|
||||
});
|
||||
|
||||
it('should not set displayName for ValueFields', () => {
|
||||
const result = transform({ data: matrixResponse } as any, options);
|
||||
expect(result[0].fields[1].config.displayName).toBeUndefined();
|
||||
expect(result[0].fields[1].config.displayNameFromDS).toBe('test{job="testjob"}');
|
||||
});
|
||||
|
||||
it('should align null values with step', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'matrix',
|
||||
result: [
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob' },
|
||||
values: [
|
||||
[4, '10'],
|
||||
[8, '10'],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = transform({ data: response } as any, { ...options, query: { step: 2, start: 0, end: 8 } });
|
||||
expect(result[0].fields[0].values).toEqual([0, 2000, 4000, 6000, 8000]);
|
||||
expect(result[0].fields[1].values).toEqual([null, null, 10, null, 10]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When infinity values are returned', () => {
|
||||
describe('When resultType is scalar', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'scalar',
|
||||
result: [1443454528, '+Inf'],
|
||||
},
|
||||
};
|
||||
|
||||
it('should correctly parse values', () => {
|
||||
const result: DataFrame[] = transform({ data: response } as any, {
|
||||
...options,
|
||||
target: { format: 'table' },
|
||||
});
|
||||
expect(result[0].fields[1].values).toEqual([Number.POSITIVE_INFINITY]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When resultType is vector', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: 'vector',
|
||||
result: [
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob' },
|
||||
value: [1443454528, '+Inf'],
|
||||
},
|
||||
{
|
||||
metric: { __name__: 'test', job: 'testjob' },
|
||||
value: [1443454528, '-Inf'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('When format is table', () => {
|
||||
it('should correctly parse values', () => {
|
||||
const result: DataFrame[] = transform({ data: response } as any, {
|
||||
...options,
|
||||
target: { format: 'table' },
|
||||
});
|
||||
expect(result[0].fields[3].values).toEqual([Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const exemplarsResponse = {
|
||||
status: 'success',
|
||||
data: [
|
||||
{
|
||||
seriesLabels: { __name__: 'test' },
|
||||
exemplars: [
|
||||
{
|
||||
timestamp: 1610449069.957,
|
||||
labels: { traceID: '5020b5bc45117f07' },
|
||||
value: 0.002074123,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('When the response is exemplar data', () => {
|
||||
it('should return as an data frame with a dataTopic annotations', () => {
|
||||
const result = transform({ data: exemplarsResponse } as any, options);
|
||||
|
||||
expect(result[0].meta?.dataTopic).toBe('annotations');
|
||||
expect(result[0].fields.length).toBe(4); // __name__, traceID, Time, Value
|
||||
expect(result[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it('should return with an empty array when data is empty', () => {
|
||||
const result = transform(
|
||||
{
|
||||
data: {
|
||||
status: 'success',
|
||||
data: [],
|
||||
},
|
||||
} as any,
|
||||
options
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should remove exemplars that are too close to each other', () => {
|
||||
const response = {
|
||||
status: 'success',
|
||||
data: [
|
||||
{
|
||||
exemplars: [
|
||||
{
|
||||
timestamp: 1610449070.0,
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
timestamp: 1610449070.0,
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
timestamp: 1610449070.5,
|
||||
value: 13,
|
||||
},
|
||||
{
|
||||
timestamp: 1610449070.3,
|
||||
value: 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
/**
|
||||
* the standard deviation for the above values is 8.4 this means that we show the highest
|
||||
* value (20) and then the next value should be 2 times the standard deviation which is 1
|
||||
**/
|
||||
const result = transform({ data: response } as any, options);
|
||||
expect(result[0].length).toBe(2);
|
||||
});
|
||||
|
||||
describe('data link', () => {
|
||||
it('should be added to the field if found with url', () => {
|
||||
const result = transform({ data: exemplarsResponse } as any, {
|
||||
...options,
|
||||
exemplarTraceIdDestinations: [{ name: 'traceID', url: 'http://localhost' }],
|
||||
});
|
||||
|
||||
expect(result[0].fields.some((f) => f.config.links?.length)).toBe(true);
|
||||
});
|
||||
|
||||
it('should be added to the field if found with internal link', () => {
|
||||
const result = transform({ data: exemplarsResponse } as any, {
|
||||
...options,
|
||||
exemplarTraceIdDestinations: [{ name: 'traceID', datasourceUid: 'jaeger' }],
|
||||
});
|
||||
|
||||
expect(result[0].fields.some((f) => f.config.links?.length)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not add link if exemplarTraceIdDestinations is not configured', () => {
|
||||
const result = transform({ data: exemplarsResponse } as any, options);
|
||||
|
||||
expect(result[0].fields.some((f) => f.config.links?.length)).toBe(false);
|
||||
});
|
||||
|
||||
it('should not add a datalink with an error when exemplarTraceIdDestinations is not configured', () => {
|
||||
const testOptions: any = {
|
||||
target: {},
|
||||
query: {},
|
||||
exemplarTraceIdDestinations: [
|
||||
{
|
||||
name: 'traceID',
|
||||
datasourceUid: 'unknown',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = transform({ data: exemplarsResponse } as any, testOptions);
|
||||
const traceField = result[0].fields.find((f) => f.name === 'traceID');
|
||||
expect(traceField).toBeDefined();
|
||||
expect(traceField!.config.links?.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { descending, deviation } from 'd3';
|
||||
import { flatten, forOwn, groupBy, partition } from 'lodash';
|
||||
|
||||
import {
|
||||
ArrayDataFrame,
|
||||
CoreApp,
|
||||
DataFrame,
|
||||
DataFrameType,
|
||||
@ -12,38 +10,19 @@ import {
|
||||
DataTopic,
|
||||
Field,
|
||||
FieldType,
|
||||
formatLabels,
|
||||
getDisplayProcessor,
|
||||
getFieldDisplayName,
|
||||
Labels,
|
||||
renderLegendFormat,
|
||||
ScopedVars,
|
||||
TIME_SERIES_TIME_FIELD_NAME,
|
||||
TIME_SERIES_VALUE_FIELD_NAME,
|
||||
} from '@grafana/data';
|
||||
import { config, FetchResponse, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime';
|
||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||
|
||||
import {
|
||||
ExemplarTraceIdDestination,
|
||||
isExemplarData,
|
||||
isMatrixData,
|
||||
MatrixOrVectorResult,
|
||||
PromDataSuccessResponse,
|
||||
PromMetric,
|
||||
PromQuery,
|
||||
PromQueryRequest,
|
||||
PromValue,
|
||||
TransformOptions,
|
||||
} from './types';
|
||||
import { ExemplarTraceIdDestination, PromMetric, PromQuery, PromValue } from './types';
|
||||
|
||||
// handles case-insensitive Inf, +Inf, -Inf (with optional "inity" suffix)
|
||||
const INFINITY_SAMPLE_REGEX = /^[+-]?inf(?:inity)?$/i;
|
||||
|
||||
interface TimeAndValue {
|
||||
[TIME_SERIES_TIME_FIELD_NAME]: number;
|
||||
[TIME_SERIES_VALUE_FIELD_NAME]: number;
|
||||
}
|
||||
|
||||
const isTableResult = (dataFrame: DataFrame, options: DataQueryRequest<PromQuery>): boolean => {
|
||||
// We want to process vector and scalar results in Explore as table
|
||||
if (
|
||||
@ -264,104 +243,6 @@ function getValueText(responseLength: number, refId = '') {
|
||||
return responseLength > 1 ? `Value #${refId}` : 'Value';
|
||||
}
|
||||
|
||||
export function transform(
|
||||
response: FetchResponse<PromDataSuccessResponse>,
|
||||
transformOptions: {
|
||||
query: PromQueryRequest;
|
||||
exemplarTraceIdDestinations?: ExemplarTraceIdDestination[];
|
||||
target: PromQuery;
|
||||
responseListLength: number;
|
||||
scopedVars?: ScopedVars;
|
||||
}
|
||||
) {
|
||||
// Create options object from transformOptions
|
||||
const options: TransformOptions = {
|
||||
format: transformOptions.target.format,
|
||||
step: transformOptions.query.step,
|
||||
legendFormat: transformOptions.target.legendFormat,
|
||||
start: transformOptions.query.start,
|
||||
end: transformOptions.query.end,
|
||||
query: transformOptions.query.expr,
|
||||
responseListLength: transformOptions.responseListLength,
|
||||
scopedVars: transformOptions.scopedVars,
|
||||
refId: transformOptions.target.refId,
|
||||
valueWithRefId: transformOptions.target.valueWithRefId,
|
||||
meta: {
|
||||
// Fix for showing of Prometheus results in Explore table
|
||||
preferredVisualisationType: transformOptions.query.instant ? 'rawPrometheus' : 'graph',
|
||||
},
|
||||
};
|
||||
const prometheusResult = response.data.data;
|
||||
|
||||
if (isExemplarData(prometheusResult)) {
|
||||
const events: TimeAndValue[] = [];
|
||||
prometheusResult.forEach((exemplarData) => {
|
||||
const data = exemplarData.exemplars.map((exemplar) => {
|
||||
return {
|
||||
[TIME_SERIES_TIME_FIELD_NAME]: exemplar.timestamp * 1000,
|
||||
[TIME_SERIES_VALUE_FIELD_NAME]: exemplar.value,
|
||||
...exemplar.labels,
|
||||
...exemplarData.seriesLabels,
|
||||
};
|
||||
});
|
||||
events.push(...data);
|
||||
});
|
||||
|
||||
// Grouping exemplars by step
|
||||
const sampledExemplars = sampleExemplars(events, options);
|
||||
|
||||
const dataFrame = new ArrayDataFrame(sampledExemplars);
|
||||
dataFrame.meta = { dataTopic: DataTopic.Annotations };
|
||||
|
||||
// Add data links if configured
|
||||
if (transformOptions.exemplarTraceIdDestinations?.length) {
|
||||
for (const exemplarTraceIdDestination of transformOptions.exemplarTraceIdDestinations) {
|
||||
const traceIDField = dataFrame.fields.find((field) => field.name === exemplarTraceIdDestination.name);
|
||||
if (traceIDField) {
|
||||
const links = getDataLinks(exemplarTraceIdDestination);
|
||||
traceIDField.config.links = traceIDField.config.links?.length
|
||||
? [...traceIDField.config.links, ...links]
|
||||
: links;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [dataFrame];
|
||||
}
|
||||
|
||||
if (!prometheusResult?.result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Return early if result type is scalar
|
||||
if (prometheusResult.resultType === 'scalar') {
|
||||
const df: DataFrame = {
|
||||
meta: options.meta,
|
||||
refId: options.refId,
|
||||
length: 1,
|
||||
fields: [getTimeField([prometheusResult.result]), getValueField({ data: [prometheusResult.result] })],
|
||||
};
|
||||
return [df];
|
||||
}
|
||||
|
||||
// Return early again if the format is table, this needs special transformation.
|
||||
if (options.format === 'table') {
|
||||
const tableData = transformMetricDataToTable(prometheusResult.result, options);
|
||||
return [tableData];
|
||||
}
|
||||
|
||||
// Process matrix and vector results to DataFrame
|
||||
const dataFrame: DataFrame[] = [];
|
||||
prometheusResult.result.forEach((data: MatrixOrVectorResult) => dataFrame.push(transformToDataFrame(data, options)));
|
||||
|
||||
// When format is heatmap use the already created data frames and transform it more
|
||||
if (options.format === 'heatmap') {
|
||||
return mergeHeatmapFrames(transformToHistogramOverTime(dataFrame.sort(sortSeriesByLabel)));
|
||||
}
|
||||
|
||||
// Return matrix or vector result as DataFrame[]
|
||||
return dataFrame;
|
||||
}
|
||||
|
||||
function getDataLinks(options: ExemplarTraceIdDestination): DataLink[] {
|
||||
const dataLinks: DataLink[] = [];
|
||||
|
||||
@ -396,160 +277,6 @@ function getDataLinks(options: ExemplarTraceIdDestination): DataLink[] {
|
||||
return dataLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the density of the exemplars by making sure that the highest value exemplar is included
|
||||
* and then only the ones that are 2 times the standard deviation of the all the values.
|
||||
* This makes sure not to show too many dots near each other.
|
||||
*/
|
||||
function sampleExemplars(events: TimeAndValue[], options: TransformOptions) {
|
||||
const step = options.step || 15;
|
||||
const bucketedExemplars: { [ts: string]: TimeAndValue[] } = {};
|
||||
const values: number[] = [];
|
||||
for (const exemplar of events) {
|
||||
// Align exemplar timestamp to nearest step second
|
||||
const alignedTs = String(Math.floor(exemplar[TIME_SERIES_TIME_FIELD_NAME] / 1000 / step) * step * 1000);
|
||||
if (!bucketedExemplars[alignedTs]) {
|
||||
// New bucket found
|
||||
bucketedExemplars[alignedTs] = [];
|
||||
}
|
||||
bucketedExemplars[alignedTs].push(exemplar);
|
||||
values.push(exemplar[TIME_SERIES_VALUE_FIELD_NAME]);
|
||||
}
|
||||
|
||||
// Getting exemplars from each bucket
|
||||
const standardDeviation = deviation(values);
|
||||
const sampledBuckets = Object.keys(bucketedExemplars).sort();
|
||||
const sampledExemplars = [];
|
||||
for (const ts of sampledBuckets) {
|
||||
const exemplarsInBucket = bucketedExemplars[ts];
|
||||
if (exemplarsInBucket.length === 1) {
|
||||
sampledExemplars.push(exemplarsInBucket[0]);
|
||||
} else {
|
||||
// Choose which values to sample
|
||||
const bucketValues = exemplarsInBucket.map((ex) => ex[TIME_SERIES_VALUE_FIELD_NAME]).sort(descending);
|
||||
const sampledBucketValues = bucketValues.reduce((acc: number[], curr) => {
|
||||
if (acc.length === 0) {
|
||||
// First value is max and is always added
|
||||
acc.push(curr);
|
||||
} else {
|
||||
// Then take values only when at least 2 standard deviation distance to previously taken value
|
||||
const prev = acc[acc.length - 1];
|
||||
if (standardDeviation && prev - curr >= 2 * standardDeviation) {
|
||||
acc.push(curr);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
// Find the exemplars for the sampled values
|
||||
sampledExemplars.push(
|
||||
...sampledBucketValues.map(
|
||||
(value) => exemplarsInBucket.find((ex) => ex[TIME_SERIES_VALUE_FIELD_NAME] === value)!
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return sampledExemplars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms matrix and vector result from Prometheus result to DataFrame
|
||||
*/
|
||||
function transformToDataFrame(data: MatrixOrVectorResult, options: TransformOptions): DataFrame {
|
||||
const { name, labels } = createLabelInfo(data.metric, options);
|
||||
|
||||
const fields: Field[] = [];
|
||||
|
||||
if (isMatrixData(data)) {
|
||||
const stepMs = options.step ? options.step * 1000 : NaN;
|
||||
let baseTimestamp = options.start * 1000;
|
||||
const dps: PromValue[] = [];
|
||||
|
||||
for (const value of data.values) {
|
||||
let dpValue: number | null = parseSampleValue(value[1]);
|
||||
|
||||
if (isNaN(dpValue)) {
|
||||
dpValue = null;
|
||||
}
|
||||
|
||||
const timestamp = value[0] * 1000;
|
||||
for (let t = baseTimestamp; t < timestamp; t += stepMs) {
|
||||
dps.push([t, null]);
|
||||
}
|
||||
baseTimestamp = timestamp + stepMs;
|
||||
dps.push([timestamp, dpValue]);
|
||||
}
|
||||
|
||||
const endTimestamp = options.end * 1000;
|
||||
for (let t = baseTimestamp; t <= endTimestamp; t += stepMs) {
|
||||
dps.push([t, null]);
|
||||
}
|
||||
fields.push(getTimeField(dps, true));
|
||||
fields.push(getValueField({ data: dps, parseValue: false, labels, displayNameFromDS: name }));
|
||||
} else {
|
||||
fields.push(getTimeField([data.value]));
|
||||
fields.push(getValueField({ data: [data.value], labels, displayNameFromDS: name }));
|
||||
}
|
||||
|
||||
return {
|
||||
meta: options.meta,
|
||||
refId: options.refId,
|
||||
length: fields[0].values.length,
|
||||
fields,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
function transformMetricDataToTable(md: MatrixOrVectorResult[], options: TransformOptions): DataFrame {
|
||||
if (!md || md.length === 0) {
|
||||
return {
|
||||
meta: options.meta,
|
||||
refId: options.refId,
|
||||
length: 0,
|
||||
fields: [],
|
||||
};
|
||||
}
|
||||
|
||||
const valueText = options.responseListLength > 1 || options.valueWithRefId ? `Value #${options.refId}` : 'Value';
|
||||
|
||||
const timeField = getTimeField([]);
|
||||
const metricFields = Object.keys(md.reduce((acc, series) => ({ ...acc, ...series.metric }), {}))
|
||||
.sort()
|
||||
.map((label) => {
|
||||
// Labels have string field type, otherwise table tries to figure out the type which can result in unexpected results
|
||||
// Only "le" label has a number field type
|
||||
const numberField = label === HISTOGRAM_QUANTILE_LABEL_NAME;
|
||||
const field: Field = {
|
||||
name: label,
|
||||
config: { filterable: true },
|
||||
type: numberField ? FieldType.number : FieldType.string,
|
||||
values: [],
|
||||
};
|
||||
return field;
|
||||
});
|
||||
const valueField = getValueField({ data: [], valueName: valueText });
|
||||
|
||||
md.forEach((d) => {
|
||||
if (isMatrixData(d)) {
|
||||
d.values.forEach((val) => {
|
||||
timeField.values.push(val[0] * 1000);
|
||||
metricFields.forEach((metricField) => metricField.values.push(getLabelValue(d.metric, metricField.name)));
|
||||
valueField.values.push(parseSampleValue(val[1]));
|
||||
});
|
||||
} else {
|
||||
timeField.values.push(d.value[0] * 1000);
|
||||
metricFields.forEach((metricField) => metricField.values.push(getLabelValue(d.metric, metricField.name)));
|
||||
valueField.values.push(parseSampleValue(d.value[1]));
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
meta: options.meta,
|
||||
refId: options.refId,
|
||||
length: timeField.values.length,
|
||||
fields: [timeField, ...metricFields, valueField],
|
||||
};
|
||||
}
|
||||
|
||||
function getLabelValue(metric: PromMetric, label: string): string | number {
|
||||
if (metric.hasOwnProperty(label)) {
|
||||
if (label === HISTOGRAM_QUANTILE_LABEL_NAME) {
|
||||
@ -596,23 +323,6 @@ function getValueField({
|
||||
};
|
||||
}
|
||||
|
||||
function createLabelInfo(labels: { [key: string]: string }, options: TransformOptions) {
|
||||
if (options?.legendFormat) {
|
||||
const title = renderLegendFormat(getTemplateSrv().replace(options.legendFormat, options?.scopedVars), labels);
|
||||
return { name: title, labels };
|
||||
}
|
||||
|
||||
const { __name__, ...labelsWithoutName } = labels;
|
||||
const labelPart = formatLabels(labelsWithoutName);
|
||||
let title = `${__name__ ?? ''}${labelPart}`;
|
||||
|
||||
if (!title) {
|
||||
title = options.query;
|
||||
}
|
||||
|
||||
return { name: title, labels: labelsWithoutName };
|
||||
}
|
||||
|
||||
export function getOriginalMetricName(labelData: { [key: string]: string }) {
|
||||
const metricName = labelData.__name__ || '';
|
||||
delete labelData.__name__;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataSourceJsonData, QueryResultMeta, ScopedVars } from '@grafana/data';
|
||||
import { DataSourceJsonData } from '@grafana/data';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
|
||||
import { Prometheus as GenPromQuery } from './dataquery.gen';
|
||||
@ -39,7 +39,6 @@ export interface PromOptions extends DataSourceJsonData {
|
||||
timeInterval?: string;
|
||||
queryTimeout?: string;
|
||||
httpMethod?: string;
|
||||
directUrl?: string;
|
||||
customQueryParameters?: string;
|
||||
disableMetricsLookup?: boolean;
|
||||
exemplarTraceIdDestinations?: ExemplarTraceIdDestination[];
|
||||
@ -79,56 +78,6 @@ export interface PromMetricsMetadata {
|
||||
[metric: string]: PromMetricsMetadataItem;
|
||||
}
|
||||
|
||||
export interface PromDataSuccessResponse<T = PromData> {
|
||||
status: 'success';
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PromDataErrorResponse<T = PromData> {
|
||||
status: 'error';
|
||||
errorType: string;
|
||||
error: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export type PromData = PromMatrixData | PromVectorData | PromScalarData | PromExemplarData[];
|
||||
|
||||
export interface Labels {
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
export interface Exemplar {
|
||||
labels: Labels;
|
||||
value: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface PromExemplarData {
|
||||
seriesLabels: PromMetric;
|
||||
exemplars: Exemplar[];
|
||||
}
|
||||
|
||||
export interface PromVectorData {
|
||||
resultType: 'vector';
|
||||
result: Array<{
|
||||
metric: PromMetric;
|
||||
value: PromValue;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface PromMatrixData {
|
||||
resultType: 'matrix';
|
||||
result: Array<{
|
||||
metric: PromMetric;
|
||||
values: PromValue[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface PromScalarData {
|
||||
resultType: 'scalar';
|
||||
result: PromValue;
|
||||
}
|
||||
|
||||
export type PromValue = [number, any];
|
||||
|
||||
export interface PromMetric {
|
||||
@ -137,33 +86,6 @@ export interface PromMetric {
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
export function isMatrixData(result: MatrixOrVectorResult): result is PromMatrixData['result'][0] {
|
||||
return 'values' in result;
|
||||
}
|
||||
|
||||
export function isExemplarData(result: PromData): result is PromExemplarData[] {
|
||||
if (result == null || !Array.isArray(result)) {
|
||||
return false;
|
||||
}
|
||||
return result.length ? 'exemplars' in result[0] : false;
|
||||
}
|
||||
|
||||
export type MatrixOrVectorResult = PromMatrixData['result'][0] | PromVectorData['result'][0];
|
||||
|
||||
export interface TransformOptions {
|
||||
format?: string;
|
||||
step?: number;
|
||||
legendFormat?: string;
|
||||
start: number;
|
||||
end: number;
|
||||
query: string;
|
||||
responseListLength: number;
|
||||
scopedVars?: ScopedVars;
|
||||
refId: string;
|
||||
valueWithRefId?: boolean;
|
||||
meta: QueryResultMeta;
|
||||
}
|
||||
|
||||
export interface PromBuildInfoResponse {
|
||||
data: {
|
||||
application?: string;
|
||||
|
Loading…
Reference in New Issue
Block a user