Explore: Add custom DataLinks on datasource level for Loki (#20060)

Adds a config section with derived fields which is a config that allows you to create a new field based on a regex matcher run on a log message create DataLink to it which is the clickable in the log detail.
This commit is contained in:
Andrej Ocenas
2019-11-06 16:15:08 +01:00
committed by GitHub
parent 9507eda9d1
commit 0a78652404
37 changed files with 1230 additions and 423 deletions

View File

@@ -17,7 +17,7 @@ export enum FieldType {
/**
* Every property is optional
*
* Plugins may extend this with additional properties. Somethign like series overrides
* Plugins may extend this with additional properties. Something like series overrides
*/
export interface FieldConfig {
title?: string; // The display value for this field. This supports template variables blank is auto

View File

@@ -1,5 +1,6 @@
import { Labels } from './data';
import { GraphSeriesXY } from './graph';
import { DataFrame } from './dataFrame';
/**
* Mapping of log level abbreviation to canonical log level.
@@ -36,7 +37,19 @@ export interface LogsMetaItem {
}
export interface LogRowModel {
// Index of the field from which the entry has been created so that we do not show it later in log row details.
entryFieldIndex: number;
// Index of the row in the dataframe. As log rows can be stitched from multiple dataFrames, this does not have to be
// the same as rows final index when rendered.
rowIndex: number;
// Full DataFrame from which we parsed this log.
// TODO: refactor this so we do not need to pass whole dataframes in addition to also parsed data.
dataFrame: DataFrame;
duplicates?: number;
// Actual log line
entry: string;
hasAnsi: boolean;
labels: Labels;

View File

@@ -1,5 +1,12 @@
import { LogLevel } from '../types/logs';
import { getLogLevel, calculateLogsLabelStats, calculateFieldStats, getParser, LogsParsers } from './logs';
import {
getLogLevel,
calculateLogsLabelStats,
calculateFieldStats,
getParser,
LogsParsers,
calculateStats,
} from './logs';
describe('getLoglevel()', () => {
it('returns no log level on empty line', () => {
@@ -208,6 +215,28 @@ describe('calculateFieldStats()', () => {
});
});
describe('calculateStats()', () => {
test('should return no stats for empty array', () => {
expect(calculateStats([])).toEqual([]);
});
test('should return correct stats', () => {
const values = ['one', 'one', null, undefined, 'two'];
expect(calculateStats(values)).toMatchObject([
{
value: 'one',
count: 2,
proportion: 2 / 3,
},
{
value: 'two',
count: 1,
proportion: 1 / 3,
},
]);
});
});
describe('getParser()', () => {
test('should return no parser on empty line', () => {
expect(getParser('')).toBeUndefined();

View File

@@ -63,22 +63,6 @@ export function addLogLevelToSeries(series: DataFrame, lineIndex: number): DataF
};
}
export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogLabelStatsModel[] {
// Consider only rows that have the given label
const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined);
const rowCount = rowsWithLabel.length;
// Get label value counts for eligible rows
const countsByValue = countBy(rowsWithLabel, row => (row as LogRowModel).labels[label]);
const sortedCounts = chain(countsByValue)
.map((count, value) => ({ count, value, proportion: count / rowCount }))
.sortBy('count')
.reverse()
.value();
return sortedCounts;
}
export const LogsParsers: { [name: string]: LogsParser } = {
JSON: {
buildMatcher: label => new RegExp(`(?:{|,)\\s*"${label}"\\s*:\\s*"?([\\d\\.]+|[^"]*)"?`),
@@ -128,14 +112,32 @@ export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): Log
return match ? match[1] : null;
});
const sortedCounts = chain(countsByValue)
return getSortedCounts(countsByValue, rowCount);
}
export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogLabelStatsModel[] {
// Consider only rows that have the given label
const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined);
const rowCount = rowsWithLabel.length;
// Get label value counts for eligible rows
const countsByValue = countBy(rowsWithLabel, row => (row as LogRowModel).labels[label]);
return getSortedCounts(countsByValue, rowCount);
}
export function calculateStats(values: any[]): LogLabelStatsModel[] {
const nonEmptyValues = values.filter(value => value !== undefined && value !== null);
const countsByValue = countBy(nonEmptyValues);
return getSortedCounts(countsByValue, nonEmptyValues.length);
}
const getSortedCounts = (countsByValue: { [value: string]: number }, rowCount: number) => {
return chain(countsByValue)
.map((count, value) => ({ count, value, proportion: count / rowCount }))
.sortBy('count')
.reverse()
.value();
return sortedCounts;
}
};
export function getParser(line: string): LogsParser | undefined {
let parser;