chore(prom): add obfuscated query to tracking (#117165)

* chore(prom): add obfuscated query to tracking

* Fix prettier

* Fix

* prettier
This commit is contained in:
Ivana Huckova 2026-02-03 15:03:14 +01:00 committed by GitHub
parent aefcbed25d
commit 2074778af5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 128 additions and 0 deletions

View file

@ -0,0 +1,71 @@
import { obfuscate } from './tracking';
describe('obfuscate', () => {
it('obfuscates metric name', () => {
expect(obfuscate('http_requests_total')).toEqual('Identifier');
});
it('obfuscates metric name with labels', () => {
expect(obfuscate('http_requests_total{job="grafana"}')).toEqual('Identifier{LabelName=StringLiteral}');
});
it('obfuscates on valid query with rate', () => {
expect(obfuscate('rate(http_requests_total{job="grafana"}[5m])')).toEqual(
'rate(Identifier{LabelName=StringLiteral}[NumberDurationLiteral])'
);
});
it('obfuscates aggregation query', () => {
expect(obfuscate('sum(rate(http_requests_total{job="grafana"}[5m])) by (instance)')).toEqual(
'sum(rate(Identifier{LabelName=StringLiteral}[NumberDurationLiteral])) by (LabelName)'
);
});
it('obfuscates arithmetic operations', () => {
expect(obfuscate('2 + 3')).toEqual('NumberDurationLiteral + NumberDurationLiteral');
});
it('obfuscates binary operations with metrics', () => {
expect(obfuscate('http_requests_total / http_requests_failed')).toEqual('Identifier / Identifier');
});
it('obfuscates query with multiple labels', () => {
expect(obfuscate('up{job="prometheus", instance="localhost:9090"}')).toEqual(
'Identifier{LabelName=StringLiteral, LabelName=StringLiteral}'
);
});
it('does not obfuscate __name__ label', () => {
expect(obfuscate('{__name__="http_requests_total"}')).toEqual('{__name__=StringLiteral}');
});
it('does not obfuscate interval variables', () => {
expect(obfuscate('rate(http_requests_total[$__interval])')).toEqual('rate(Identifier[$__interval])');
});
it('does not obfuscate rate_interval variable', () => {
expect(obfuscate('rate(http_requests_total[$__rate_interval])')).toEqual('rate(Identifier[$__rate_interval])');
});
it('does not obfuscate range variables', () => {
expect(obfuscate('rate(http_requests_total[$__range])')).toEqual('rate(Identifier[$__range])');
expect(obfuscate('rate(http_requests_total[$__range_s])')).toEqual('rate(Identifier[$__range_s])');
expect(obfuscate('rate(http_requests_total[$__range_ms])')).toEqual('rate(Identifier[$__range_ms])');
});
it('obfuscates complex query with histogram_quantile', () => {
expect(
obfuscate('histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="api"}[5m])) by (le))')
).toEqual(
'histogram_quantile(NumberDurationLiteral, sum(rate(Identifier{LabelName=StringLiteral}[NumberDurationLiteral])) by (LabelName))'
);
});
it('obfuscates offset modifier', () => {
expect(obfuscate('http_requests_total offset 5m')).toEqual('Identifier offset NumberDurationLiteral');
});
it('handles empty query', () => {
expect(obfuscate('')).toEqual('');
});
});

View file

@ -1,9 +1,65 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/tracking.ts
import {
Identifier,
LabelName,
NumberDurationLiteral,
NumberDurationLiteralInDurationContext,
parser,
StringLiteral,
} from '@prometheus-io/lezer-promql';
import { CoreApp, DataQueryRequest, DataQueryResponse } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { PromQuery } from './types';
const tagsToObscure = [
StringLiteral,
Identifier,
LabelName,
NumberDurationLiteral,
NumberDurationLiteralInDurationContext,
];
const partsToKeep = [
'__name__',
'__interval',
'__interval_ms',
'__rate_interval',
'__range',
'__range_s',
'__range_ms',
];
export function obfuscate(query: string): string {
const replacements: Array<{ from: number; to: number; replacement: string }> = [];
const tree = parser.parse(query);
tree.iterate({
enter: ({ type, from, to }): false | void => {
const queryPart = query.substring(from, to);
// Skip empty parts, parts to keep, and Grafana variable syntax
if (
queryPart.length === 0 ||
partsToKeep.includes(queryPart) ||
queryPart.startsWith('$__') ||
!tagsToObscure.includes(type.id)
) {
return;
}
// Use consistent name for duration literals
const replacement = type.id === NumberDurationLiteralInDurationContext ? 'NumberDurationLiteral' : type.name;
replacements.push({ from, to, replacement });
},
});
// Apply replacements from end to start to preserve positions
replacements.sort((a, b) => b.from - a.from);
let obfuscatedQuery = query;
for (const { from, to, replacement } of replacements) {
obfuscatedQuery = obfuscatedQuery.substring(0, from) + replacement + obfuscatedQuery.substring(to);
}
return obfuscatedQuery;
}
export function trackQuery(
response: DataQueryResponse,
request: DataQueryRequest<PromQuery> & { targets: PromQuery[] },
@ -24,6 +80,7 @@ export function trackQuery(
has_data: response.data.some((frame) => frame.length > 0),
has_error: response.error !== undefined,
expr: query.expr,
obfuscated_query: obfuscate(query.expr),
format: query.format,
instant: query.instant,
range: query.range,