Alerting: Fix backtick handling when hashing queries (#108757)
Update the hashQuery algorithm to handle more formatting edge cases
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`hashRulerRule should hash alerting rule 1`] = `"7317348"`;
|
||||
exports[`hashRulerRule should hash alerting rule 1`] = `"852037155"`;
|
||||
|
||||
exports[`hashRulerRule should hash recording rules 1`] = `"-447747460"`;
|
||||
exports[`hashRulerRule should hash recording rules 1`] = `"914562864"`;
|
||||
|
||||
@@ -13,7 +13,15 @@ import {
|
||||
RulerRecordingRuleDTO,
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { equal, getRuleIdFromPathname, hashRule, hashRulerRule, parse, stringifyIdentifier } from './rule-id';
|
||||
import {
|
||||
equal,
|
||||
getRuleIdFromPathname,
|
||||
hashQuery,
|
||||
hashRule,
|
||||
hashRulerRule,
|
||||
parse,
|
||||
stringifyIdentifier,
|
||||
} from './rule-id';
|
||||
|
||||
const alertingRule = {
|
||||
prom: {
|
||||
@@ -258,3 +266,111 @@ describe('useRuleIdFromPathname', () => {
|
||||
expect(result.current).toBe('abc%25def');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hashQuery', () => {
|
||||
it('should produce the same hash for queries with different whitespace formatting', () => {
|
||||
const query1 = `sum by (client,origin,destination,met_val)(
|
||||
sum_over_time(
|
||||
{client=~"PRU|RVSI"}
|
||||
)
|
||||
)`;
|
||||
const query2 = `sum by (client,origin,destination,met_val)(sum_over_time({client=~"PRU|RVSI"}))`;
|
||||
|
||||
expect(hashQuery(query1)).toBe(hashQuery(query2));
|
||||
});
|
||||
|
||||
it('should produce the same hash for queries with and without outer parentheses', () => {
|
||||
const query1 = `sum by (client)(rate(requests_total[5m]))`;
|
||||
const query2 = `(sum by (client)(rate(requests_total[5m])))`;
|
||||
|
||||
expect(hashQuery(query1)).toBe(hashQuery(query2));
|
||||
});
|
||||
|
||||
it('should produce the same hash for queries with different quote types in label formats', () => {
|
||||
const query1 = `label_format origin=\`{{.app_host}}\``;
|
||||
const query2 = `label_format origin="{{.app_host}}"`;
|
||||
|
||||
expect(hashQuery(query1)).toBe(hashQuery(query2));
|
||||
});
|
||||
|
||||
it('should produce the same hash for queries with escaped vs unescaped quotes', () => {
|
||||
const query1 = `label_format met_val=\`{{"REQ_SENT"}}\``;
|
||||
const query2 = `label_format met_val="{{\"REQ_SENT\"}}"`;
|
||||
|
||||
expect(hashQuery(query1)).toBe(hashQuery(query2));
|
||||
});
|
||||
|
||||
it('should handle complex Loki recording rules with all formatting differences', () => {
|
||||
const query1 = `sum by (client,origin,destination,metric_type)(
|
||||
sum_over_time(
|
||||
{client=~"FOO|BAR|BAZ", service_name="app_sessions"}
|
||||
|= "server"
|
||||
|= "component"
|
||||
| logfmt
|
||||
| label_format origin=\`{{.host_name}}\`
|
||||
| label_format destination=\`{{.component_name}}\`
|
||||
| label_format metric_type=\`{{"REQUEST_COUNT"}}\`
|
||||
| keep client,destination,origin,metric_type,response_time
|
||||
| unwrap response_time
|
||||
[5m])
|
||||
) > 0`;
|
||||
|
||||
const query2 = `(sum by (client,origin,destination,metric_type)(sum_over_time({client=~"FOO|BAR|BAZ", service_name="app_sessions"} |= "server" |= "component" | logfmt | label_format origin="{{.host_name}}" | label_format destination="{{.component_name}}" | label_format metric_type="{{\"REQUEST_COUNT\"}}" | keep client,destination,origin,metric_type,response_time | unwrap response_time[5m])) > 0)`;
|
||||
|
||||
expect(hashQuery(query1)).toBe(hashQuery(query2));
|
||||
});
|
||||
|
||||
it('should produce the same hash for queries with reordered label matchers', () => {
|
||||
const query1 = `{job="prometheus", instance="localhost:9090"}`;
|
||||
const query2 = `{instance="localhost:9090", job="prometheus"}`;
|
||||
|
||||
expect(hashQuery(query1)).toBe(hashQuery(query2));
|
||||
});
|
||||
|
||||
it('should handle multiple types of brackets and quotes', () => {
|
||||
const query1 = `rate(http_requests_total{method="GET"}[5m])`;
|
||||
const query2 = `rate(http_requests_total{method=\`GET\`}[5m])`;
|
||||
|
||||
expect(hashQuery(query1)).toBe(hashQuery(query2));
|
||||
});
|
||||
|
||||
it('should normalize backslashes properly', () => {
|
||||
const query1 = `label_format path="{{.file_path}}"`;
|
||||
const query2 = `label_format path="{{\.file_path}}"`;
|
||||
|
||||
expect(hashQuery(query1)).toBe(hashQuery(query2));
|
||||
});
|
||||
|
||||
it('should handle empty queries', () => {
|
||||
expect(hashQuery('')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle queries with only parentheses', () => {
|
||||
expect(hashQuery('()')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle complex nested parentheses and brackets', () => {
|
||||
const query1 = `((sum(rate(requests[5m]))))`;
|
||||
const query2 = `sum(rate(requests[5m]))`;
|
||||
|
||||
expect(hashQuery(query1)).toBe(hashQuery(query2));
|
||||
});
|
||||
|
||||
it('should handle mixed quote escaping scenarios', () => {
|
||||
const query1 = `label_format msg=\`{{"error: \\"timeout\\""}}\``;
|
||||
const query2 = `label_format msg="{{\"error: \\\"timeout\\\"\"}}"`;
|
||||
|
||||
expect(hashQuery(query1)).toBe(hashQuery(query2));
|
||||
});
|
||||
|
||||
it('should produce consistent results for character sorting', () => {
|
||||
const query1 = `abc{x="1",y="2"}`;
|
||||
const query2 = `abc{y="2",x="1"}`;
|
||||
|
||||
const hash1 = hashQuery(query1);
|
||||
const hash2 = hashQuery(query2);
|
||||
|
||||
expect(hash1).toBe(hash2);
|
||||
expect(hash1).toBe(hash1.split('').sort().join(''));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -311,9 +311,21 @@ export function hashQuery(query: string) {
|
||||
if (query.length > 1 && query[0] === '(' && query[query.length - 1] === ')') {
|
||||
query = query.slice(1, -1);
|
||||
}
|
||||
|
||||
// whitespace could be added or removed
|
||||
query = query.replace(/\s|\n/g, '');
|
||||
// labels matchers can be reordered, so sort the enitre string, esentially comparing just the character counts
|
||||
|
||||
// normalize escaped quotes in template strings like {{\"REQ_SENT\"}} -> {{"REQ_SENT"}}
|
||||
query = query.replace(/\\"/g, '"');
|
||||
|
||||
// normalize backtick template strings to double quotes for consistency
|
||||
// Convert `{{.field}}` to "{{.field}}"
|
||||
query = query.replace(/`([^`]*)`/g, '"$1"');
|
||||
|
||||
// remove quotes, brackets, parentheses, backslashes, and backticks
|
||||
query = query.replace(/['"()\[\]\\`]/g, '');
|
||||
|
||||
// labels matchers can be reordered, so sort the entire string, essentially comparing just the character counts
|
||||
return query.split('').sort().join('');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user