import type { Situation, Label } from './situation'; import { NeverCaseError } from './util'; // FIXME: we should not load this from the "outside", but we cannot do that while we have the "old" query-field too import { FUNCTIONS } from '../../../promql'; import { escapeLabelValueInExactSelector } from '../../../language_utils'; export type CompletionType = 'HISTORY' | 'FUNCTION' | 'METRIC_NAME' | 'DURATION' | 'LABEL_NAME' | 'LABEL_VALUE'; type Completion = { type: CompletionType; label: string; insertText: string; detail?: string; documentation?: string; triggerOnInsert?: boolean; }; type Metric = { name: string; help: string; type: string; }; export type DataProvider = { getHistory: () => Promise; getAllMetricNames: () => Promise; getAllLabelNames: () => Promise; getLabelValues: (labelName: string) => Promise; getSeries: (selector: string) => Promise>; }; // we order items like: history, functions, metrics async function getAllMetricNamesCompletions(dataProvider: DataProvider): Promise { const metrics = await dataProvider.getAllMetricNames(); return metrics.map((metric) => ({ type: 'METRIC_NAME', label: metric.name, insertText: metric.name, detail: `${metric.name} : ${metric.type}`, documentation: metric.help, })); } const FUNCTION_COMPLETIONS: Completion[] = FUNCTIONS.map((f) => ({ type: 'FUNCTION', label: f.label, insertText: f.insertText ?? '', // i don't know what to do when this is nullish. it should not be. detail: f.detail, documentation: f.documentation, })); async function getAllFunctionsAndMetricNamesCompletions(dataProvider: DataProvider): Promise { const metricNames = await getAllMetricNamesCompletions(dataProvider); return [...FUNCTION_COMPLETIONS, ...metricNames]; } const DURATION_COMPLETIONS: Completion[] = [ '$__interval', '$__range', '$__rate_interval', '1m', '5m', '10m', '30m', '1h', '1d', ].map((text) => ({ type: 'DURATION', label: text, insertText: text, })); async function getAllHistoryCompletions(dataProvider: DataProvider): Promise { // function getAllHistoryCompletions(queryHistory: PromHistoryItem[]): Completion[] { // NOTE: the typescript types are wrong. historyItem.query.expr can be undefined const allHistory = await dataProvider.getHistory(); // FIXME: find a better history-limit return allHistory.slice(0, 10).map((expr) => ({ type: 'HISTORY', label: expr, insertText: expr, })); } function makeSelector(metricName: string | undefined, labels: Label[]): string { const allLabels = [...labels]; // we transform the metricName to a label, if it exists if (metricName !== undefined) { allLabels.push({ name: '__name__', value: metricName, op: '=' }); } const allLabelTexts = allLabels.map( (label) => `${label.name}${label.op}"${escapeLabelValueInExactSelector(label.value)}"` ); return `{${allLabelTexts.join(',')}}`; } async function getLabelNames( metric: string | undefined, otherLabels: Label[], dataProvider: DataProvider ): Promise { if (metric === undefined && otherLabels.length === 0) { // if there is no filtering, we have to use a special endpoint return dataProvider.getAllLabelNames(); } else { const selector = makeSelector(metric, otherLabels); const data = await dataProvider.getSeries(selector); const possibleLabelNames = Object.keys(data); // all names from prometheus const usedLabelNames = new Set(otherLabels.map((l) => l.name)); // names used in the query return possibleLabelNames.filter((l) => !usedLabelNames.has(l)); } } async function getLabelNamesForCompletions( metric: string | undefined, suffix: string, triggerOnInsert: boolean, otherLabels: Label[], dataProvider: DataProvider ): Promise { const labelNames = await getLabelNames(metric, otherLabels, dataProvider); return labelNames.map((text) => ({ type: 'LABEL_NAME', label: text, insertText: `${text}${suffix}`, triggerOnInsert, })); } async function getLabelNamesForSelectorCompletions( metric: string | undefined, otherLabels: Label[], dataProvider: DataProvider ): Promise { return getLabelNamesForCompletions(metric, '=', true, otherLabels, dataProvider); } async function getLabelNamesForByCompletions( metric: string | undefined, otherLabels: Label[], dataProvider: DataProvider ): Promise { return getLabelNamesForCompletions(metric, '', false, otherLabels, dataProvider); } async function getLabelValues( metric: string | undefined, labelName: string, otherLabels: Label[], dataProvider: DataProvider ): Promise { if (metric === undefined && otherLabels.length === 0) { // if there is no filtering, we have to use a special endpoint return dataProvider.getLabelValues(labelName); } else { const selector = makeSelector(metric, otherLabels); const data = await dataProvider.getSeries(selector); return data[labelName] ?? []; } } async function getLabelValuesForMetricCompletions( metric: string | undefined, labelName: string, betweenQuotes: boolean, otherLabels: Label[], dataProvider: DataProvider ): Promise { const values = await getLabelValues(metric, labelName, otherLabels, dataProvider); return values.map((text) => ({ type: 'LABEL_VALUE', label: text, insertText: betweenQuotes ? text : `"${text}"`, // FIXME: escaping strange characters? })); } export async function getCompletions(situation: Situation, dataProvider: DataProvider): Promise { switch (situation.type) { case 'IN_DURATION': return DURATION_COMPLETIONS; case 'IN_FUNCTION': return getAllFunctionsAndMetricNamesCompletions(dataProvider); case 'AT_ROOT': { return getAllFunctionsAndMetricNamesCompletions(dataProvider); } case 'EMPTY': { const metricNames = await getAllMetricNamesCompletions(dataProvider); const historyCompletions = await getAllHistoryCompletions(dataProvider); return [...historyCompletions, ...FUNCTION_COMPLETIONS, ...metricNames]; } case 'IN_LABEL_SELECTOR_NO_LABEL_NAME': return getLabelNamesForSelectorCompletions(situation.metricName, situation.otherLabels, dataProvider); case 'IN_GROUPING': return getLabelNamesForByCompletions(situation.metricName, situation.otherLabels, dataProvider); case 'IN_LABEL_SELECTOR_WITH_LABEL_NAME': return getLabelValuesForMetricCompletions( situation.metricName, situation.labelName, situation.betweenQuotes, situation.otherLabels, dataProvider ); default: throw new NeverCaseError(situation); } }