496 lines
17 KiB
TypeScript
496 lines
17 KiB
TypeScript
import { QueryBuilderOperation, QueryBuilderOperationDefinition } from '@grafana/plugin-ui';
|
|
|
|
import {
|
|
createAggregationOperation,
|
|
createAggregationOperationWithParam,
|
|
createRangeOperation,
|
|
createRangeOperationWithGrouping,
|
|
getLineFilterRenderer,
|
|
isConflictingFilter,
|
|
labelFilterRenderer,
|
|
pipelineRenderer,
|
|
} from './operationUtils';
|
|
import { operationDefinitions } from './operations';
|
|
import { LokiOperationId, LokiVisualQueryOperationCategory } from './types';
|
|
|
|
describe('createRangeOperation', () => {
|
|
it('should create basic range operation without possible grouping', () => {
|
|
expect(createRangeOperation('test_range_operation')).toMatchObject({
|
|
id: 'test_range_operation',
|
|
name: 'Test range operation',
|
|
params: [{ name: 'Range', type: 'string' }],
|
|
defaultParams: ['$__auto'],
|
|
alternativesKey: 'range function',
|
|
category: LokiVisualQueryOperationCategory.RangeFunctions,
|
|
});
|
|
});
|
|
|
|
it('should create basic range operation with possible grouping', () => {
|
|
expect(createRangeOperation('test_range_operation', true)).toMatchObject({
|
|
id: 'test_range_operation',
|
|
name: 'Test range operation',
|
|
params: [
|
|
{ name: 'Range', type: 'string' },
|
|
{
|
|
name: 'By label',
|
|
type: 'string',
|
|
restParam: true,
|
|
optional: true,
|
|
},
|
|
],
|
|
defaultParams: ['$__auto'],
|
|
alternativesKey: 'range function',
|
|
category: LokiVisualQueryOperationCategory.RangeFunctions,
|
|
});
|
|
});
|
|
|
|
it('should create range operation for quantile_over_time', () => {
|
|
expect(createRangeOperation('quantile_over_time', true)).toMatchObject({
|
|
id: 'quantile_over_time',
|
|
name: 'Quantile over time',
|
|
params: [
|
|
{ name: 'Range', type: 'string' },
|
|
{ name: 'Quantile', type: 'number' },
|
|
{ name: 'By label', type: 'string', restParam: true, optional: true },
|
|
],
|
|
defaultParams: ['$__auto', '0.95'],
|
|
alternativesKey: 'range function',
|
|
category: LokiVisualQueryOperationCategory.RangeFunctions,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createRangeOperationWithGrouping', () => {
|
|
it('returns correct operation definitions with overrides and params', () => {
|
|
const operations = createRangeOperationWithGrouping('quantile_over_time');
|
|
expect(operations).toHaveLength(3);
|
|
expect(operations[0]).toMatchObject({
|
|
id: 'quantile_over_time',
|
|
name: 'Quantile over time',
|
|
params: [
|
|
{ name: 'Range', type: 'string' },
|
|
{ name: 'Quantile', type: 'number' },
|
|
{ name: 'By label', type: 'string', restParam: true, optional: true },
|
|
],
|
|
defaultParams: ['$__auto', '0.95'],
|
|
alternativesKey: 'range function',
|
|
category: LokiVisualQueryOperationCategory.RangeFunctions,
|
|
});
|
|
|
|
expect(operations[1]).toMatchObject({
|
|
id: '__quantile_over_time_by',
|
|
name: 'Quantile over time by',
|
|
params: [
|
|
{ name: 'Range', type: 'string' },
|
|
{ name: 'Quantile', type: 'number' },
|
|
{ name: 'Label', type: 'string', restParam: true, optional: true },
|
|
],
|
|
defaultParams: ['$__auto', '0.95', ''],
|
|
alternativesKey: 'range function with grouping',
|
|
category: LokiVisualQueryOperationCategory.RangeFunctions,
|
|
});
|
|
|
|
expect(operations[2]).toMatchObject({
|
|
id: '__quantile_over_time_without',
|
|
name: 'Quantile over time without',
|
|
params: [
|
|
{ name: 'Range', type: 'string' },
|
|
{ name: 'Quantile', type: 'number' },
|
|
{ name: 'Label', type: 'string', restParam: true, optional: true },
|
|
],
|
|
defaultParams: ['$__auto', '0.95', ''],
|
|
alternativesKey: 'range function with grouping',
|
|
category: LokiVisualQueryOperationCategory.RangeFunctions,
|
|
});
|
|
});
|
|
|
|
it('returns correct query string using range operation definitions for quantile_over_time with by grouping', () => {
|
|
const operations = createRangeOperationWithGrouping('quantile_over_time');
|
|
const query = operations[1].renderer(
|
|
{ id: '__quantile_over_time_by', params: ['[5m]', '0.95', 'source', 'place'] },
|
|
operations[1],
|
|
'{job="grafana"}'
|
|
);
|
|
expect(query).toBe('quantile_over_time(0.95, {job="grafana"} [[5m]]) by (source, place)');
|
|
});
|
|
|
|
it('returns correct query string using range operation definitions for quantile_over_time with without grouping', () => {
|
|
const operations = createRangeOperationWithGrouping('quantile_over_time');
|
|
const query = operations[2].renderer(
|
|
{ id: '__quantile_over_time_without', params: ['[$__interval]', '0.91', 'source', 'place'] },
|
|
operations[2],
|
|
'{job="grafana"}'
|
|
);
|
|
expect(query).toBe('quantile_over_time(0.91, {job="grafana"} [[$__interval]]) without (source, place)');
|
|
});
|
|
|
|
it('returns correct query string using range operation definitions for avg_over_time with without grouping', () => {
|
|
const operations = createRangeOperationWithGrouping('avg_over_time');
|
|
const query = operations[2].renderer(
|
|
{ id: '__avg_over_time_without', params: ['[$__interval]', 'source'] },
|
|
operations[2],
|
|
'{job="grafana"}'
|
|
);
|
|
expect(query).toBe('avg_over_time({job="grafana"} [[$__interval]]) without (source)');
|
|
});
|
|
});
|
|
|
|
describe('getLineFilterRenderer', () => {
|
|
const MOCK_MODEL = {
|
|
id: '__line_contains',
|
|
params: ['error'],
|
|
};
|
|
const MOCK_MODEL_INSENSITIVE = {
|
|
id: '__line_contains_case_insensitive',
|
|
params: ['ERrOR'],
|
|
};
|
|
const MOCK_MODEL_BACKTICKS = {
|
|
id: '__line_contains',
|
|
params: ['`error`'],
|
|
};
|
|
|
|
const MOCK_DEF = undefined as unknown as QueryBuilderOperationDefinition;
|
|
|
|
const MOCK_INNER_EXPR = '{job="grafana"}';
|
|
|
|
it('getLineFilterRenderer returns a function', () => {
|
|
const lineFilterRenderer = getLineFilterRenderer('!~');
|
|
expect(typeof lineFilterRenderer).toBe('function');
|
|
});
|
|
|
|
it('lineFilterRenderer returns the correct query for line contains', () => {
|
|
const lineFilterRenderer = getLineFilterRenderer('!~');
|
|
expect(lineFilterRenderer(MOCK_MODEL, MOCK_DEF, MOCK_INNER_EXPR)).toBe('{job="grafana"} !~ `error`');
|
|
});
|
|
|
|
it('lineFilterRenderer returns the correct query for line contains, containing backticks', () => {
|
|
const lineFilterRenderer = getLineFilterRenderer('!~');
|
|
expect(lineFilterRenderer(MOCK_MODEL_BACKTICKS, MOCK_DEF, MOCK_INNER_EXPR)).toBe('{job="grafana"} !~ "`error`"');
|
|
});
|
|
|
|
it('lineFilterRenderer returns the correct query for line contains case insensitive', () => {
|
|
const lineFilterRenderer = getLineFilterRenderer('!~', true);
|
|
expect(lineFilterRenderer(MOCK_MODEL_INSENSITIVE, MOCK_DEF, MOCK_INNER_EXPR)).toBe(
|
|
'{job="grafana"} !~ `(?i)ERrOR`'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('labelFilterRenderer', () => {
|
|
const MOCK_MODEL = { id: '__label_filter', params: ['label', '', 'value'] };
|
|
const MOCK_DEF = undefined as unknown as QueryBuilderOperationDefinition;
|
|
const MOCK_INNER_EXPR = '{job="grafana"}';
|
|
|
|
it.each`
|
|
operator | type | expected
|
|
${'='} | ${'string'} | ${'`value`'}
|
|
${'!='} | ${'string'} | ${'`value`'}
|
|
${'=~'} | ${'string'} | ${'`value`'}
|
|
${'!~'} | ${'string'} | ${'`value`'}
|
|
${'>'} | ${'number'} | ${'value'}
|
|
${'>='} | ${'number'} | ${'value'}
|
|
${'<'} | ${'number'} | ${'value'}
|
|
${'<='} | ${'number'} | ${'value'}
|
|
`("value should be of type '$type' when operator is: $operator", ({ operator, expected }) => {
|
|
MOCK_MODEL.params[1] = operator;
|
|
expect(labelFilterRenderer(MOCK_MODEL, MOCK_DEF, MOCK_INNER_EXPR)).toBe(
|
|
`{job="grafana"} | label ${operator} ${expected}`
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('isConflictingFilter', () => {
|
|
it('should return true if the operation conflict with another label filter', () => {
|
|
const operation = { id: '__label_filter', params: ['abc', '!=', '123'] };
|
|
const queryOperations = [
|
|
{ id: '__label_filter', params: ['abc', '=', '123'] },
|
|
{ id: '__label_filter', params: ['abc', '!=', '123'] },
|
|
];
|
|
expect(isConflictingFilter(operation, queryOperations)).toBe(true);
|
|
});
|
|
|
|
it("should return false if the operation doesn't conflict with another label filter", () => {
|
|
const operation = { id: '__label_filter', params: ['abc', '=', '123'] };
|
|
const queryOperations = [
|
|
{ id: '__label_filter', params: ['abc', '=', '123'] },
|
|
{ id: '__label_filter', params: ['abc', '=', '123'] },
|
|
];
|
|
expect(isConflictingFilter(operation, queryOperations)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('pipelineRenderer', () => {
|
|
it('correctly renders unpack expressions', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Unpack,
|
|
params: [],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Unpack);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | unpack');
|
|
});
|
|
|
|
it('correctly renders unpack expressions', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Unpack,
|
|
params: [],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Unpack);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | unpack');
|
|
});
|
|
|
|
it('correctly renders empty logfmt expression', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Logfmt,
|
|
params: [],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Logfmt);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | logfmt');
|
|
});
|
|
|
|
it('correctly renders logfmt expression', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Logfmt,
|
|
params: [true, false, 'foo', ''],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Logfmt);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | logfmt --strict foo');
|
|
});
|
|
|
|
it('correctly renders logfmt expression with multiple params', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Logfmt,
|
|
params: [true, false, 'foo', 'bar', 'baz'],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Logfmt);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | logfmt --strict foo, bar, baz');
|
|
});
|
|
|
|
it('correctly renders empty json expression', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Json,
|
|
params: [],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Json);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | json');
|
|
});
|
|
|
|
it('correctly renders json expression', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Json,
|
|
params: ['foo', ''],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Json);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | json foo');
|
|
});
|
|
|
|
it('correctly renders json expression with multiple params', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Json,
|
|
params: ['foo', 'bar', 'baz'],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Json);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | json foo, bar, baz');
|
|
});
|
|
|
|
it('correctly renders keep expression', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Keep,
|
|
params: ['foo', ''],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Keep);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | keep foo');
|
|
});
|
|
|
|
it('correctly renders keep expression with multiple params', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Keep,
|
|
params: ['foo', 'bar', 'baz'],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Keep);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | keep foo, bar, baz');
|
|
});
|
|
|
|
it('correctly renders drop expression', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Drop,
|
|
params: ['foo', ''],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Drop);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | drop foo');
|
|
});
|
|
|
|
it('correctly renders drop expression with multiple params', () => {
|
|
const model: QueryBuilderOperation = {
|
|
id: LokiOperationId.Drop,
|
|
params: ['foo', 'bar', 'baz'],
|
|
};
|
|
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Drop);
|
|
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | drop foo, bar, baz');
|
|
});
|
|
});
|
|
|
|
describe('createAggregationOperation', () => {
|
|
it('returns correct aggregation definitions with overrides', () => {
|
|
expect(createAggregationOperation('test_aggregation', { category: 'test_category' })).toMatchObject([
|
|
{
|
|
addOperationHandler: {},
|
|
alternativesKey: 'plain aggregations',
|
|
category: 'test_category',
|
|
defaultParams: [],
|
|
explainHandler: {},
|
|
id: 'test_aggregation',
|
|
name: 'Test aggregation',
|
|
paramChangedHandler: {},
|
|
params: [
|
|
{
|
|
name: 'By label',
|
|
optional: true,
|
|
restParam: true,
|
|
type: 'string',
|
|
},
|
|
],
|
|
renderer: {},
|
|
},
|
|
{
|
|
alternativesKey: 'aggregations by',
|
|
category: 'test_category',
|
|
defaultParams: [''],
|
|
explainHandler: {},
|
|
hideFromList: true,
|
|
id: '__test_aggregation_by',
|
|
name: 'Test aggregation by',
|
|
paramChangedHandler: {},
|
|
params: [
|
|
{
|
|
editor: {},
|
|
name: 'Label',
|
|
optional: true,
|
|
restParam: true,
|
|
type: 'string',
|
|
},
|
|
],
|
|
renderer: {},
|
|
},
|
|
{
|
|
alternativesKey: 'aggregations by',
|
|
category: 'test_category',
|
|
defaultParams: [''],
|
|
explainHandler: {},
|
|
hideFromList: true,
|
|
id: '__test_aggregation_without',
|
|
name: 'Test aggregation without',
|
|
paramChangedHandler: {},
|
|
params: [
|
|
{
|
|
name: 'Label',
|
|
optional: true,
|
|
restParam: true,
|
|
type: 'string',
|
|
},
|
|
],
|
|
renderer: {},
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('createAggregationOperationWithParams', () => {
|
|
it('returns correct aggregation definitions with overrides and params', () => {
|
|
expect(
|
|
createAggregationOperationWithParam(
|
|
'test_aggregation',
|
|
{
|
|
params: [{ name: 'K-value', type: 'number' }],
|
|
defaultParams: [5],
|
|
},
|
|
{ category: 'test_category' }
|
|
)
|
|
).toMatchObject([
|
|
{
|
|
addOperationHandler: {},
|
|
alternativesKey: 'plain aggregations',
|
|
category: 'test_category',
|
|
defaultParams: [5],
|
|
explainHandler: {},
|
|
id: 'test_aggregation',
|
|
name: 'Test aggregation',
|
|
paramChangedHandler: {},
|
|
params: [
|
|
{ name: 'K-value', type: 'number' },
|
|
{ name: 'By label', optional: true, restParam: true, type: 'string' },
|
|
],
|
|
renderer: {},
|
|
},
|
|
{
|
|
alternativesKey: 'aggregations by',
|
|
category: 'test_category',
|
|
defaultParams: [5, ''],
|
|
explainHandler: {},
|
|
hideFromList: true,
|
|
id: '__test_aggregation_by',
|
|
name: 'Test aggregation by',
|
|
paramChangedHandler: {},
|
|
params: [
|
|
{ name: 'K-value', type: 'number' },
|
|
{ editor: {}, name: 'Label', optional: true, restParam: true, type: 'string' },
|
|
],
|
|
renderer: {},
|
|
},
|
|
{
|
|
alternativesKey: 'aggregations by',
|
|
category: 'test_category',
|
|
defaultParams: [5, ''],
|
|
explainHandler: {},
|
|
hideFromList: true,
|
|
id: '__test_aggregation_without',
|
|
name: 'Test aggregation without',
|
|
paramChangedHandler: {},
|
|
params: [
|
|
{ name: 'K-value', type: 'number' },
|
|
{ name: 'Label', optional: true, restParam: true, type: 'string' },
|
|
],
|
|
renderer: {},
|
|
},
|
|
]);
|
|
});
|
|
it('returns correct query string using aggregation definitions with overrides and number type param', () => {
|
|
const def = createAggregationOperationWithParam(
|
|
'test_aggregation',
|
|
{
|
|
params: [{ name: 'K-value', type: 'number' }],
|
|
defaultParams: [5],
|
|
},
|
|
{ category: 'test_category' }
|
|
);
|
|
|
|
const topKByDefinition = def[1];
|
|
expect(
|
|
topKByDefinition.renderer(
|
|
{ id: '__topk_by', params: ['5', 'source', 'place'] },
|
|
def[1],
|
|
'rate({place="luna"} |= `` [5m])'
|
|
)
|
|
).toBe('test_aggregation by(source, place) (5, rate({place="luna"} |= `` [5m]))');
|
|
});
|
|
|
|
it('returns correct query string using aggregation definitions with overrides and string type param', () => {
|
|
const def = createAggregationOperationWithParam(
|
|
'test_aggregation',
|
|
{
|
|
params: [{ name: 'Identifier', type: 'string' }],
|
|
defaultParams: ['count'],
|
|
},
|
|
{ category: 'test_category' }
|
|
);
|
|
|
|
const countValueDefinition = def[1];
|
|
expect(
|
|
countValueDefinition.renderer(
|
|
{ id: 'count_values', params: ['5', 'source', 'place'] },
|
|
def[1],
|
|
'rate({place="luna"} |= `` [5m])'
|
|
)
|
|
).toBe('test_aggregation by(source, place) ("5", rate({place="luna"} |= `` [5m]))');
|
|
});
|
|
});
|