language_utils.test.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import { AbstractLabelOperator, AbstractQuery } from '@grafana/data';
  2. import {
  3. escapeLabelValueInExactSelector,
  4. escapeLabelValueInRegexSelector,
  5. expandRecordingRules,
  6. fixSummariesMetadata,
  7. parseSelector,
  8. toPromLikeQuery,
  9. } from './language_utils';
  10. describe('parseSelector()', () => {
  11. let parsed;
  12. it('returns a clean selector from an empty selector', () => {
  13. parsed = parseSelector('{}', 1);
  14. expect(parsed.selector).toBe('{}');
  15. expect(parsed.labelKeys).toEqual([]);
  16. });
  17. it('returns a clean selector from an unclosed selector', () => {
  18. const parsed = parseSelector('{foo');
  19. expect(parsed.selector).toBe('{}');
  20. });
  21. it('returns the selector sorted by label key', () => {
  22. parsed = parseSelector('{foo="bar"}');
  23. expect(parsed.selector).toBe('{foo="bar"}');
  24. expect(parsed.labelKeys).toEqual(['foo']);
  25. parsed = parseSelector('{foo="bar",baz="xx"}');
  26. expect(parsed.selector).toBe('{baz="xx",foo="bar"}');
  27. });
  28. it('returns a clean selector from an incomplete one', () => {
  29. parsed = parseSelector('{foo}');
  30. expect(parsed.selector).toBe('{}');
  31. parsed = parseSelector('{foo="bar",baz}');
  32. expect(parsed.selector).toBe('{foo="bar"}');
  33. parsed = parseSelector('{foo="bar",baz="}');
  34. expect(parsed.selector).toBe('{foo="bar"}');
  35. // Cursor in value area counts as incomplete
  36. parsed = parseSelector('{foo="bar",baz=""}', 16);
  37. expect(parsed.selector).toBe('{foo="bar"}');
  38. parsed = parseSelector('{foo="bar",baz="4"}', 17);
  39. expect(parsed.selector).toBe('{foo="bar"}');
  40. });
  41. it('throws if not inside a selector', () => {
  42. expect(() => parseSelector('foo{}', 0)).toThrow();
  43. expect(() => parseSelector('foo{} + bar{}', 5)).toThrow();
  44. });
  45. it('returns the selector nearest to the cursor offset', () => {
  46. expect(() => parseSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow();
  47. parsed = parseSelector('{foo="bar"} + {foo="bar"}', 1);
  48. expect(parsed.selector).toBe('{foo="bar"}');
  49. parsed = parseSelector('{foo="bar"} + {baz="xx"}', 1);
  50. expect(parsed.selector).toBe('{foo="bar"}');
  51. parsed = parseSelector('{baz="xx"} + {foo="bar"}', 16);
  52. expect(parsed.selector).toBe('{foo="bar"}');
  53. });
  54. it('returns a selector with metric if metric is given', () => {
  55. parsed = parseSelector('bar{foo}', 4);
  56. expect(parsed.selector).toBe('{__name__="bar"}');
  57. parsed = parseSelector('baz{foo="bar"}', 13);
  58. expect(parsed.selector).toBe('{__name__="baz",foo="bar"}');
  59. parsed = parseSelector('bar:metric:1m{}', 14);
  60. expect(parsed.selector).toBe('{__name__="bar:metric:1m"}');
  61. });
  62. });
  63. describe('fixSummariesMetadata', () => {
  64. const synthetics = {
  65. ALERTS: {
  66. type: 'counter',
  67. help: 'Time series showing pending and firing alerts. The sample value is set to 1 as long as the alert is in the indicated active (pending or firing) state.',
  68. },
  69. };
  70. it('returns only synthetics on empty metadata', () => {
  71. expect(fixSummariesMetadata({})).toEqual({ ...synthetics });
  72. });
  73. it('returns unchanged metadata if no summary is present', () => {
  74. const metadataRaw = {
  75. foo: [{ type: 'not_a_summary', help: 'foo help' }],
  76. };
  77. const metadata = {
  78. foo: { type: 'not_a_summary', help: 'foo help' },
  79. };
  80. expect(fixSummariesMetadata(metadataRaw)).toEqual({ ...metadata, ...synthetics });
  81. });
  82. it('returns metadata with added count and sum for a summary', () => {
  83. const metadata = {
  84. foo: [{ type: 'not_a_summary', help: 'foo help' }],
  85. bar: [{ type: 'summary', help: 'bar help' }],
  86. };
  87. const expected = {
  88. foo: { type: 'not_a_summary', help: 'foo help' },
  89. bar: { type: 'summary', help: 'bar help' },
  90. bar_count: { type: 'counter', help: 'Count of events that have been observed for the base metric (bar help)' },
  91. bar_sum: { type: 'counter', help: 'Total sum of all observed values for the base metric (bar help)' },
  92. };
  93. expect(fixSummariesMetadata(metadata)).toEqual({ ...expected, ...synthetics });
  94. });
  95. it('returns metadata with added bucket/count/sum for a histogram', () => {
  96. const metadata = {
  97. foo: [{ type: 'not_a_histogram', help: 'foo help' }],
  98. bar: [{ type: 'histogram', help: 'bar help' }],
  99. };
  100. const expected = {
  101. foo: { type: 'not_a_histogram', help: 'foo help' },
  102. bar: { type: 'histogram', help: 'bar help' },
  103. bar_bucket: { type: 'counter', help: 'Cumulative counters for the observation buckets (bar help)' },
  104. bar_count: {
  105. type: 'counter',
  106. help: 'Count of events that have been observed for the histogram metric (bar help)',
  107. },
  108. bar_sum: { type: 'counter', help: 'Total sum of all observed values for the histogram metric (bar help)' },
  109. };
  110. expect(fixSummariesMetadata(metadata)).toEqual({ ...expected, ...synthetics });
  111. });
  112. });
  113. describe('expandRecordingRules()', () => {
  114. it('returns query w/o recording rules as is', () => {
  115. expect(expandRecordingRules('metric', {})).toBe('metric');
  116. expect(expandRecordingRules('metric + metric', {})).toBe('metric + metric');
  117. expect(expandRecordingRules('metric{}', {})).toBe('metric{}');
  118. });
  119. it('does not modify recording rules name in label values', () => {
  120. expect(expandRecordingRules('{__name__="metric"} + bar', { metric: 'foo', bar: 'super' })).toBe(
  121. '{__name__="metric"} + super'
  122. );
  123. });
  124. it('returns query with expanded recording rules', () => {
  125. expect(expandRecordingRules('metric', { metric: 'foo' })).toBe('foo');
  126. expect(expandRecordingRules('metric + metric', { metric: 'foo' })).toBe('foo + foo');
  127. expect(expandRecordingRules('metric{}', { metric: 'foo' })).toBe('foo{}');
  128. expect(expandRecordingRules('metric[]', { metric: 'foo' })).toBe('foo[]');
  129. expect(expandRecordingRules('metric + foo', { metric: 'foo', foo: 'bar' })).toBe('foo + bar');
  130. });
  131. it('returns query with labels with expanded recording rules', () => {
  132. expect(
  133. expandRecordingRules('metricA{label1="value1"} / metricB{label2="value2"}', { metricA: 'fooA', metricB: 'fooB' })
  134. ).toBe('fooA{label1="value1"} / fooB{label2="value2"}');
  135. expect(
  136. expandRecordingRules('metricA{label1="value1",label2="value,2"}', {
  137. metricA: 'rate(fooA[])',
  138. })
  139. ).toBe('rate(fooA{label1="value1", label2="value,2"}[])');
  140. expect(
  141. expandRecordingRules('metricA{label1="value1"} / metricB{label2="value2"}', {
  142. metricA: 'rate(fooA[])',
  143. metricB: 'rate(fooB[])',
  144. })
  145. ).toBe('rate(fooA{label1="value1"}[])/ rate(fooB{label2="value2"}[])');
  146. expect(
  147. expandRecordingRules('metricA{label1="value1",label2="value2"} / metricB{label3="value3"}', {
  148. metricA: 'rate(fooA[])',
  149. metricB: 'rate(fooB[])',
  150. })
  151. ).toBe('rate(fooA{label1="value1", label2="value2"}[])/ rate(fooB{label3="value3"}[])');
  152. });
  153. });
  154. describe('escapeLabelValueInExactSelector()', () => {
  155. it('handles newline characters', () => {
  156. expect(escapeLabelValueInExactSelector('t\nes\nt')).toBe('t\\nes\\nt');
  157. });
  158. it('handles backslash characters', () => {
  159. expect(escapeLabelValueInExactSelector('t\\es\\t')).toBe('t\\\\es\\\\t');
  160. });
  161. it('handles double-quote characters', () => {
  162. expect(escapeLabelValueInExactSelector('t"es"t')).toBe('t\\"es\\"t');
  163. });
  164. it('handles all together', () => {
  165. expect(escapeLabelValueInExactSelector('t\\e"st\nl\nab"e\\l')).toBe('t\\\\e\\"st\\nl\\nab\\"e\\\\l');
  166. });
  167. });
  168. describe('escapeLabelValueInRegexSelector()', () => {
  169. it('handles newline characters', () => {
  170. expect(escapeLabelValueInRegexSelector('t\nes\nt')).toBe('t\\nes\\nt');
  171. });
  172. it('handles backslash characters', () => {
  173. expect(escapeLabelValueInRegexSelector('t\\es\\t')).toBe('t\\\\\\\\es\\\\\\\\t');
  174. });
  175. it('handles double-quote characters', () => {
  176. expect(escapeLabelValueInRegexSelector('t"es"t')).toBe('t\\"es\\"t');
  177. });
  178. it('handles regex-meaningful characters', () => {
  179. expect(escapeLabelValueInRegexSelector('t+es$t')).toBe('t\\\\+es\\\\$t');
  180. });
  181. it('handles all together', () => {
  182. expect(escapeLabelValueInRegexSelector('t\\e"s+t\nl\n$ab"e\\l')).toBe(
  183. 't\\\\\\\\e\\"s\\\\+t\\nl\\n\\\\$ab\\"e\\\\\\\\l'
  184. );
  185. });
  186. });
  187. describe('toPromLikeQuery', () => {
  188. it('export abstract query to PromQL-like query', () => {
  189. const abstractQuery: AbstractQuery = {
  190. refId: 'bar',
  191. labelMatchers: [
  192. { name: 'label1', operator: AbstractLabelOperator.Equal, value: 'value1' },
  193. { name: 'label2', operator: AbstractLabelOperator.NotEqual, value: 'value2' },
  194. { name: 'label3', operator: AbstractLabelOperator.EqualRegEx, value: 'value3' },
  195. { name: 'label4', operator: AbstractLabelOperator.NotEqualRegEx, value: 'value4' },
  196. ],
  197. };
  198. expect(toPromLikeQuery(abstractQuery)).toMatchObject({
  199. refId: 'bar',
  200. expr: '{label1="value1", label2!="value2", label3=~"value3", label4!~"value4"}',
  201. range: true,
  202. });
  203. });
  204. });