mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user