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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 221 additions and 98 deletions

View File

@ -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,

View File

@ -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++) {

View File

@ -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);
});
}

View File

@ -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());

View 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 });
}
}
}

View File

@ -22,7 +22,6 @@ export class PieChartSuggestionsSupplier {
previewModifier: (s) => {
// Hide labels in preview
s.options!.legend.displayMode = LegendDisplayMode.Hidden;
s.options!.displayLabels = [];
},
});

View File

@ -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: {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -25,4 +25,5 @@ export enum SuggestionName {
StatusHistory = 'Status history',
TextPanel = 'Text panel',
DashboardList = 'Dashboard list',
Logs = 'Logs',
}