introduce new methods

This commit is contained in:
ismail simsek
2025-05-25 00:59:02 +02:00
parent 7d5b7680cc
commit db20a017da
4 changed files with 259 additions and 82 deletions
@@ -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}`;
}
@@ -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
* */