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:
Torkel Ödegaard
2021-11-15 15:13:01 +01:00
committed by GitHub
parent dbcefb70f6
commit dfd9813d70
10 changed files with 221 additions and 98 deletions
+25 -1
View File
@@ -1,7 +1,7 @@
import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource'; import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource';
import { PluginMeta } from './plugin'; import { PluginMeta } from './plugin';
import { ScopedVars } from './ScopedVars'; import { ScopedVars } from './ScopedVars';
import { LoadingState } from './data'; import { LoadingState, PreferredVisualisationType } from './data';
import { DataFrame, FieldType } from './dataFrame'; import { DataFrame, FieldType } from './dataFrame';
import { AbsoluteTimeRange, TimeRange, TimeZone } from './time'; import { AbsoluteTimeRange, TimeRange, TimeZone } from './time';
import { EventBus } from '../events'; import { EventBus } from '../events';
@@ -203,6 +203,17 @@ export interface VisualizationSuggestion<TOptions = any, TFieldConfig = any> {
transformations?: DataTransformerConfig[]; transformations?: DataTransformerConfig[];
/** Tweak for small preview */ /** Tweak for small preview */
previewModifier?: (suggestion: VisualizationSuggestion) => void; 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; rowCountTotal: number;
rowCountMax: number; rowCountMax: number;
frameCount: number; frameCount: number;
fieldCount: number;
numberFieldCount: number; numberFieldCount: number;
timeFieldCount: number; timeFieldCount: number;
stringFieldCount: number; stringFieldCount: number;
hasNumberField?: boolean; hasNumberField?: boolean;
hasTimeField?: boolean; hasTimeField?: boolean;
hasStringField?: boolean; hasStringField?: boolean;
/** The first frame that set's this value */
preferredVisualisationType?: PreferredVisualisationType;
} }
/** /**
@@ -252,11 +266,19 @@ export class VisualizationSuggestionsBuilder {
let stringFieldCount = 0; let stringFieldCount = 0;
let rowCountTotal = 0; let rowCountTotal = 0;
let rowCountMax = 0; let rowCountMax = 0;
let fieldCount = 0;
let preferredVisualisationType: PreferredVisualisationType | undefined;
for (const frame of frames) { for (const frame of frames) {
rowCountTotal += frame.length; rowCountTotal += frame.length;
if (frame.meta?.preferredVisualisationType) {
preferredVisualisationType = frame.meta.preferredVisualisationType;
}
for (const field of frame.fields) { for (const field of frame.fields) {
fieldCount++;
switch (field.type) { switch (field.type) {
case FieldType.number: case FieldType.number:
numberFieldCount += 1; numberFieldCount += 1;
@@ -281,6 +303,8 @@ export class VisualizationSuggestionsBuilder {
stringFieldCount, stringFieldCount,
rowCountTotal, rowCountTotal,
rowCountMax, rowCountMax,
fieldCount,
preferredVisualisationType,
frameCount: frames.length, frameCount: frames.length,
hasData: rowCountTotal > 0, hasData: rowCountTotal > 0,
hasTimeField: timeFieldCount > 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([ ctx.setData([
toDataFrame({ toDataFrame({
fields: [{ name: 'Name', type: FieldType.string, values: ['Hugo', 'Dominik', 'Marcus'] }], 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', () => { 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', () => { 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[] { function repeatFrame(count: number, frame: DataFrame): DataFrame[] {
const frames: DataFrame[] = []; const frames: DataFrame[] = [];
for (let i = 0; i < count; i++) { 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'; import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin';
export const panelsToCheckFirst = [ export const panelsToCheckFirst = [
@@ -13,6 +19,7 @@ export const panelsToCheckFirst = [
'status-history', 'status-history',
'text', 'text',
'dashlist', 'dashlist',
'logs',
]; ];
export async function getAllSuggestions(data?: PanelData, panel?: PanelModel): Promise<VisualizationSuggestion[]> { 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);
});
} }
+80 -77
View File
@@ -1,81 +1,84 @@
import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription } from '@grafana/data'; import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription } from '@grafana/data';
import { Options } from './types'; import { Options } from './types';
import { LogsPanel } from './LogsPanel'; import { LogsPanel } from './LogsPanel';
import { LogsPanelSuggestionsSupplier } from './suggestions';
export const plugin = new PanelPlugin<Options>(LogsPanel).setPanelOptions((builder) => { export const plugin = new PanelPlugin<Options>(LogsPanel)
builder .setPanelOptions((builder) => {
.addBooleanSwitch({ builder
path: 'showTime', .addBooleanSwitch({
name: 'Time', path: 'showTime',
description: '', name: 'Time',
defaultValue: false, description: '',
}) defaultValue: false,
.addBooleanSwitch({ })
path: 'showLabels', .addBooleanSwitch({
name: 'Unique labels', path: 'showLabels',
description: '', name: 'Unique labels',
defaultValue: false, description: '',
}) defaultValue: false,
.addBooleanSwitch({ })
path: 'showCommonLabels', .addBooleanSwitch({
name: 'Common labels', path: 'showCommonLabels',
description: '', name: 'Common labels',
defaultValue: false, description: '',
}) defaultValue: false,
.addBooleanSwitch({ })
path: 'wrapLogMessage', .addBooleanSwitch({
name: 'Wrap lines', path: 'wrapLogMessage',
description: '', name: 'Wrap lines',
defaultValue: false, description: '',
}) defaultValue: false,
.addBooleanSwitch({ })
path: 'prettifyLogMessage', .addBooleanSwitch({
name: 'Prettify JSON', path: 'prettifyLogMessage',
description: '', name: 'Prettify JSON',
defaultValue: false, description: '',
}) defaultValue: false,
.addBooleanSwitch({ })
path: 'enableLogDetails', .addBooleanSwitch({
name: 'Enable log details', path: 'enableLogDetails',
description: '', name: 'Enable log details',
defaultValue: true, description: '',
}) defaultValue: true,
.addRadio({ })
path: 'dedupStrategy', .addRadio({
name: 'Deduplication', path: 'dedupStrategy',
description: '', name: 'Deduplication',
settings: { description: '',
options: [ settings: {
{ value: LogsDedupStrategy.none, label: 'None', description: LogsDedupDescription[LogsDedupStrategy.none] }, options: [
{ { value: LogsDedupStrategy.none, label: 'None', description: LogsDedupDescription[LogsDedupStrategy.none] },
value: LogsDedupStrategy.exact, {
label: 'Exact', value: LogsDedupStrategy.exact,
description: LogsDedupDescription[LogsDedupStrategy.exact], label: 'Exact',
}, description: LogsDedupDescription[LogsDedupStrategy.exact],
{ },
value: LogsDedupStrategy.numbers, {
label: 'Numbers', value: LogsDedupStrategy.numbers,
description: LogsDedupDescription[LogsDedupStrategy.numbers], label: 'Numbers',
}, description: LogsDedupDescription[LogsDedupStrategy.numbers],
{ },
value: LogsDedupStrategy.signature, {
label: 'Signature', value: LogsDedupStrategy.signature,
description: LogsDedupDescription[LogsDedupStrategy.signature], label: 'Signature',
}, description: LogsDedupDescription[LogsDedupStrategy.signature],
], },
}, ],
defaultValue: LogsDedupStrategy.none, },
}) defaultValue: LogsDedupStrategy.none,
.addRadio({ })
path: 'sortOrder', .addRadio({
name: 'Order', path: 'sortOrder',
description: '', name: 'Order',
settings: { description: '',
options: [ settings: {
{ value: LogsSortOrder.Descending, label: 'Descending' }, options: [
{ value: LogsSortOrder.Ascending, label: 'Ascending' }, { value: LogsSortOrder.Descending, label: 'Descending' },
], { value: LogsSortOrder.Ascending, label: 'Ascending' },
}, ],
defaultValue: LogsSortOrder.Descending, },
}); defaultValue: LogsSortOrder.Descending,
}); });
})
.setSuggestionsSupplier(new LogsPanelSuggestionsSupplier());
@@ -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) => { previewModifier: (s) => {
// Hide labels in preview // Hide labels in preview
s.options!.legend.displayMode = LegendDisplayMode.Hidden; s.options!.legend.displayMode = LegendDisplayMode.Hidden;
s.options!.displayLabels = [];
}, },
}); });
+24 -6
View File
@@ -5,9 +5,9 @@ import { StatPanelOptions } from './types';
export class StatSuggestionsSupplier { export class StatSuggestionsSupplier {
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
const { dataSummary } = builder; const { dataSummary: ds } = builder;
if (!dataSummary.hasData) { if (!ds.hasData) {
return; 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({ list.append({
name: SuggestionName.Stat, name: SuggestionName.Stat,
options: { options: {
reduceOptions: { reduceOptions: {
values: true, values: true,
calcs: [], calcs: [],
fields: dataSummary.hasNumberField ? undefined : '/.*/', fields: '/.*/',
}, },
}, },
}); });
@@ -46,12 +47,29 @@ export class StatSuggestionsSupplier {
reduceOptions: { reduceOptions: {
values: true, values: true,
calcs: [], calcs: [],
fields: dataSummary.hasNumberField ? undefined : '/.*/', fields: '/.*/',
}, },
colorMode: BigValueColorMode.Background, 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({ list.append({
options: { options: {
reduceOptions: { reduceOptions: {
@@ -4,19 +4,24 @@ import { TimelineFieldConfig, TimelineOptions } from './types';
export class StatTimelineSuggestionsSupplier { export class StatTimelineSuggestionsSupplier {
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
const { dataSummary } = builder; const { dataSummary: ds } = builder;
if (!dataSummary.hasData) { if (!ds.hasData) {
return; return;
} }
// This panel needs a time field and a string or number field // 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; return;
} }
// If there are many series then they won't fit on y-axis so this panel is not good fit // 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; return;
} }
@@ -4,24 +4,29 @@ import { StatusPanelOptions, StatusFieldConfig } from './types';
export class StatusHistorySuggestionsSupplier { export class StatusHistorySuggestionsSupplier {
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
const { dataSummary } = builder; const { dataSummary: ds } = builder;
if (!dataSummary.hasData) { if (!ds.hasData) {
return; return;
} }
// This panel needs a time field and a string or number field // 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; return;
} }
// If there are many series then they won't fit on y-axis so this panel is not good fit // 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; return;
} }
// if there a lot of data points for each series then this is not a good match // 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; return;
} }
+1
View File
@@ -25,4 +25,5 @@ export enum SuggestionName {
StatusHistory = 'Status history', StatusHistory = 'Status history',
TextPanel = 'Text panel', TextPanel = 'Text panel',
DashboardList = 'Dashboard list', DashboardList = 'Dashboard list',
Logs = 'Logs',
} }