From e3e51f6e3b9583e61262bb1add148e06390702a9 Mon Sep 17 00:00:00 2001 From: zenador Date: Sat, 4 Jan 2025 03:09:19 +0800 Subject: [PATCH] Prometheus datasource: Show info annotations in the UI (#97978) * Show info annotations from Prometheus datasources in the UI * Add test data for backend --- pkg/promlib/converter/prom.go | 22 +- pkg/promlib/converter/prom_test.go | 29 +- .../converter/testdata/prom-infos-frame.jsonc | 443 ++++++++++++++++++ .../testdata/prom-infos-no-data-frame.jsonc | 51 ++ .../testdata/prom-infos-no-data.json | 8 + .../converter/testdata/prom-infos.json | 37 ++ .../query/components/QueryEditorRow.test.tsx | 120 ++++- .../query/components/QueryEditorRow.tsx | 29 +- 8 files changed, 717 insertions(+), 22 deletions(-) create mode 100644 pkg/promlib/converter/testdata/prom-infos-frame.jsonc create mode 100644 pkg/promlib/converter/testdata/prom-infos-no-data-frame.jsonc create mode 100644 pkg/promlib/converter/testdata/prom-infos-no-data.json create mode 100644 pkg/promlib/converter/testdata/prom-infos.json diff --git a/pkg/promlib/converter/prom.go b/pkg/promlib/converter/prom.go index 5bc0f5ecb43..2d0a35524ca 100644 --- a/pkg/promlib/converter/prom.go +++ b/pkg/promlib/converter/prom.go @@ -34,6 +34,7 @@ func ReadPrometheusStyleResult(jIter *jsoniter.Iterator, opt Options) backend.Da errorType := "" promErrString := "" warnings := []data.Notice{} + infos := []data.Notice{} l1Fields: for l1Field, err := iter.ReadObject(); ; l1Field, err = iter.ReadObject() { @@ -67,6 +68,11 @@ l1Fields: return rspErr(err) } + case "infos": + if infos, err = readInfos(iter); err != nil { + return rspErr(err) + } + case "": if err != nil { return rspErr(err) @@ -89,6 +95,10 @@ l1Fields: } } + if len(infos) > 0 { + warnings = append(warnings, infos...) + } + if len(warnings) > 0 { if len(rsp.Frames) == 0 { rsp.Frames = append(rsp.Frames, data.NewFrame("Warnings")) @@ -105,7 +115,7 @@ l1Fields: return rsp } -func readWarnings(iter *sdkjsoniter.Iterator) ([]data.Notice, error) { +func readAnnotations(iter *sdkjsoniter.Iterator, sevLevel data.NoticeSeverity) ([]data.Notice, error) { warnings := []data.Notice{} next, err := iter.WhatIsNext() if err != nil { @@ -130,7 +140,7 @@ func readWarnings(iter *sdkjsoniter.Iterator) ([]data.Notice, error) { return nil, err } notice := data.Notice{ - Severity: data.NoticeSeverityWarning, + Severity: sevLevel, Text: s, } warnings = append(warnings, notice) @@ -140,6 +150,14 @@ func readWarnings(iter *sdkjsoniter.Iterator) ([]data.Notice, error) { return warnings, nil } +func readWarnings(iter *sdkjsoniter.Iterator) ([]data.Notice, error) { + return readAnnotations(iter, data.NoticeSeverityWarning) +} + +func readInfos(iter *sdkjsoniter.Iterator) ([]data.Notice, error) { + return readAnnotations(iter, data.NoticeSeverityInfo) +} + func readPrometheusData(iter *sdkjsoniter.Iterator, opt Options) backend.DataResponse { var rsp backend.DataResponse t, err := iter.WhatIsNext() diff --git a/pkg/promlib/converter/prom_test.go b/pkg/promlib/converter/prom_test.go index 775465cbaff..30221123b16 100644 --- a/pkg/promlib/converter/prom_test.go +++ b/pkg/promlib/converter/prom_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/grafana/grafana-plugin-sdk-go/data" sdkjsoniter "github.com/grafana/grafana-plugin-sdk-go/data/utils/jsoniter" "github.com/grafana/grafana-plugin-sdk-go/experimental" jsoniter "github.com/json-iterator/go" @@ -29,6 +30,8 @@ var files = []string{ "prom-series", "prom-warnings", "prom-warnings-no-data", + "prom-infos", + "prom-infos-no-data", "prom-error", "prom-exemplars-a", "prom-exemplars-b", @@ -62,8 +65,13 @@ func runScenario(name string, opts Options) func(t *testing.T) { if strings.Contains(name, "warnings") { hasWarning := false for _, frame := range rsp.Frames { - if len(frame.Meta.Notices) > 0 { - hasWarning = true + for _, notice := range frame.Meta.Notices { + if notice.Severity == data.NoticeSeverityWarning { + hasWarning = true + break + } + } + if hasWarning { break } } @@ -71,6 +79,23 @@ func runScenario(name string, opts Options) func(t *testing.T) { require.True(t, hasWarning) } + if strings.Contains(name, "infos") { + hasInfo := false + for _, frame := range rsp.Frames { + for _, notice := range frame.Meta.Notices { + if notice.Severity == data.NoticeSeverityInfo { + hasInfo = true + break + } + } + if hasInfo { + break + } + } + + require.True(t, hasInfo) + } + require.NoError(t, rsp.Error) fname := name + "-frame" diff --git a/pkg/promlib/converter/testdata/prom-infos-frame.jsonc b/pkg/promlib/converter/testdata/prom-infos-frame.jsonc new file mode 100644 index 00000000000..84e6302d64b --- /dev/null +++ b/pkg/promlib/converter/testdata/prom-infos-frame.jsonc @@ -0,0 +1,443 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "type": "timeseries-multi", +// "typeVersion": [ +// 0, +// 0 +// ], +// "custom": { +// "resultType": "vector" +// }, +// "notices": [ +// { +// "text": "info 1" +// }, +// { +// "text": "info 2" +// } +// ] +// } +// Name: +// Dimensions: 2 Fields by 1 Rows +// +-----------------------------------+--------------------------------------------------------------+ +// | Name: Time | Name: Value | +// | Labels: | Labels: __name__=up, instance=localhost:9090, job=prometheus | +// | Type: []time.Time | Type: []float64 | +// +-----------------------------------+--------------------------------------------------------------+ +// | 2015-07-01 20:10:51.781 +0000 UTC | 1 | +// +-----------------------------------+--------------------------------------------------------------+ +// +// +// +// Frame[1] { +// "type": "timeseries-multi", +// "typeVersion": [ +// 0, +// 0 +// ], +// "custom": { +// "resultType": "vector" +// }, +// "notices": [ +// { +// "text": "info 1" +// }, +// { +// "text": "info 2" +// } +// ] +// } +// Name: +// Dimensions: 2 Fields by 1 Rows +// +-----------------------------------+--------------------------------------------------------+ +// | Name: Time | Name: Value | +// | Labels: | Labels: __name__=up, instance=localhost:9100, job=node | +// | Type: []time.Time | Type: []float64 | +// +-----------------------------------+--------------------------------------------------------+ +// | 2015-07-01 20:10:51.781 +0000 UTC | 0 | +// +-----------------------------------+--------------------------------------------------------+ +// +// +// +// Frame[2] { +// "type": "timeseries-multi", +// "typeVersion": [ +// 0, +// 0 +// ], +// "custom": { +// "resultType": "vector" +// }, +// "notices": [ +// { +// "text": "info 1" +// }, +// { +// "text": "info 2" +// } +// ] +// } +// Name: +// Dimensions: 2 Fields by 1 Rows +// +-------------------------------+------------------------------------+ +// | Name: Time | Name: Value | +// | Labels: | Labels: level=error, location=moon | +// | Type: []time.Time | Type: []float64 | +// +-------------------------------+------------------------------------+ +// | 2022-02-16 16:41:39 +0000 UTC | +Inf | +// +-------------------------------+------------------------------------+ +// +// +// +// Frame[3] { +// "type": "timeseries-multi", +// "typeVersion": [ +// 0, +// 0 +// ], +// "custom": { +// "resultType": "vector" +// }, +// "notices": [ +// { +// "text": "info 1" +// }, +// { +// "text": "info 2" +// } +// ] +// } +// Name: +// Dimensions: 2 Fields by 1 Rows +// +-------------------------------+-----------------------------------+ +// | Name: Time | Name: Value | +// | Labels: | Labels: level=info, location=moon | +// | Type: []time.Time | Type: []float64 | +// +-------------------------------+-----------------------------------+ +// | 2022-02-16 16:41:39 +0000 UTC | -Inf | +// +-------------------------------+-----------------------------------+ +// +// +// +// Frame[4] { +// "type": "timeseries-multi", +// "typeVersion": [ +// 0, +// 0 +// ], +// "custom": { +// "resultType": "vector" +// }, +// "notices": [ +// { +// "text": "info 1" +// }, +// { +// "text": "info 2" +// } +// ] +// } +// Name: +// Dimensions: 2 Fields by 1 Rows +// +-------------------------------+------------------------------------+ +// | Name: Time | Name: Value | +// | Labels: | Labels: level=debug, location=moon | +// | Type: []time.Time | Type: []float64 | +// +-------------------------------+------------------------------------+ +// | 2022-02-16 16:41:39 +0000 UTC | NaN | +// +-------------------------------+------------------------------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "meta": { + "type": "timeseries-multi", + "typeVersion": [ + 0, + 0 + ], + "custom": { + "resultType": "vector" + }, + "notices": [ + { + "text": "info 1" + }, + { + "text": "info 2" + } + ] + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64" + }, + "labels": { + "__name__": "up", + "instance": "localhost:9090", + "job": "prometheus" + } + } + ] + }, + "data": { + "values": [ + [ + 1435781451781 + ], + [ + 1 + ] + ] + } + }, + { + "schema": { + "meta": { + "type": "timeseries-multi", + "typeVersion": [ + 0, + 0 + ], + "custom": { + "resultType": "vector" + }, + "notices": [ + { + "text": "info 1" + }, + { + "text": "info 2" + } + ] + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64" + }, + "labels": { + "__name__": "up", + "instance": "localhost:9100", + "job": "node" + } + } + ] + }, + "data": { + "values": [ + [ + 1435781451781 + ], + [ + 0 + ] + ] + } + }, + { + "schema": { + "meta": { + "type": "timeseries-multi", + "typeVersion": [ + 0, + 0 + ], + "custom": { + "resultType": "vector" + }, + "notices": [ + { + "text": "info 1" + }, + { + "text": "info 2" + } + ] + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64" + }, + "labels": { + "level": "error", + "location": "moon" + } + } + ] + }, + "data": { + "values": [ + [ + 1645029699000 + ], + [ + null + ] + ], + "entities": [ + null, + { + "Inf": [ + 0 + ] + } + ] + } + }, + { + "schema": { + "meta": { + "type": "timeseries-multi", + "typeVersion": [ + 0, + 0 + ], + "custom": { + "resultType": "vector" + }, + "notices": [ + { + "text": "info 1" + }, + { + "text": "info 2" + } + ] + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64" + }, + "labels": { + "level": "info", + "location": "moon" + } + } + ] + }, + "data": { + "values": [ + [ + 1645029699000 + ], + [ + null + ] + ], + "entities": [ + null, + { + "NegInf": [ + 0 + ] + } + ] + } + }, + { + "schema": { + "meta": { + "type": "timeseries-multi", + "typeVersion": [ + 0, + 0 + ], + "custom": { + "resultType": "vector" + }, + "notices": [ + { + "text": "info 1" + }, + { + "text": "info 2" + } + ] + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64" + }, + "labels": { + "level": "debug", + "location": "moon" + } + } + ] + }, + "data": { + "values": [ + [ + 1645029699000 + ], + [ + null + ] + ], + "entities": [ + null, + { + "NaN": [ + 0 + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/promlib/converter/testdata/prom-infos-no-data-frame.jsonc b/pkg/promlib/converter/testdata/prom-infos-no-data-frame.jsonc new file mode 100644 index 00000000000..544146e2cce --- /dev/null +++ b/pkg/promlib/converter/testdata/prom-infos-no-data-frame.jsonc @@ -0,0 +1,51 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "notices": [ +// { +// "text": "info 1" +// }, +// { +// "text": "info 2" +// } +// ] +// } +// Name: Warnings +// Dimensions: 0 Fields by 0 Rows +// + +// + +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "name": "Warnings", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "notices": [ + { + "text": "info 1" + }, + { + "text": "info 2" + } + ] + }, + "fields": [] + }, + "data": { + "values": [] + } + } + ] +} \ No newline at end of file diff --git a/pkg/promlib/converter/testdata/prom-infos-no-data.json b/pkg/promlib/converter/testdata/prom-infos-no-data.json new file mode 100644 index 00000000000..425943ab5cc --- /dev/null +++ b/pkg/promlib/converter/testdata/prom-infos-no-data.json @@ -0,0 +1,8 @@ +{ + "status" : "success", + "data" : { + "resultType" : "vector", + "result" : [] + }, + "infos" : ["info 1", "info 2"] + } diff --git a/pkg/promlib/converter/testdata/prom-infos.json b/pkg/promlib/converter/testdata/prom-infos.json new file mode 100644 index 00000000000..2365d4bf9a4 --- /dev/null +++ b/pkg/promlib/converter/testdata/prom-infos.json @@ -0,0 +1,37 @@ +{ + "status" : "success", + "data" : { + "resultType" : "vector", + "result" : [ + { + "metric" : { + "__name__" : "up", + "job" : "prometheus", + "instance" : "localhost:9090" + }, + "value": [ 1435781451.781, "1" ] + }, + { + "metric" : { + "__name__" : "up", + "job" : "node", + "instance" : "localhost:9100" + }, + "value" : [ 1435781451.781, "0" ] + }, + { + "metric": { "level": "error", "location": "moon"}, + "value": [1645029699, "+Inf"] + }, + { + "metric": { "level": "info", "location": "moon" }, + "value": [1645029699, "-Inf"] + }, + { + "metric": { "level": "debug", "location": "moon" }, + "value": [1645029699, "NaN"] + } + ] + }, + "infos" : ["info 1", "info 2"] + } diff --git a/public/app/features/query/components/QueryEditorRow.test.tsx b/public/app/features/query/components/QueryEditorRow.test.tsx index f348ceb793d..b10e0617b8e 100644 --- a/public/app/features/query/components/QueryEditorRow.test.tsx +++ b/public/app/features/query/components/QueryEditorRow.test.tsx @@ -170,7 +170,7 @@ describe('filterPanelDataToQuery', () => { }); describe('frame results with warnings', () => { - const meta = { + const metaWarning = { notices: [ { severity: 'warning', @@ -178,25 +178,77 @@ describe('frame results with warnings', () => { }, ], }; + const metaInfo = { + notices: [ + { + text: 'For your info, something is up.', + }, + ], + }; + const metaWarningAndInfo = { + notices: [ + { + severity: 'warning', + text: 'Reduce operation is not needed. Input query or expression A is already reduced data.', + }, + { + text: 'For your info, something is up.', + }, + ], + }; - const dataWithWarnings: PanelData = { + const dataWithWarningsAndInfo: PanelData = { state: LoadingState.Done, series: [ toDataFrame({ refId: 'B', fields: [{ name: 'B1' }], - meta, + meta: metaWarningAndInfo, }), toDataFrame({ refId: 'B', fields: [{ name: 'B2' }], - meta, + meta: metaWarningAndInfo, }), ], timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } }, }; - const dataWithoutWarnings: PanelData = { + const dataWithWarningsOnly: PanelData = { + state: LoadingState.Done, + series: [ + toDataFrame({ + refId: 'B', + fields: [{ name: 'B1' }], + meta: metaWarning, + }), + toDataFrame({ + refId: 'B', + fields: [{ name: 'B2' }], + meta: metaWarning, + }), + ], + timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } }, + }; + + const dataWithInfosOnly: PanelData = { + state: LoadingState.Done, + series: [ + toDataFrame({ + refId: 'B', + fields: [{ name: 'B1' }], + meta: metaInfo, + }), + toDataFrame({ + refId: 'B', + fields: [{ name: 'B2' }], + meta: metaInfo, + }), + ], + timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } }, + }; + + const dataWithoutWarningsOrInfo: PanelData = { state: LoadingState.Done, series: [ toDataFrame({ @@ -213,33 +265,79 @@ describe('frame results with warnings', () => { timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } }, }; - it('should show a warning badge and de-duplicate warning messages', () => { + it('should show both badges and de-duplicate messages', () => { // @ts-ignore: there are _way_ too many props to inject here :( const editorRow = new QueryEditorRow({ - data: dataWithWarnings, + data: dataWithWarningsAndInfo, query: { refId: 'B', }, }); - const warningsComponent = editorRow.renderWarnings(); + const warningsComponent = editorRow.renderWarnings('warning'); expect(warningsComponent).not.toBe(null); + const infosComponent = editorRow.renderWarnings('info'); + expect(infosComponent).not.toBe(null); + + render(warningsComponent!); + render(infosComponent!); + expect(screen.getByText('1 warning')).toBeInTheDocument(); + expect(screen.getByText('1 info')).toBeInTheDocument(); + }); + + it('should show a warning badge and de-duplicate warning messages', () => { + // @ts-ignore: there are _way_ too many props to inject here :( + const editorRow = new QueryEditorRow({ + data: dataWithWarningsOnly, + query: { + refId: 'B', + }, + }); + + const warningsComponent = editorRow.renderWarnings('warning'); + expect(warningsComponent).not.toBe(null); + + const infosComponent = editorRow.renderWarnings('info'); + expect(infosComponent).toBe(null); + render(warningsComponent!); expect(screen.getByText('1 warning')).toBeInTheDocument(); }); - it('should not show a warning badge when there are no warnings', () => { + it('should show an info badge and de-duplicate info messages', () => { // @ts-ignore: there are _way_ too many props to inject here :( const editorRow = new QueryEditorRow({ - data: dataWithoutWarnings, + data: dataWithInfosOnly, query: { refId: 'B', }, }); - const warningsComponent = editorRow.renderWarnings(); + const warningsComponent = editorRow.renderWarnings('warning'); expect(warningsComponent).toBe(null); + + const infosComponent = editorRow.renderWarnings('info'); + expect(infosComponent).not.toBe(null); + + render(infosComponent!); + expect(screen.getByText('1 info')).toBeInTheDocument(); + }); + + it('should not show any badge when there are no warnings or info', () => { + // @ts-ignore: there are _way_ too many props to inject here :( + const editorRow = new QueryEditorRow({ + data: dataWithoutWarningsOrInfo, + query: { + refId: 'B', + }, + }); + + const warningsComponent = editorRow.renderWarnings('warning'); + expect(warningsComponent).toBe(null); + + const infosComponent = editorRow.renderWarnings('info'); + expect(infosComponent).toBe(null); }); }); describe('QueryEditorRow', () => { diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 959fd40e73e..143a5b8a384 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -389,7 +389,7 @@ export class QueryEditorRow extends PureComponent { + renderWarnings = (type: string): JSX.Element | null => { const { data, query } = this.props; const dataFilteredByRefId = filterPanelDataToQuery(data, query.refId)?.series ?? []; @@ -398,7 +398,17 @@ export class QueryEditorRow extends PureComponent item.severity === 'warning'; + } else { + // The first condition is because sometimes info notices are not marked as info. + // We don't filter on severity not being equal to warnings because there's still + // the error severity, which does not seem to be used as errors are indicated + // separately, but we do this just to be safe. + criterion = (item: QueryResultMetaNotice) => !('severity' in item) || item.severity === 'info'; + } + const warnings = filter(serie.meta.notices, criterion) ?? []; return acc.concat(warnings); }, []); @@ -409,16 +419,20 @@ export class QueryEditorRow extends PureComponent warning.text).join('\n'); return ( - {uniqueWarnings.length} {pluralize('warning', uniqueWarnings.length)} + {uniqueWarnings.length} {pluralize(type, uniqueWarnings.length)} } tooltip={serializedWarnings} @@ -450,7 +464,8 @@ export class QueryEditorRow extends PureComponent