Files
grafana/public/app/plugins/datasource/influxdb/influxql_query_builder.ts
Andreas Christou 8288e947a8 InfluxDB: Add an optional time range filter for tag queries in the query panel autocompleteInflux tag filter (#107195)
* InfluxDB: Add an optional time range filter for tag queries in the query panel autocomplete

* Betterer updates

* Update test

---------

Co-authored-by: Nikolay Tsvetkov <nikolay.cvetkov@gmail.com>
2025-06-25 21:37:02 +00:00

213 lines
5.9 KiB
TypeScript

import { reduce } from 'lodash';
import { escapeRegex, ScopedVars } from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime';
import { DEFAULT_POLICY, InfluxQueryTag, MetadataQueryType } from './types';
export const buildMetadataQuery = (params: {
type: MetadataQueryType;
templateService: TemplateSrv;
scopedVars?: ScopedVars;
database?: string;
measurement?: string;
retentionPolicy?: string;
tags?: InfluxQueryTag[];
withKey?: string;
withMeasurementFilter?: string;
withTimeFilter?: string;
}): string => {
let query = '';
let {
type,
templateService,
scopedVars,
database,
measurement,
retentionPolicy,
tags,
withKey,
withMeasurementFilter,
withTimeFilter,
} = params;
switch (type) {
case 'RETENTION_POLICIES':
return 'SHOW RETENTION POLICIES on "' + database + '"';
case 'FIELDS':
if (!measurement || measurement === '') {
return 'SHOW FIELD KEYS';
}
// If there is a measurement and it is not empty string
if (measurement && !measurement.match(/^\/.*\/|^$/)) {
measurement = '"' + measurement + '"';
if (retentionPolicy && retentionPolicy !== DEFAULT_POLICY) {
retentionPolicy = '"' + retentionPolicy + '"';
measurement = retentionPolicy + '.' + measurement;
}
}
return 'SHOW FIELD KEYS FROM ' + measurement;
case 'TAG_KEYS':
query = 'SHOW TAG KEYS';
break;
case 'TAG_VALUES':
query = 'SHOW TAG VALUES';
break;
case 'MEASUREMENTS':
query = 'SHOW MEASUREMENTS';
if (withMeasurementFilter) {
// we do a case-insensitive regex-based lookup
query += ' WITH MEASUREMENT =~ /(?i)' + escapeRegex(withMeasurementFilter) + '/';
}
break;
default:
return query;
}
if (measurement) {
if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) {
measurement = '"' + measurement + '"';
}
if (retentionPolicy && retentionPolicy !== DEFAULT_POLICY) {
retentionPolicy = '"' + retentionPolicy + '"';
measurement = retentionPolicy + '.' + measurement;
}
if (measurement !== '') {
query += ' FROM ' + measurement;
}
}
if (withKey) {
let keyIdentifier = withKey;
if (keyIdentifier.endsWith('::tag')) {
keyIdentifier = keyIdentifier.slice(0, -5);
}
query += ' WITH KEY = "' + keyIdentifier + '"';
}
let whereConditions: string[] = [];
if (tags && tags.length > 0) {
whereConditions = reduce<InfluxQueryTag, string[]>(
tags,
(memo, tag) => {
// do not add a condition for the key we want to explore for
if (tag.key && tag.key === withKey) {
return memo;
}
// value operators not supported in these types of queries
if (tag.operator === '>' || tag.operator === '<') {
return memo;
}
memo.push(renderTagCondition(tag, memo.length, templateService, scopedVars, true));
return memo;
},
[]
);
}
let shouldUseTime = isValidTimeFilter(withTimeFilter) && type !== 'MEASUREMENTS';
if (whereConditions.length > 0) {
query += ' WHERE ' + whereConditions.join(' ') + (shouldUseTime ? ' AND time > now() - ' + withTimeFilter : '');
} else {
query += shouldUseTime ? ' WHERE time > now() - ' + withTimeFilter : '';
}
if (type === 'MEASUREMENTS') {
query += ' LIMIT 100';
//Solve issue #2524 by limiting the number of measurements returned
//LIMIT must be after WITH MEASUREMENT and WHERE clauses
//This also could be used for TAG KEYS and TAG VALUES, if desired
}
return query;
};
// Function to validate the provided time filter value
export function isValidTimeFilter(value?: string): boolean {
if (!value || typeof value !== 'string') {
return false;
}
// Normalize microseconds unit
const normalizedValue = value.replace(/µ/g, 'u');
const validUnits = new Set(['ns', 'u', 'ms', 's', 'm', 'h', 'd', 'w']);
const pattern = /(\d+)(ns|u|ms|s|m|h|d|w)/g;
const usedUnits = new Set<string>();
let match: RegExpExecArray | null;
let totalLength = 0;
while ((match = pattern.exec(normalizedValue)) !== null) {
const [fullMatch, numberPart, unit] = match;
totalLength += fullMatch.length;
if (!validUnits.has(unit) || usedUnits.has(unit) || numberPart.startsWith('0')) {
return false;
}
usedUnits.add(unit);
}
return totalLength === normalizedValue.length;
}
// A merge of query_builder/renderTagCondition and influx_query_model/renderTagCondition
export function renderTagCondition(
tag: InfluxQueryTag,
index: number,
templateSrv: TemplateSrv,
scopedVars?: ScopedVars,
interpolate?: boolean
) {
let str = '';
let operator = tag.operator;
let value = tag.value;
if (index > 0) {
str = (tag.condition || 'AND') + ' ';
}
if (!operator) {
if (/^\/.*\/$/.test(tag.value)) {
operator = '=~';
} else {
operator = '=';
}
}
// quote value unless regex or empty-string
// Influx versions before 0.13 had inconsistent requirements on if (numeric) tags are quoted or not.
if (value === '' || (operator !== '=~' && operator !== '!~')) {
value = "'" + value.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") + "'";
}
// quote value unless regex
if (operator !== '=~' && operator !== '!~') {
if (interpolate) {
value = templateSrv.replace(value, scopedVars);
} else if (operator !== '>' && operator !== '<') {
value = "'" + value.replace(/\\/g, '\\\\').replace(/\'/g, "\\'") + "'";
}
} else if (interpolate) {
value = templateSrv.replace(value, scopedVars, 'regex');
}
let escapedKey = `"${tag.key}"`;
if (tag.key.endsWith('::tag')) {
escapedKey = `"${tag.key.slice(0, -5)}"::tag`;
}
if (tag.key.endsWith('::field')) {
escapedKey = `"${tag.key.slice(0, -7)}"::field`;
}
return str + escapedKey + ' ' + operator + ' ' + value;
}