introduce new methods
This commit is contained in:
@@ -6,14 +6,12 @@ import { Label } from './components/monaco-query-field/monaco-completion-provide
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import LanguageProvider, {
|
||||
exportToAbstractQuery,
|
||||
getMetadataHelp,
|
||||
getMetadataString,
|
||||
getMetadataType,
|
||||
importFromAbstractQuery,
|
||||
processSeries,
|
||||
removeQuotesIfExist,
|
||||
} from './language_provider';
|
||||
import { getClientCacheDurationInMinutes, getPrometheusTime, getRangeSnapInterval } from './language_utils';
|
||||
import { PrometheusCacheLevel, PromMetricsMetadata, PromQuery } from './types';
|
||||
import { PrometheusCacheLevel, PromQuery } from './types';
|
||||
|
||||
const now = new Date(1681300293392).getTime();
|
||||
const timeRangeDurationSeconds = 1;
|
||||
@@ -645,49 +643,6 @@ describe('Prometheus Language Provider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Metadata utility functions', () => {
|
||||
const testMetadata: PromMetricsMetadata = {
|
||||
metric1: { type: 'counter', help: 'Test counter help text' },
|
||||
metric2: { type: 'gauge', help: 'Test gauge help text' },
|
||||
};
|
||||
|
||||
describe('getMetadataString', () => {
|
||||
it('should return formatted string with type and help text', () => {
|
||||
const result = getMetadataString('metric1', testMetadata);
|
||||
expect(result).toBe('COUNTER: Test counter help text');
|
||||
});
|
||||
|
||||
it('should return undefined for unknown metrics', () => {
|
||||
const result = getMetadataString('unknown_metric', testMetadata);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMetadataHelp', () => {
|
||||
it('should return help text for known metric', () => {
|
||||
const result = getMetadataHelp('metric2', testMetadata);
|
||||
expect(result).toBe('Test gauge help text');
|
||||
});
|
||||
|
||||
it('should return undefined for unknown metrics', () => {
|
||||
const result = getMetadataHelp('unknown_metric', testMetadata);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMetadataType', () => {
|
||||
it('should return type for known metric', () => {
|
||||
const result = getMetadataType('metric1', testMetadata);
|
||||
expect(result).toBe('counter');
|
||||
});
|
||||
|
||||
it('should return undefined for unknown metrics', () => {
|
||||
const result = getMetadataType('unknown_metric', testMetadata);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSuggestions', () => {
|
||||
it('should send POST request with correct parameters', async () => {
|
||||
const timeRange = getMockTimeRange();
|
||||
@@ -827,3 +782,71 @@ describe('removeQuotesIfExist', () => {
|
||||
expect(result).toBe('12345');
|
||||
});
|
||||
});
|
||||
|
||||
describe('processSeries', () => {
|
||||
it('should extract metrics and label keys from series data', () => {
|
||||
const result = processSeries([
|
||||
{
|
||||
__name__: 'alerts',
|
||||
alertname: 'AppCrash',
|
||||
alertstate: 'firing',
|
||||
instance: 'host.docker.internal:3000',
|
||||
job: 'grafana',
|
||||
severity: 'critical',
|
||||
},
|
||||
{
|
||||
__name__: 'alerts',
|
||||
alertname: 'AppCrash',
|
||||
alertstate: 'firing',
|
||||
instance: 'prometheus-utf8:9112',
|
||||
job: 'prometheus-utf8',
|
||||
severity: 'critical',
|
||||
},
|
||||
{
|
||||
__name__: 'counters_logins',
|
||||
app: 'backend',
|
||||
geohash: '9wvfgzurfzb',
|
||||
instance: 'fake-prometheus-data:9091',
|
||||
job: 'fake-data-gen',
|
||||
server: 'backend-01',
|
||||
},
|
||||
]);
|
||||
|
||||
// Check structure
|
||||
expect(result).toHaveProperty('metrics');
|
||||
expect(result).toHaveProperty('labelKeys');
|
||||
|
||||
// Verify metrics are extracted correctly
|
||||
expect(result.metrics).toEqual(['alerts', 'counters_logins']);
|
||||
|
||||
// Verify all metrics are unique
|
||||
expect(result.metrics.length).toBe(new Set(result.metrics).size);
|
||||
|
||||
// Verify label keys are extracted correctly and don't include __name__
|
||||
expect(result.labelKeys).toContain('instance');
|
||||
expect(result.labelKeys).toContain('job');
|
||||
expect(result.labelKeys).not.toContain('__name__');
|
||||
|
||||
// Verify all label keys are unique
|
||||
expect(result.labelKeys.length).toBe(new Set(result.labelKeys).size);
|
||||
});
|
||||
|
||||
it('should handle empty series data', () => {
|
||||
const result = processSeries([]);
|
||||
|
||||
expect(result.metrics).toEqual([]);
|
||||
expect(result.labelKeys).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle series without __name__ attribute', () => {
|
||||
const series = [
|
||||
{ instance: 'localhost:9090', job: 'prometheus' },
|
||||
{ instance: 'localhost:9100', job: 'node' },
|
||||
];
|
||||
|
||||
const result = processSeries(series);
|
||||
|
||||
expect(result.metrics).toEqual([]);
|
||||
expect(result.labelKeys).toEqual(['instance', 'job']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
extractLabelMatchers,
|
||||
fixSummariesMetadata,
|
||||
getClientCacheDurationInMinutes,
|
||||
getRangeSnapInterval,
|
||||
processHistogramMetrics,
|
||||
processLabels,
|
||||
toPromLikeQuery,
|
||||
@@ -82,29 +83,6 @@ const getDefaultCacheHeaders = (cacheLevel: PrometheusCacheLevel) => {
|
||||
return;
|
||||
};
|
||||
|
||||
export function getMetadataString(metric: string, metadata: PromMetricsMetadata): string | undefined {
|
||||
if (!metadata[metric]) {
|
||||
return undefined;
|
||||
}
|
||||
const { type, help } = metadata[metric];
|
||||
return `${type.toUpperCase()}: ${help}`;
|
||||
}
|
||||
|
||||
export function getMetadataHelp(metric: string, metadata: PromMetricsMetadata): string | undefined {
|
||||
if (!metadata[metric]) {
|
||||
return undefined;
|
||||
}
|
||||
return metadata[metric].help;
|
||||
}
|
||||
|
||||
export function getMetadataType(metric: string, metadata: PromMetricsMetadata): string | undefined {
|
||||
if (!metadata[metric]) {
|
||||
return undefined;
|
||||
}
|
||||
return metadata[metric].type;
|
||||
}
|
||||
|
||||
const secondsInDay = 86400;
|
||||
export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
histogramMetrics: string[];
|
||||
metrics: string[];
|
||||
@@ -142,23 +120,147 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Prevent ts yelling
|
||||
console.log(
|
||||
this._withLabelsApiFetchLabelKeys,
|
||||
this._withLabelsApiFetchLabelValues,
|
||||
this._withSeriesApiFetchAllSeries,
|
||||
this._withSeriesApiFetchLabelKeys,
|
||||
this._withSeriesApiFetchLabelValues
|
||||
);
|
||||
|
||||
this.metrics = (await this.fetchLabelValues(timeRange, '__name__')) || [];
|
||||
this.histogramMetrics = processHistogramMetrics(this.metrics).sort();
|
||||
return Promise.all([this.loadMetricsMetadata(), this.fetchLabels(timeRange)]);
|
||||
};
|
||||
|
||||
async loadMetricsMetadata() {
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* Fetches metadata for metrics from Prometheus.
|
||||
* Sets cache headers based on the configured metadata cache duration.
|
||||
*
|
||||
* @returns {Promise<PromMetricsMetadata>} Promise that resolves when metadata has been fetched
|
||||
*/
|
||||
fetchMetadata = async () => {
|
||||
const secondsInDay = 86400;
|
||||
const headers = buildCacheHeaders(this.datasource.getDaysToCacheMetadata() * secondsInDay);
|
||||
this.metricsMetadata = fixSummariesMetadata(
|
||||
await this.request(
|
||||
API_V1.METADATA,
|
||||
{},
|
||||
{
|
||||
showErrorAlert: false,
|
||||
...headers,
|
||||
}
|
||||
)
|
||||
const metadata = await this.request(
|
||||
API_V1.METADATA,
|
||||
{},
|
||||
{
|
||||
showErrorAlert: false,
|
||||
...headers,
|
||||
}
|
||||
);
|
||||
this.metricsMetadata = fixSummariesMetadata(metadata);
|
||||
return this.metricsMetadata;
|
||||
};
|
||||
|
||||
// ===================================
|
||||
// Labels API
|
||||
// ===================================
|
||||
|
||||
/**
|
||||
* Fetches all available label keys from Prometheus using labels endpoint.
|
||||
* Uses the labels endpoint with optional match parameter for filtering.
|
||||
*
|
||||
* @param {TimeRange} timeRange - Time range to use for the query
|
||||
* @param {string} match - Optional label matcher to filter results
|
||||
* @param {string} limit - Maximum number of results to return
|
||||
* @returns {Promise<string[]>} Array of label keys sorted alphabetically
|
||||
*/
|
||||
private _withLabelsApiFetchLabelKeys = async (
|
||||
timeRange: TimeRange,
|
||||
match?: string,
|
||||
limit: string = DEFAULT_SERIES_LIMIT
|
||||
): Promise<string[]> => {
|
||||
let url = API_V1.LABELS;
|
||||
const timeParams = getRangeSnapInterval(this.datasource.cacheLevel, timeRange);
|
||||
const searchParams = { limit, ...timeParams, ...(match ? { 'match[]': match } : {}) };
|
||||
|
||||
const res = await this.request(url, searchParams, getDefaultCacheHeaders(this.datasource.cacheLevel));
|
||||
if (Array.isArray(res)) {
|
||||
this.labelKeys = res.slice().sort();
|
||||
return this.labelKeys.slice();
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches all values for a specific label key from Prometheus using labels values endpoint.
|
||||
*
|
||||
* @param {TimeRange} timeRange - Time range to use for the query
|
||||
* @param {string} labelKey - The label key to fetch values for
|
||||
* @param {string} match - Optional label matcher to filter results
|
||||
* @param {string} limit - Maximum number of results to return
|
||||
* @returns {Promise<string[]>} Array of label values
|
||||
*/
|
||||
private _withLabelsApiFetchLabelValues = async (
|
||||
timeRange: TimeRange,
|
||||
labelKey: string,
|
||||
match?: string,
|
||||
limit: string = DEFAULT_SERIES_LIMIT
|
||||
): Promise<string[]> => {
|
||||
const timeParams = this.datasource.getAdjustedInterval(timeRange);
|
||||
const searchParams = { limit, ...timeParams, ...(match ? { 'match[]': match } : {}) };
|
||||
const interpolatedName = this.datasource.interpolateString(labelKey);
|
||||
const interpolatedAndEscapedName = escapeForUtf8Support(removeQuotesIfExist(interpolatedName));
|
||||
const url = API_V1.LABELS_VALUES(interpolatedAndEscapedName);
|
||||
const value = await this.request(url, searchParams, getDefaultCacheHeaders(this.datasource.cacheLevel));
|
||||
return value ?? [];
|
||||
};
|
||||
|
||||
// ===================================
|
||||
// Series API
|
||||
// ===================================
|
||||
|
||||
/**
|
||||
* Fetches all time series that match a specific label matcher using series endpoint.
|
||||
*
|
||||
* @param {TimeRange} timeRange - Time range to use for the query
|
||||
* @param {string} match - Label matcher to filter time series
|
||||
* @param {string} limit - Maximum number of series to return
|
||||
*/
|
||||
private _withSeriesApiFetchAllSeries = async (
|
||||
timeRange: TimeRange,
|
||||
match: string,
|
||||
limit: string = DEFAULT_SERIES_LIMIT
|
||||
) => {
|
||||
const timeParams = this.datasource.getTimeRangeParams(timeRange);
|
||||
const searchParams = { ...timeParams, 'match[]': match, limit };
|
||||
return await this.request(API_V1.SERIES, searchParams, getDefaultCacheHeaders(this.datasource.cacheLevel));
|
||||
};
|
||||
|
||||
private _withSeriesApiFetchLabelKeys = async (
|
||||
timeRange: TimeRange,
|
||||
match: string,
|
||||
limit: string = DEFAULT_SERIES_LIMIT
|
||||
): Promise<string[]> => {
|
||||
const series = await this._withSeriesApiFetchAllSeries(timeRange, match, limit);
|
||||
const { labelKeys } = processSeries(series);
|
||||
return labelKeys;
|
||||
};
|
||||
|
||||
private _withSeriesApiFetchLabelValues = async (
|
||||
timeRange: TimeRange,
|
||||
labelKey: string,
|
||||
match: string,
|
||||
limit: string = DEFAULT_SERIES_LIMIT
|
||||
): Promise<string[]> => {
|
||||
const series = await this._withSeriesApiFetchAllSeries(timeRange, match, limit);
|
||||
const { labelValues } = processSeries(series, labelKey);
|
||||
return labelValues;
|
||||
};
|
||||
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* @deprecated Use fetchMetadata instead
|
||||
*/
|
||||
async loadMetricsMetadata() {
|
||||
this.fetchMetadata();
|
||||
}
|
||||
|
||||
getLabelKeys(): string[] {
|
||||
@@ -472,6 +574,36 @@ export const exportToAbstractQuery = (query: PromQuery): AbstractQuery => {
|
||||
};
|
||||
};
|
||||
|
||||
export function processSeries(series: Array<{ [key: string]: string }>, findValuesForKey?: string) {
|
||||
const metrics: Set<string> = new Set();
|
||||
const labelKeys: Set<string> = new Set();
|
||||
const labelValues: Set<string> = new Set();
|
||||
|
||||
// Extract metrics and label keys
|
||||
series.forEach((item) => {
|
||||
// Add the __name__ value to metrics
|
||||
if ('__name__' in item) {
|
||||
metrics.add(item.__name__);
|
||||
}
|
||||
|
||||
// Add all keys except __name__ to labelKeys
|
||||
Object.keys(item).forEach((key) => {
|
||||
if (key !== '__name__') {
|
||||
labelKeys.add(key);
|
||||
}
|
||||
if (findValuesForKey && key === findValuesForKey) {
|
||||
labelValues.add(item[key]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
metrics: Array.from(metrics).sort(),
|
||||
labelKeys: Array.from(labelKeys).sort(),
|
||||
labelValues: Array.from(labelValues).sort(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an error is a cancelled request error.
|
||||
* Used to avoid logging cancelled request errors.
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useCallback } from 'react';
|
||||
import { SelectableValue, TimeRange } from '@grafana/data';
|
||||
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { getMetadataString } from '../../language_provider';
|
||||
import { truncateResult } from '../../language_utils';
|
||||
import { PromMetricsMetadata } from '../../types';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { regexifyLabelValuesQueryString } from '../parsingUtils';
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
@@ -254,3 +254,11 @@ async function getMetrics(
|
||||
description: getMetadataString(m, datasource.languageProvider.metricsMetadata!),
|
||||
}));
|
||||
}
|
||||
|
||||
export function getMetadataString(metric: string, metadata: PromMetricsMetadata): string | undefined {
|
||||
if (!metadata[metric]) {
|
||||
return undefined;
|
||||
}
|
||||
const { type, help } = metadata[metric];
|
||||
return `${type.toUpperCase()}: ${help}`;
|
||||
}
|
||||
|
||||
+15
-1
@@ -4,7 +4,7 @@ import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
|
||||
import { PrometheusDatasource } from '../../../../datasource';
|
||||
import { getMetadataHelp, getMetadataType } from '../../../../language_provider';
|
||||
import { PromMetricsMetadata } from '../../../../types';
|
||||
import { regexifyLabelValuesQueryString } from '../../../parsingUtils';
|
||||
import { QueryBuilderLabelFilter } from '../../../shared/types';
|
||||
import { PromVisualQuery } from '../../../types';
|
||||
@@ -89,6 +89,20 @@ function buildMetricData(metric: string, datasource: PrometheusDatasource): Metr
|
||||
return metricData;
|
||||
}
|
||||
|
||||
export function getMetadataHelp(metric: string, metadata: PromMetricsMetadata): string | undefined {
|
||||
if (!metadata[metric]) {
|
||||
return undefined;
|
||||
}
|
||||
return metadata[metric].help;
|
||||
}
|
||||
|
||||
export function getMetadataType(metric: string, metadata: PromMetricsMetadata): string | undefined {
|
||||
if (!metadata[metric]) {
|
||||
return undefined;
|
||||
}
|
||||
return metadata[metric].type;
|
||||
}
|
||||
|
||||
/**
|
||||
* The filtered and paginated metrics displayed in the modal
|
||||
* */
|
||||
|
||||
Reference in New Issue
Block a user