mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Loki: Add hints for level-like labels (#52414)
* Loki: Add hint for level-like label * Add test to addToQuery * Add tests * Update tests * Update copy * Get log position in metrics query and add test * Update
This commit is contained in:
parent
9f81b3dcc6
commit
772b6396f5
@ -1,3 +1,5 @@
|
|||||||
|
import { sortBy } from 'lodash';
|
||||||
|
|
||||||
import { parser } from '@grafana/lezer-logql';
|
import { parser } from '@grafana/lezer-logql';
|
||||||
|
|
||||||
import { QueryBuilderLabelFilter } from '../prometheus/querybuilder/shared/types';
|
import { QueryBuilderLabelFilter } from '../prometheus/querybuilder/shared/types';
|
||||||
@ -76,6 +78,18 @@ export function addNoPipelineErrorToQuery(query: string): string {
|
|||||||
return addFilterAsLabelFilter(query, parserPositions, filter);
|
return addFilterAsLabelFilter(query, parserPositions, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds label format to existing query. Useful for query modification for hints.
|
||||||
|
* It uses LogQL parser to find log query and add label format at the end.
|
||||||
|
*
|
||||||
|
* @param query
|
||||||
|
* @param labelFormat
|
||||||
|
*/
|
||||||
|
export function addLabelFormatToQuery(query: string, labelFormat: { originalLabel: string; renameTo: string }): string {
|
||||||
|
const logQueryPositions = getLogQueryPositions(query);
|
||||||
|
return addLabelFormat(query, logQueryPositions, labelFormat);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the string and get all Selector positions in the query together with parsed representation of the
|
* Parse the string and get all Selector positions in the query together with parsed representation of the
|
||||||
* selector.
|
* selector.
|
||||||
@ -149,6 +163,50 @@ function getLineFiltersPositions(query: string): Position[] {
|
|||||||
return positions;
|
return positions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the string and get all Log query positions in the query.
|
||||||
|
* @param query
|
||||||
|
*/
|
||||||
|
function getLogQueryPositions(query: string): Position[] {
|
||||||
|
const tree = parser.parse(query);
|
||||||
|
const positions: Position[] = [];
|
||||||
|
tree.iterate({
|
||||||
|
enter: (type, from, to, get): false | void => {
|
||||||
|
if (type.name === 'LogExpr') {
|
||||||
|
positions.push({ from, to });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a case in metrics query
|
||||||
|
if (type.name === 'LogRangeExpr') {
|
||||||
|
// Unfortunately, LogRangeExpr includes both log and non-log (e.g. Duration/Range/...) parts of query.
|
||||||
|
// We get position of all log-parts within LogRangeExpr: Selector, PipelineExpr and UnwrapExpr.
|
||||||
|
const logPartsPositions: Position[] = [];
|
||||||
|
const selector = get().getChild('Selector');
|
||||||
|
if (selector) {
|
||||||
|
logPartsPositions.push({ from: selector.from, to: selector.to });
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipeline = get().getChild('PipelineExpr');
|
||||||
|
if (pipeline) {
|
||||||
|
logPartsPositions.push({ from: pipeline.from, to: pipeline.to });
|
||||||
|
}
|
||||||
|
|
||||||
|
const unwrap = get().getChild('UnwrapExpr');
|
||||||
|
if (unwrap) {
|
||||||
|
logPartsPositions.push({ from: unwrap.from, to: unwrap.to });
|
||||||
|
}
|
||||||
|
|
||||||
|
// We sort them and then pick "from" from first position and "to" from last position.
|
||||||
|
const sorted = sortBy(logPartsPositions, (position) => position.to);
|
||||||
|
positions.push({ from: sorted[0].from, to: sorted[sorted.length - 1].to });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
function toLabelFilter(key: string, value: string, operator: string): QueryBuilderLabelFilter {
|
function toLabelFilter(key: string, value: string, operator: string): QueryBuilderLabelFilter {
|
||||||
// We need to make sure that we convert the value back to string because it may be a number
|
// We need to make sure that we convert the value back to string because it may be a number
|
||||||
return { label: key, op: operator, value };
|
return { label: key, op: operator, value };
|
||||||
@ -244,6 +302,35 @@ function addParser(query: string, queryPartPositions: Position[], parser: string
|
|||||||
return newQuery;
|
return newQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add filter as label filter after the parsers
|
||||||
|
* @param query
|
||||||
|
* @param logQueryPositions
|
||||||
|
* @param labelFormat
|
||||||
|
*/
|
||||||
|
function addLabelFormat(
|
||||||
|
query: string,
|
||||||
|
logQueryPositions: Position[],
|
||||||
|
labelFormat: { originalLabel: string; renameTo: string }
|
||||||
|
): string {
|
||||||
|
let newQuery = '';
|
||||||
|
let prev = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < logQueryPositions.length; i++) {
|
||||||
|
// This is basically just doing splice on a string for each matched vector selector.
|
||||||
|
const match = logQueryPositions[i];
|
||||||
|
const isLast = i === logQueryPositions.length - 1;
|
||||||
|
|
||||||
|
const start = query.substring(prev, match.to);
|
||||||
|
const end = isLast ? query.substring(match.to) : '';
|
||||||
|
|
||||||
|
const labelFilter = ` | label_format ${labelFormat.renameTo}=${labelFormat.originalLabel}`;
|
||||||
|
newQuery += start + labelFilter + end;
|
||||||
|
prev = match.to;
|
||||||
|
}
|
||||||
|
return newQuery;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if label exists in the list of labels but ignore the operator.
|
* Check if label exists in the list of labels but ignore the operator.
|
||||||
* @param labels
|
* @param labels
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { addLabelToQuery, addNoPipelineErrorToQuery, addParserToQuery } from './addToQuery';
|
import { addLabelFormatToQuery, addLabelToQuery, addNoPipelineErrorToQuery, addParserToQuery } from './addToQuery';
|
||||||
|
|
||||||
describe('addLabelToQuery()', () => {
|
describe('addLabelToQuery()', () => {
|
||||||
it('should add label to simple query', () => {
|
it('should add label to simple query', () => {
|
||||||
@ -193,3 +193,43 @@ describe('addNoPipelineErrorToQuery', () => {
|
|||||||
expect(addNoPipelineErrorToQuery('{job="grafana"} |="no parser"')).toBe('{job="grafana"} |="no parser"');
|
expect(addNoPipelineErrorToQuery('{job="grafana"} |="no parser"')).toBe('{job="grafana"} |="no parser"');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('addLabelFormatToQuery', () => {
|
||||||
|
it('should add label format at the end of log query when parser', () => {
|
||||||
|
expect(addLabelFormatToQuery('{job="grafana"} | logfmt', { originalLabel: 'lvl', renameTo: 'level' })).toBe(
|
||||||
|
'{job="grafana"} | logfmt | label_format level=lvl'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add label format at the end of log query when no parser', () => {
|
||||||
|
expect(addLabelFormatToQuery('{job="grafana"}', { originalLabel: 'lvl', renameTo: 'level' })).toBe(
|
||||||
|
'{job="grafana"} | label_format level=lvl'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add label format at the end of log query when more label parser', () => {
|
||||||
|
expect(
|
||||||
|
addLabelFormatToQuery('{job="grafana"} | logfmt | label_format a=b', { originalLabel: 'lvl', renameTo: 'level' })
|
||||||
|
).toBe('{job="grafana"} | logfmt | label_format a=b | label_format level=lvl');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add label format at the end of log query part of metrics query', () => {
|
||||||
|
expect(
|
||||||
|
addLabelFormatToQuery('rate({job="grafana"} | logfmt | label_format a=b [5m])', {
|
||||||
|
originalLabel: 'lvl',
|
||||||
|
renameTo: 'level',
|
||||||
|
})
|
||||||
|
).toBe('rate({job="grafana"} | logfmt | label_format a=b | label_format level=lvl [5m])');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add label format at the end of multiple log query part of metrics query', () => {
|
||||||
|
expect(
|
||||||
|
addLabelFormatToQuery(
|
||||||
|
'rate({job="grafana"} | logfmt | label_format a=b [5m]) + rate({job="grafana"} | logfmt | label_format a=b [5m])',
|
||||||
|
{ originalLabel: 'lvl', renameTo: 'level' }
|
||||||
|
)
|
||||||
|
).toBe(
|
||||||
|
'rate({job="grafana"} | logfmt | label_format a=b | label_format level=lvl [5m]) + rate({job="grafana"} | logfmt | label_format a=b | label_format level=lvl [5m])'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -44,7 +44,7 @@ import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_sr
|
|||||||
import { serializeParams } from '../../../core/utils/fetch';
|
import { serializeParams } from '../../../core/utils/fetch';
|
||||||
import { renderLegendFormat } from '../prometheus/legend';
|
import { renderLegendFormat } from '../prometheus/legend';
|
||||||
|
|
||||||
import { addLabelToQuery, addNoPipelineErrorToQuery, addParserToQuery } from './addToQuery';
|
import { addLabelFormatToQuery, addLabelToQuery, addNoPipelineErrorToQuery, addParserToQuery } from './addToQuery';
|
||||||
import { transformBackendResult } from './backendResultTransformer';
|
import { transformBackendResult } from './backendResultTransformer';
|
||||||
import { LokiAnnotationsQueryEditor } from './components/AnnotationsQueryEditor';
|
import { LokiAnnotationsQueryEditor } from './components/AnnotationsQueryEditor';
|
||||||
import LanguageProvider from './language_provider';
|
import LanguageProvider from './language_provider';
|
||||||
@ -358,7 +358,7 @@ export class LokiDatasource
|
|||||||
expr: query.expr,
|
expr: query.expr,
|
||||||
queryType: LokiQueryType.Range,
|
queryType: LokiQueryType.Range,
|
||||||
refId: 'log-samples',
|
refId: 'log-samples',
|
||||||
maxLines: 50,
|
maxLines: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
// For samples, we use defaultTimeRange (now-6h/now) and limit od 10 lines so queries are small and fast
|
// For samples, we use defaultTimeRange (now-6h/now) and limit od 10 lines so queries are small and fast
|
||||||
@ -417,6 +417,15 @@ export class LokiDatasource
|
|||||||
expression = addNoPipelineErrorToQuery(expression);
|
expression = addNoPipelineErrorToQuery(expression);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'ADD_LEVEL_LABEL_FORMAT': {
|
||||||
|
if (action.options?.originalLabel && action.options?.renameTo) {
|
||||||
|
expression = addLabelFormatToQuery(expression, {
|
||||||
|
renameTo: action.options.renameTo,
|
||||||
|
originalLabel: action.options.originalLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -70,4 +70,45 @@ describe('getQueryHints', () => {
|
|||||||
expect(getQueryHints('{job="grafana" | json', [jsonAndLogfmtSeries])).toEqual([]);
|
expect(getQueryHints('{job="grafana" | json', [jsonAndLogfmtSeries])).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when series with level-like label', () => {
|
||||||
|
const createSeriesWithLabel = (labelName?: string): DataFrame => {
|
||||||
|
const labelVariable: { [key: string]: string } = { job: 'a' };
|
||||||
|
if (labelName) {
|
||||||
|
labelVariable[labelName] = 'error';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: 'logs',
|
||||||
|
length: 2,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'Line',
|
||||||
|
type: FieldType.string,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector(['{"foo": "bar", "bar": "baz"}', 'foo="bar" bar="baz"']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'labels',
|
||||||
|
type: FieldType.other,
|
||||||
|
config: {},
|
||||||
|
values: new ArrayVector([labelVariable, { job: 'baz', foo: 'bar' }]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
it('suggest level renaming when no level label', () => {
|
||||||
|
expect(getQueryHints('{job="grafana"', [createSeriesWithLabel('lvl')])).toMatchObject([
|
||||||
|
{ type: 'ADD_JSON_PARSER' },
|
||||||
|
{ type: 'ADD_LOGFMT_PARSER' },
|
||||||
|
{ type: 'ADD_LEVEL_LABEL_FORMAT' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('does not suggest level renaming if level label', () => {
|
||||||
|
expect(getQueryHints('{job="grafana"', [createSeriesWithLabel('level')])).toMatchObject([
|
||||||
|
{ type: 'ADD_JSON_PARSER' },
|
||||||
|
{ type: 'ADD_LOGFMT_PARSER' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { DataFrame, QueryHint } from '@grafana/data';
|
import { DataFrame, QueryHint } from '@grafana/data';
|
||||||
|
|
||||||
import { isQueryPipelineErrorFiltering, isQueryWithParser } from './query_utils';
|
import { isQueryPipelineErrorFiltering, isQueryWithLabelFormat, isQueryWithParser } from './query_utils';
|
||||||
import { extractHasErrorLabelFromDataFrame, extractLogParserFromDataFrame } from './responseUtils';
|
import {
|
||||||
|
dataFrameHasLevelLabel,
|
||||||
|
extractHasErrorLabelFromDataFrame,
|
||||||
|
extractLevelLikeLabelFromDataFrame,
|
||||||
|
extractLogParserFromDataFrame,
|
||||||
|
} from './responseUtils';
|
||||||
|
|
||||||
export function getQueryHints(query: string, series: DataFrame[]): QueryHint[] {
|
export function getQueryHints(query: string, series: DataFrame[]): QueryHint[] {
|
||||||
if (series.length === 0) {
|
if (series.length === 0) {
|
||||||
@ -63,5 +68,30 @@ export function getQueryHints(query: string, series: DataFrame[]): QueryHint[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queryWithLabelFormat = isQueryWithLabelFormat(query);
|
||||||
|
if (!queryWithLabelFormat) {
|
||||||
|
const hasLevel = dataFrameHasLevelLabel(series[0]);
|
||||||
|
const levelLikeLabel = extractLevelLikeLabelFromDataFrame(series[0]);
|
||||||
|
|
||||||
|
// Add hint only if we don't have "level" label and have level-like label
|
||||||
|
if (!hasLevel && levelLikeLabel) {
|
||||||
|
hints.push({
|
||||||
|
type: 'ADD_LEVEL_LABEL_FORMAT',
|
||||||
|
label: `Some logs in your selected log stream have "${levelLikeLabel}" label.`,
|
||||||
|
fix: {
|
||||||
|
label: `If ${levelLikeLabel} label has level values, consider using label_format to rename it to "level". Level label can be then visualized in log volumes.`,
|
||||||
|
action: {
|
||||||
|
type: 'ADD_LEVEL_LABEL_FORMAT',
|
||||||
|
query,
|
||||||
|
options: {
|
||||||
|
renameTo: 'level',
|
||||||
|
originalLabel: levelLikeLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return hints;
|
return hints;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
getHighlighterExpressionsFromQuery,
|
getHighlighterExpressionsFromQuery,
|
||||||
getNormalizedLokiQuery,
|
getNormalizedLokiQuery,
|
||||||
isLogsQuery,
|
isLogsQuery,
|
||||||
|
isQueryWithLabelFormat,
|
||||||
isQueryWithParser,
|
isQueryWithParser,
|
||||||
isValidQuery,
|
isValidQuery,
|
||||||
} from './query_utils';
|
} from './query_utils';
|
||||||
@ -187,3 +188,21 @@ describe('isQueryWithParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isQueryWithLabelFormat', () => {
|
||||||
|
it('returns true if log query with label format', () => {
|
||||||
|
expect(isQueryWithLabelFormat('{job="grafana"} | label_format level=lvl')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if metrics query with label format', () => {
|
||||||
|
expect(isQueryWithLabelFormat('rate({job="grafana"} | label_format level=lvl [5m])')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if log query without label format', () => {
|
||||||
|
expect(isQueryWithLabelFormat('{job="grafana"} | json')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if metrics query without label format', () => {
|
||||||
|
expect(isQueryWithLabelFormat('rate({job="grafana"} [5m])')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -159,3 +159,16 @@ export function isQueryPipelineErrorFiltering(query: string): boolean {
|
|||||||
|
|
||||||
return isQueryPipelineErrorFiltering;
|
return isQueryPipelineErrorFiltering;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isQueryWithLabelFormat(query: string): boolean {
|
||||||
|
let queryWithLabelFormat = false;
|
||||||
|
const tree = parser.parse(query);
|
||||||
|
tree.iterate({
|
||||||
|
enter: (type): false | void => {
|
||||||
|
if (type.name === 'LabelFormatExpr') {
|
||||||
|
queryWithLabelFormat = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return queryWithLabelFormat;
|
||||||
|
}
|
||||||
|
@ -170,9 +170,9 @@ describe('LokiQueryModeller', () => {
|
|||||||
expect(
|
expect(
|
||||||
modeller.renderQuery({
|
modeller.renderQuery({
|
||||||
labels: [{ label: 'app', op: '=', value: 'grafana' }],
|
labels: [{ label: 'app', op: '=', value: 'grafana' }],
|
||||||
operations: [{ id: LokiOperationId.LabelFormat, params: ['new', 'old'] }],
|
operations: [{ id: LokiOperationId.LabelFormat, params: ['original', 'renameTo'] }],
|
||||||
})
|
})
|
||||||
).toBe('{app="grafana"} | label_format old=`new`');
|
).toBe('{app="grafana"} | label_format renameTo=original');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Can render simply binary operation with scalar', () => {
|
it('Can render simply binary operation with scalar', () => {
|
||||||
|
@ -187,13 +187,13 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
|||||||
name: 'Label format',
|
name: 'Label format',
|
||||||
params: [
|
params: [
|
||||||
{ name: 'Label', type: 'string' },
|
{ name: 'Label', type: 'string' },
|
||||||
{ name: 'Rename', type: 'string' },
|
{ name: 'Rename to', type: 'string' },
|
||||||
],
|
],
|
||||||
defaultParams: ['', ''],
|
defaultParams: ['', ''],
|
||||||
alternativesKey: 'format',
|
alternativesKey: 'format',
|
||||||
category: LokiVisualQueryOperationCategory.Formats,
|
category: LokiVisualQueryOperationCategory.Formats,
|
||||||
orderRank: LokiOperationOrder.LineFormats,
|
orderRank: LokiOperationOrder.LineFormats,
|
||||||
renderer: (model, def, innerExpr) => `${innerExpr} | label_format ${model.params[1]}=\`${model.params[0]}\``,
|
renderer: (model, def, innerExpr) => `${innerExpr} | label_format ${model.params[1]}=${model.params[0]}`,
|
||||||
addOperationHandler: addLokiOperation,
|
addOperationHandler: addLokiOperation,
|
||||||
explainHandler: () =>
|
explainHandler: () =>
|
||||||
`This will change name of label to desired new label. In the example below, label "error_level" will be renamed to "level".
|
`This will change name of label to desired new label. In the example below, label "error_level" will be renamed to "level".
|
||||||
|
@ -456,7 +456,7 @@ describe('buildVisualQueryFromString', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('parses query with label format', () => {
|
it('parses query with label format', () => {
|
||||||
expect(buildVisualQueryFromString('{app="frontend"} | label_format newLabel=oldLabel')).toEqual(
|
expect(buildVisualQueryFromString('{app="frontend"} | label_format renameTo=original')).toEqual(
|
||||||
noErrors({
|
noErrors({
|
||||||
labels: [
|
labels: [
|
||||||
{
|
{
|
||||||
@ -465,13 +465,13 @@ describe('buildVisualQueryFromString', () => {
|
|||||||
label: 'app',
|
label: 'app',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
operations: [{ id: 'label_format', params: ['newLabel', 'oldLabel'] }],
|
operations: [{ id: 'label_format', params: ['original', 'renameTo'] }],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses query with multiple label format', () => {
|
it('parses query with multiple label format', () => {
|
||||||
expect(buildVisualQueryFromString('{app="frontend"} | label_format newLabel=oldLabel, bar="baz"')).toEqual(
|
expect(buildVisualQueryFromString('{app="frontend"} | label_format renameTo=original, bar=baz')).toEqual(
|
||||||
noErrors({
|
noErrors({
|
||||||
labels: [
|
labels: [
|
||||||
{
|
{
|
||||||
@ -481,8 +481,8 @@ describe('buildVisualQueryFromString', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
operations: [
|
operations: [
|
||||||
{ id: 'label_format', params: ['newLabel', 'oldLabel'] },
|
{ id: 'label_format', params: ['original', 'renameTo'] },
|
||||||
{ id: 'label_format', params: ['bar', 'baz'] },
|
{ id: 'label_format', params: ['baz', 'bar'] },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -295,15 +295,13 @@ function getLineFormat(expr: string, node: SyntaxNode): QueryBuilderOperation {
|
|||||||
|
|
||||||
function getLabelFormat(expr: string, node: SyntaxNode): QueryBuilderOperation {
|
function getLabelFormat(expr: string, node: SyntaxNode): QueryBuilderOperation {
|
||||||
const id = 'label_format';
|
const id = 'label_format';
|
||||||
const identifier = node.getChild('Identifier');
|
const renameTo = node.getChild('Identifier');
|
||||||
const op = identifier!.nextSibling;
|
const op = renameTo!.nextSibling;
|
||||||
const value = op!.nextSibling;
|
const originalLabel = op!.nextSibling;
|
||||||
|
|
||||||
let valueString = handleQuotes(getString(expr, value));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
params: [getString(expr, identifier), valueString],
|
params: [getString(expr, originalLabel), handleQuotes(getString(expr, renameTo))],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash';
|
|||||||
|
|
||||||
import { ArrayVector, DataFrame, FieldType } from '@grafana/data';
|
import { ArrayVector, DataFrame, FieldType } from '@grafana/data';
|
||||||
|
|
||||||
import { dataFrameHasLokiError } from './responseUtils';
|
import { dataFrameHasLevelLabel, dataFrameHasLokiError, extractLevelLikeLabelFromDataFrame } from './responseUtils';
|
||||||
|
|
||||||
const frame: DataFrame = {
|
const frame: DataFrame = {
|
||||||
length: 1,
|
length: 1,
|
||||||
@ -28,7 +28,7 @@ const frame: DataFrame = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('dataframeHasParsingError', () => {
|
describe('dataFrameHasParsingError', () => {
|
||||||
it('handles frame with parsing error', () => {
|
it('handles frame with parsing error', () => {
|
||||||
const input = cloneDeep(frame);
|
const input = cloneDeep(frame);
|
||||||
input.fields[1].values = new ArrayVector([{ level: 'info', __error__: 'error' }]);
|
input.fields[1].values = new ArrayVector([{ level: 'info', __error__: 'error' }]);
|
||||||
@ -39,3 +39,34 @@ describe('dataframeHasParsingError', () => {
|
|||||||
expect(dataFrameHasLokiError(input)).toBe(false);
|
expect(dataFrameHasLokiError(input)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('dataFrameHasLevelLabel', () => {
|
||||||
|
it('returns true if level label is present', () => {
|
||||||
|
const input = cloneDeep(frame);
|
||||||
|
input.fields[1].values = new ArrayVector([{ level: 'info' }]);
|
||||||
|
expect(dataFrameHasLevelLabel(input)).toBe(true);
|
||||||
|
});
|
||||||
|
it('returns false if level label is present', () => {
|
||||||
|
const input = cloneDeep(frame);
|
||||||
|
input.fields[1].values = new ArrayVector([{ foo: 'bar' }]);
|
||||||
|
expect(dataFrameHasLevelLabel(input)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractLevelLikeLabelFromDataFrame', () => {
|
||||||
|
it('returns label if lvl label is present', () => {
|
||||||
|
const input = cloneDeep(frame);
|
||||||
|
input.fields[1].values = new ArrayVector([{ lvl: 'info' }]);
|
||||||
|
expect(extractLevelLikeLabelFromDataFrame(input)).toBe('lvl');
|
||||||
|
});
|
||||||
|
it('returns label if level-like label is present', () => {
|
||||||
|
const input = cloneDeep(frame);
|
||||||
|
input.fields[1].values = new ArrayVector([{ error_level: 'info' }]);
|
||||||
|
expect(extractLevelLikeLabelFromDataFrame(input)).toBe('error_level');
|
||||||
|
});
|
||||||
|
it('returns undefined if no level-like label is present', () => {
|
||||||
|
const input = cloneDeep(frame);
|
||||||
|
input.fields[1].values = new ArrayVector([{ foo: 'info' }]);
|
||||||
|
expect(extractLevelLikeLabelFromDataFrame(input)).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -4,6 +4,12 @@ export function dataFrameHasLokiError(frame: DataFrame): boolean {
|
|||||||
const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values.toArray() ?? [];
|
const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values.toArray() ?? [];
|
||||||
return labelSets.some((labels) => labels.__error__ !== undefined);
|
return labelSets.some((labels) => labels.__error__ !== undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dataFrameHasLevelLabel(frame: DataFrame): boolean {
|
||||||
|
const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values.toArray() ?? [];
|
||||||
|
return labelSets.some((labels) => labels.level !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
export function extractLogParserFromDataFrame(frame: DataFrame): { hasLogfmt: boolean; hasJSON: boolean } {
|
export function extractLogParserFromDataFrame(frame: DataFrame): { hasLogfmt: boolean; hasJSON: boolean } {
|
||||||
const lineField = frame.fields.find((field) => field.type === FieldType.string);
|
const lineField = frame.fields.find((field) => field.type === FieldType.string);
|
||||||
if (lineField == null) {
|
if (lineField == null) {
|
||||||
@ -37,3 +43,25 @@ export function extractHasErrorLabelFromDataFrame(frame: DataFrame): boolean {
|
|||||||
const labels: Array<{ [key: string]: string }> = labelField.values.toArray();
|
const labels: Array<{ [key: string]: string }> = labelField.values.toArray();
|
||||||
return labels.some((label) => label['__error__']);
|
return labels.some((label) => label['__error__']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractLevelLikeLabelFromDataFrame(frame: DataFrame): string | null {
|
||||||
|
const labelField = frame.fields.find((field) => field.name === 'labels' && field.type === FieldType.other);
|
||||||
|
if (labelField == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depending on number of labels, this can be pretty heavy operation.
|
||||||
|
// Let's just look at first 2 lines If needed, we can introduce more later.
|
||||||
|
const labelsArray: Array<{ [key: string]: string }> = labelField.values.toArray().slice(0, 2);
|
||||||
|
let levelLikeLabel: string | null = null;
|
||||||
|
|
||||||
|
// Find first level-like label
|
||||||
|
for (let labels of labelsArray) {
|
||||||
|
const label = Object.keys(labels).find((label) => label === 'lvl' || label.includes('level'));
|
||||||
|
if (label) {
|
||||||
|
levelLikeLabel = label;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return levelLikeLabel;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user