mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Suggestions: Adds logs suggestions and sorting by score to suggestions (#41613)
* Suggestions: Adds logs suggestions for sorting by score to suggestions * Introduced an enum * review feedback
This commit is contained in:
parent
dbcefb70f6
commit
dfd9813d70
@ -1,7 +1,7 @@
|
||||
import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource';
|
||||
import { PluginMeta } from './plugin';
|
||||
import { ScopedVars } from './ScopedVars';
|
||||
import { LoadingState } from './data';
|
||||
import { LoadingState, PreferredVisualisationType } from './data';
|
||||
import { DataFrame, FieldType } from './dataFrame';
|
||||
import { AbsoluteTimeRange, TimeRange, TimeZone } from './time';
|
||||
import { EventBus } from '../events';
|
||||
@ -203,6 +203,17 @@ export interface VisualizationSuggestion<TOptions = any, TFieldConfig = any> {
|
||||
transformations?: DataTransformerConfig[];
|
||||
/** Tweak for small preview */
|
||||
previewModifier?: (suggestion: VisualizationSuggestion) => void;
|
||||
/** A value between 0-100 how suitable suggestion is */
|
||||
score?: VisualizationSuggestionScore;
|
||||
}
|
||||
|
||||
export enum VisualizationSuggestionScore {
|
||||
/** We are pretty sure this is the best possible option */
|
||||
Best = 100,
|
||||
/** Should be a really good option */
|
||||
Good = 70,
|
||||
/** Can be visualized but there are likely better options. If no score is set this score is assumed */
|
||||
OK = 50,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -213,12 +224,15 @@ export interface PanelDataSummary {
|
||||
rowCountTotal: number;
|
||||
rowCountMax: number;
|
||||
frameCount: number;
|
||||
fieldCount: number;
|
||||
numberFieldCount: number;
|
||||
timeFieldCount: number;
|
||||
stringFieldCount: number;
|
||||
hasNumberField?: boolean;
|
||||
hasTimeField?: boolean;
|
||||
hasStringField?: boolean;
|
||||
/** The first frame that set's this value */
|
||||
preferredVisualisationType?: PreferredVisualisationType;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -252,11 +266,19 @@ export class VisualizationSuggestionsBuilder {
|
||||
let stringFieldCount = 0;
|
||||
let rowCountTotal = 0;
|
||||
let rowCountMax = 0;
|
||||
let fieldCount = 0;
|
||||
let preferredVisualisationType: PreferredVisualisationType | undefined;
|
||||
|
||||
for (const frame of frames) {
|
||||
rowCountTotal += frame.length;
|
||||
|
||||
if (frame.meta?.preferredVisualisationType) {
|
||||
preferredVisualisationType = frame.meta.preferredVisualisationType;
|
||||
}
|
||||
|
||||
for (const field of frame.fields) {
|
||||
fieldCount++;
|
||||
|
||||
switch (field.type) {
|
||||
case FieldType.number:
|
||||
numberFieldCount += 1;
|
||||
@ -281,6 +303,8 @@ export class VisualizationSuggestionsBuilder {
|
||||
stringFieldCount,
|
||||
rowCountTotal,
|
||||
rowCountMax,
|
||||
fieldCount,
|
||||
preferredVisualisationType,
|
||||
frameCount: frames.length,
|
||||
hasData: rowCountTotal > 0,
|
||||
hasTimeField: timeFieldCount > 0,
|
||||
|
@ -280,7 +280,7 @@ scenario('Single frame with string and 2 number field', (ctx) => {
|
||||
});
|
||||
});
|
||||
|
||||
scenario('Single frame with string with only string field', (ctx) => {
|
||||
scenario('Single frame with only string field', (ctx) => {
|
||||
ctx.setData([
|
||||
toDataFrame({
|
||||
fields: [{ name: 'Name', type: FieldType.string, values: ['Hugo', 'Dominik', 'Marcus'] }],
|
||||
@ -288,7 +288,7 @@ scenario('Single frame with string with only string field', (ctx) => {
|
||||
]);
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.names()).toEqual([SuggestionName.Stat, SuggestionName.StatColoredBackground, SuggestionName.Table]);
|
||||
expect(ctx.names()).toEqual([SuggestionName.Stat, SuggestionName.Table]);
|
||||
});
|
||||
|
||||
it('Stat panels have reduceOptions.fields set to show all fields', () => {
|
||||
@ -300,6 +300,32 @@ scenario('Single frame with string with only string field', (ctx) => {
|
||||
});
|
||||
});
|
||||
|
||||
scenario('Given default loki logs data', (ctx) => {
|
||||
ctx.setData([
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'ts', type: FieldType.time, values: ['2021-11-11T13:38:45.440Z', '2021-11-11T13:38:45.190Z'] },
|
||||
{
|
||||
name: 'line',
|
||||
type: FieldType.string,
|
||||
values: [
|
||||
't=2021-11-11T14:38:45+0100 lvl=dbug msg="Client connected" logger=live user=1 client=ee79155b-a8d1-4730-bcb3-94d8690df35c',
|
||||
't=2021-11-11T14:38:45+0100 lvl=dbug msg="Adding CSP header to response" logger=http.server cfg=0xc0005fed00',
|
||||
],
|
||||
labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
preferredVisualisationType: 'logs',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.names()).toEqual([SuggestionName.Logs, SuggestionName.Table]);
|
||||
});
|
||||
});
|
||||
|
||||
function repeatFrame(count: number, frame: DataFrame): DataFrame[] {
|
||||
const frames: DataFrame[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
@ -1,4 +1,10 @@
|
||||
import { PanelData, VisualizationSuggestion, VisualizationSuggestionsBuilder, PanelModel } from '@grafana/data';
|
||||
import {
|
||||
PanelData,
|
||||
VisualizationSuggestion,
|
||||
VisualizationSuggestionsBuilder,
|
||||
PanelModel,
|
||||
VisualizationSuggestionScore,
|
||||
} from '@grafana/data';
|
||||
import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin';
|
||||
|
||||
export const panelsToCheckFirst = [
|
||||
@ -13,6 +19,7 @@ export const panelsToCheckFirst = [
|
||||
'status-history',
|
||||
'text',
|
||||
'dashlist',
|
||||
'logs',
|
||||
];
|
||||
|
||||
export async function getAllSuggestions(data?: PanelData, panel?: PanelModel): Promise<VisualizationSuggestion[]> {
|
||||
@ -27,5 +34,7 @@ export async function getAllSuggestions(data?: PanelData, panel?: PanelModel): P
|
||||
}
|
||||
}
|
||||
|
||||
return builder.getList();
|
||||
return builder.getList().sort((a, b) => {
|
||||
return (b.score ?? VisualizationSuggestionScore.OK) - (a.score ?? VisualizationSuggestionScore.OK);
|
||||
});
|
||||
}
|
||||
|
@ -1,81 +1,84 @@
|
||||
import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription } from '@grafana/data';
|
||||
import { Options } from './types';
|
||||
import { LogsPanel } from './LogsPanel';
|
||||
import { LogsPanelSuggestionsSupplier } from './suggestions';
|
||||
|
||||
export const plugin = new PanelPlugin<Options>(LogsPanel).setPanelOptions((builder) => {
|
||||
builder
|
||||
.addBooleanSwitch({
|
||||
path: 'showTime',
|
||||
name: 'Time',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showLabels',
|
||||
name: 'Unique labels',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showCommonLabels',
|
||||
name: 'Common labels',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'wrapLogMessage',
|
||||
name: 'Wrap lines',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'prettifyLogMessage',
|
||||
name: 'Prettify JSON',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'enableLogDetails',
|
||||
name: 'Enable log details',
|
||||
description: '',
|
||||
defaultValue: true,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'dedupStrategy',
|
||||
name: 'Deduplication',
|
||||
description: '',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: LogsDedupStrategy.none, label: 'None', description: LogsDedupDescription[LogsDedupStrategy.none] },
|
||||
{
|
||||
value: LogsDedupStrategy.exact,
|
||||
label: 'Exact',
|
||||
description: LogsDedupDescription[LogsDedupStrategy.exact],
|
||||
},
|
||||
{
|
||||
value: LogsDedupStrategy.numbers,
|
||||
label: 'Numbers',
|
||||
description: LogsDedupDescription[LogsDedupStrategy.numbers],
|
||||
},
|
||||
{
|
||||
value: LogsDedupStrategy.signature,
|
||||
label: 'Signature',
|
||||
description: LogsDedupDescription[LogsDedupStrategy.signature],
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultValue: LogsDedupStrategy.none,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'sortOrder',
|
||||
name: 'Order',
|
||||
description: '',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: LogsSortOrder.Descending, label: 'Descending' },
|
||||
{ value: LogsSortOrder.Ascending, label: 'Ascending' },
|
||||
],
|
||||
},
|
||||
defaultValue: LogsSortOrder.Descending,
|
||||
});
|
||||
});
|
||||
export const plugin = new PanelPlugin<Options>(LogsPanel)
|
||||
.setPanelOptions((builder) => {
|
||||
builder
|
||||
.addBooleanSwitch({
|
||||
path: 'showTime',
|
||||
name: 'Time',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showLabels',
|
||||
name: 'Unique labels',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showCommonLabels',
|
||||
name: 'Common labels',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'wrapLogMessage',
|
||||
name: 'Wrap lines',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'prettifyLogMessage',
|
||||
name: 'Prettify JSON',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'enableLogDetails',
|
||||
name: 'Enable log details',
|
||||
description: '',
|
||||
defaultValue: true,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'dedupStrategy',
|
||||
name: 'Deduplication',
|
||||
description: '',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: LogsDedupStrategy.none, label: 'None', description: LogsDedupDescription[LogsDedupStrategy.none] },
|
||||
{
|
||||
value: LogsDedupStrategy.exact,
|
||||
label: 'Exact',
|
||||
description: LogsDedupDescription[LogsDedupStrategy.exact],
|
||||
},
|
||||
{
|
||||
value: LogsDedupStrategy.numbers,
|
||||
label: 'Numbers',
|
||||
description: LogsDedupDescription[LogsDedupStrategy.numbers],
|
||||
},
|
||||
{
|
||||
value: LogsDedupStrategy.signature,
|
||||
label: 'Signature',
|
||||
description: LogsDedupDescription[LogsDedupStrategy.signature],
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultValue: LogsDedupStrategy.none,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'sortOrder',
|
||||
name: 'Order',
|
||||
description: '',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: LogsSortOrder.Descending, label: 'Descending' },
|
||||
{ value: LogsSortOrder.Ascending, label: 'Ascending' },
|
||||
],
|
||||
},
|
||||
defaultValue: LogsSortOrder.Descending,
|
||||
});
|
||||
})
|
||||
.setSuggestionsSupplier(new LogsPanelSuggestionsSupplier());
|
||||
|
33
public/app/plugins/panel/logs/suggestions.ts
Normal file
33
public/app/plugins/panel/logs/suggestions.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data';
|
||||
import { SuggestionName } from 'app/types/suggestions';
|
||||
import { Options } from './types';
|
||||
|
||||
export class LogsPanelSuggestionsSupplier {
|
||||
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
|
||||
const list = builder.getListAppender<Options, {}>({
|
||||
name: '',
|
||||
pluginId: 'logs',
|
||||
options: {},
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
custom: {},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
previewModifier: () => {},
|
||||
});
|
||||
|
||||
const { dataSummary: ds } = builder;
|
||||
|
||||
// Require a string & time field
|
||||
if (!ds.hasData || !ds.hasTimeField || !ds.hasStringField) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ds.preferredVisualisationType === 'logs') {
|
||||
list.append({ name: SuggestionName.Logs, score: VisualizationSuggestionScore.Best });
|
||||
} else {
|
||||
list.append({ name: SuggestionName.Logs });
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,6 @@ export class PieChartSuggestionsSupplier {
|
||||
previewModifier: (s) => {
|
||||
// Hide labels in preview
|
||||
s.options!.legend.displayMode = LegendDisplayMode.Hidden;
|
||||
s.options!.displayLabels = [];
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -5,9 +5,9 @@ import { StatPanelOptions } from './types';
|
||||
|
||||
export class StatSuggestionsSupplier {
|
||||
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
|
||||
const { dataSummary } = builder;
|
||||
const { dataSummary: ds } = builder;
|
||||
|
||||
if (!dataSummary.hasData) {
|
||||
if (!ds.hasData) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -29,14 +29,15 @@ export class StatSuggestionsSupplier {
|
||||
},
|
||||
});
|
||||
|
||||
if (dataSummary.hasStringField && dataSummary.frameCount === 1 && dataSummary.rowCountTotal < 10) {
|
||||
// String and number field with low row count show individual rows
|
||||
if (ds.hasStringField && ds.hasNumberField && ds.frameCount === 1 && ds.rowCountTotal < 10) {
|
||||
list.append({
|
||||
name: SuggestionName.Stat,
|
||||
options: {
|
||||
reduceOptions: {
|
||||
values: true,
|
||||
calcs: [],
|
||||
fields: dataSummary.hasNumberField ? undefined : '/.*/',
|
||||
fields: '/.*/',
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -46,12 +47,29 @@ export class StatSuggestionsSupplier {
|
||||
reduceOptions: {
|
||||
values: true,
|
||||
calcs: [],
|
||||
fields: dataSummary.hasNumberField ? undefined : '/.*/',
|
||||
fields: '/.*/',
|
||||
},
|
||||
colorMode: BigValueColorMode.Background,
|
||||
},
|
||||
});
|
||||
} else if (dataSummary.hasNumberField) {
|
||||
}
|
||||
|
||||
// Just a single string field
|
||||
if (ds.stringFieldCount === 1 && ds.frameCount === 1 && ds.rowCountTotal < 10 && ds.fieldCount === 1) {
|
||||
list.append({
|
||||
name: SuggestionName.Stat,
|
||||
options: {
|
||||
reduceOptions: {
|
||||
values: true,
|
||||
calcs: [],
|
||||
fields: '/.*/',
|
||||
},
|
||||
colorMode: BigValueColorMode.None,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (ds.hasNumberField && ds.hasTimeField) {
|
||||
list.append({
|
||||
options: {
|
||||
reduceOptions: {
|
||||
|
@ -4,19 +4,24 @@ import { TimelineFieldConfig, TimelineOptions } from './types';
|
||||
|
||||
export class StatTimelineSuggestionsSupplier {
|
||||
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
|
||||
const { dataSummary } = builder;
|
||||
const { dataSummary: ds } = builder;
|
||||
|
||||
if (!dataSummary.hasData) {
|
||||
if (!ds.hasData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This panel needs a time field and a string or number field
|
||||
if (!dataSummary.hasTimeField || (!dataSummary.hasStringField && !dataSummary.hasNumberField)) {
|
||||
if (!ds.hasTimeField || (!ds.hasStringField && !ds.hasNumberField)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there are many series then they won't fit on y-axis so this panel is not good fit
|
||||
if (dataSummary.numberFieldCount >= 30) {
|
||||
if (ds.numberFieldCount >= 30) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Probably better ways to filter out this by inspecting the types of string values so view this as temporary
|
||||
if (ds.preferredVisualisationType === 'logs') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -4,24 +4,29 @@ import { StatusPanelOptions, StatusFieldConfig } from './types';
|
||||
|
||||
export class StatusHistorySuggestionsSupplier {
|
||||
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
|
||||
const { dataSummary } = builder;
|
||||
const { dataSummary: ds } = builder;
|
||||
|
||||
if (!dataSummary.hasData) {
|
||||
if (!ds.hasData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This panel needs a time field and a string or number field
|
||||
if (!dataSummary.hasTimeField || (!dataSummary.hasStringField && !dataSummary.hasNumberField)) {
|
||||
if (!ds.hasTimeField || (!ds.hasStringField && !ds.hasNumberField)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there are many series then they won't fit on y-axis so this panel is not good fit
|
||||
if (dataSummary.numberFieldCount >= 30) {
|
||||
if (ds.numberFieldCount >= 30) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if there a lot of data points for each series then this is not a good match
|
||||
if (dataSummary.rowCountMax > 100) {
|
||||
if (ds.rowCountMax > 100) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Probably better ways to filter out this by inspecting the types of string values so view this as temporary
|
||||
if (ds.preferredVisualisationType === 'logs') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -25,4 +25,5 @@ export enum SuggestionName {
|
||||
StatusHistory = 'Status history',
|
||||
TextPanel = 'Text panel',
|
||||
DashboardList = 'Dashboard list',
|
||||
Logs = 'Logs',
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user