Loki: Add unwrap with conversion function to builder (#52639)

* Loki: Add unwrap with conversion operator to builder

* Update explain section

* Update test
This commit is contained in:
Ivana Huckova 2022-07-25 12:51:28 +02:00 committed by GitHub
parent 9d6994c565
commit 53b8e528fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 97 additions and 29 deletions

View File

@ -323,16 +323,30 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
{
id: LokiOperationId.Unwrap,
name: 'Unwrap',
params: [{ name: 'Identifier', type: 'string', hideName: true, minWidth: 16, placeholder: 'Label key' }],
defaultParams: [''],
params: [
{ name: 'Identifier', type: 'string', hideName: true, minWidth: 16, placeholder: 'Label key' },
{
name: 'Conversion function',
hideName: true,
type: 'string',
options: ['duration', 'duration_seconds', 'bytes'],
optional: true,
},
],
defaultParams: ['', ''],
alternativesKey: 'format',
category: LokiVisualQueryOperationCategory.Formats,
orderRank: LokiOperationOrder.Unwrap,
renderer: (op, def, innerExpr) => `${innerExpr} | unwrap ${op.params[0]}`,
renderer: (op, def, innerExpr) =>
`${innerExpr} | unwrap ${op.params[1] ? `${op.params[1]}(${op.params[0]})` : op.params[0]}`,
addOperationHandler: addLokiOperation,
explainHandler: (op) => {
let label = String(op.params[0]).length > 0 ? op.params[0] : '<label>';
return `Use the extracted label \`${label}\` as sample values instead of log lines for the subsequent range aggregation.`;
return `Use the extracted label \`${label}\` as sample values instead of log lines for the subsequent range aggregation.${
op.params[1]
? ` Conversion function \`${op.params[1]}\` wrapping \`${label}\` will attempt to convert this label from a specific format (e.g. 3k, 500ms).`
: ''
}`;
},
},
...binaryScalarOperations,

View File

@ -217,7 +217,7 @@ describe('buildVisualQueryFromString', () => {
],
operations: [
{ id: 'logfmt', params: [] },
{ id: 'unwrap', params: ['bytes_processed'] },
{ id: 'unwrap', params: ['bytes_processed', ''] },
{ id: 'sum_over_time', params: ['1m'] },
],
})
@ -238,7 +238,7 @@ describe('buildVisualQueryFromString', () => {
],
operations: [
{ id: 'logfmt', params: [] },
{ id: 'unwrap', params: ['duration'] },
{ id: 'unwrap', params: ['duration', ''] },
{ id: '__label_filter_no_errors', params: [] },
{ id: 'sum_over_time', params: ['1m'] },
],
@ -260,7 +260,7 @@ describe('buildVisualQueryFromString', () => {
],
operations: [
{ id: 'logfmt', params: [] },
{ id: 'unwrap', params: ['duration'] },
{ id: 'unwrap', params: ['duration', ''] },
{ id: '__label_filter', params: ['label', '=', 'value'] },
{ id: 'sum_over_time', params: ['1m'] },
],
@ -268,18 +268,26 @@ describe('buildVisualQueryFromString', () => {
);
});
it('returns error for query with unwrap and conversion operation', () => {
it('parses query with unwrap and conversion function', () => {
const context = buildVisualQueryFromString(
'sum_over_time({app="frontend"} | logfmt | unwrap duration(label) [5m])'
);
expect(context.errors).toEqual([
{
text: 'Unwrap with conversion operator not supported in query builder: | unwrap duration(label)',
from: 40,
to: 64,
parentType: 'LogRangeExpr',
},
]);
expect(context).toEqual(
noErrors({
labels: [
{
op: '=',
value: 'frontend',
label: 'app',
},
],
operations: [
{ id: 'logfmt', params: [] },
{ id: 'unwrap', params: ['label', 'duration'] },
{ id: 'sum_over_time', params: ['5m'] },
],
})
);
});
it('parses metrics query with function', () => {

View File

@ -323,16 +323,21 @@ function handleUnwrapExpr(
}
if (unwrapChild) {
if (unwrapChild?.nextSibling?.type.name === 'ConvOp') {
if (unwrapChild.nextSibling?.type.name === 'ConvOp') {
const convOp = unwrapChild.nextSibling;
const identifier = convOp.nextSibling;
return {
error: 'Unwrap with conversion operator not supported in query builder',
operation: {
id: 'unwrap',
params: [getString(expr, identifier), getString(expr, convOp)],
},
};
}
return {
operation: {
id: 'unwrap',
params: [getString(expr, unwrapChild?.nextSibling)],
params: [getString(expr, unwrapChild?.nextSibling), ''],
},
};
}

View File

@ -1,7 +1,8 @@
import { css } from '@emotion/css';
import React, { ComponentType } from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { AutoSizeInput, Checkbox, Select } from '@grafana/ui';
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
import { AutoSizeInput, Button, Checkbox, Select, Stack, useStyles2 } from '@grafana/ui';
import { QueryBuilderOperationParamDef, QueryBuilderOperationParamEditorProps } from '../shared/types';
@ -63,6 +64,7 @@ function SelectInputParamEditor({
operationIndex,
onChange,
}: QueryBuilderOperationParamEditorProps) {
const styles = useStyles2(getStyles);
let selectOptions = paramDef.options as Array<SelectableValue<any>>;
if (!selectOptions[0]?.label) {
@ -74,14 +76,53 @@ function SelectInputParamEditor({
let valueOption = selectOptions.find((x) => x.value === value) ?? toOption(value as string);
// If we have optional options param and don't have value, we want to render button with which we add optional options.
// This makes it easier to understand what needs to be selected and what is optional.
if (!value && paramDef.optional) {
return (
<div className={styles.optionalParam}>
<Button
size="sm"
variant="secondary"
title={`Add ${paramDef.name}`}
icon="plus"
onClick={() => onChange(index, selectOptions[0].value)}
>
{paramDef.name}
</Button>
</div>
);
}
return (
<Select
id={getOperationParamId(operationIndex, index)}
value={valueOption}
options={selectOptions}
placeholder={paramDef.placeholder}
allowCustomValue={true}
onChange={(value) => onChange(index, value.value!)}
/>
<Stack gap={0.5} direction="row" alignItems="center" wrap={false}>
<Select
id={getOperationParamId(operationIndex, index)}
value={valueOption}
options={selectOptions}
placeholder={paramDef.placeholder}
allowCustomValue={true}
onChange={(value) => onChange(index, value.value!)}
/>
{paramDef.optional && (
<Button
data-testid={`operations.${index}.remove-param`}
size="sm"
fill="text"
icon="times"
variant="secondary"
title={`Remove ${paramDef.name}`}
onClick={() => onChange(index, '')}
/>
)}
</Stack>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
optionalParam: css({
marginTop: theme.spacing(1),
}),
};
};