Prometheus datasource: Show info annotations in the UI (#97978)

* Show info annotations from Prometheus datasources in the UI

* Add test data for backend
pull/98489/head
zenador 6 months ago committed by GitHub
parent 228ac25ff4
commit e3e51f6e3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 22
      pkg/promlib/converter/prom.go
  2. 29
      pkg/promlib/converter/prom_test.go
  3. 443
      pkg/promlib/converter/testdata/prom-infos-frame.jsonc
  4. 51
      pkg/promlib/converter/testdata/prom-infos-no-data-frame.jsonc
  5. 8
      pkg/promlib/converter/testdata/prom-infos-no-data.json
  6. 37
      pkg/promlib/converter/testdata/prom-infos.json
  7. 118
      public/app/features/query/components/QueryEditorRow.test.tsx
  8. 29
      public/app/features/query/components/QueryEditorRow.tsx

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

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

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

@ -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": []
}
}
]
}

@ -0,0 +1,8 @@
{
"status" : "success",
"data" : {
"resultType" : "vector",
"result" : []
},
"infos" : ["info 1", "info 2"]
}

@ -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"]
}

@ -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 dataWithWarningsAndInfo: PanelData = {
state: LoadingState.Done,
series: [
toDataFrame({
refId: 'B',
fields: [{ name: 'B1' }],
meta: metaWarningAndInfo,
}),
toDataFrame({
refId: 'B',
fields: [{ name: 'B2' }],
meta: metaWarningAndInfo,
}),
],
timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } },
};
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 dataWithWarnings: PanelData = {
const dataWithInfosOnly: PanelData = {
state: LoadingState.Done,
series: [
toDataFrame({
refId: 'B',
fields: [{ name: 'B1' }],
meta,
meta: metaInfo,
}),
toDataFrame({
refId: 'B',
fields: [{ name: 'B2' }],
meta,
meta: metaInfo,
}),
],
timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } },
};
const dataWithoutWarnings: PanelData = {
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 both badges and de-duplicate messages', () => {
// @ts-ignore: there are _way_ too many props to inject here :(
const editorRow = new QueryEditorRow({
data: dataWithWarningsAndInfo,
query: {
refId: 'B',
},
});
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: dataWithWarnings,
data: dataWithWarningsOnly,
query: {
refId: 'B',
},
});
const warningsComponent = editorRow.renderWarnings();
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', () => {

@ -389,7 +389,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
return null;
}
renderWarnings = (): JSX.Element | null => {
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<TQuery extends DataQuery> extends PureComponent<Prop
return acc;
}
const warnings = filter(serie.meta.notices, { severity: 'warning' }) ?? [];
let criterion;
if (type === 'warning') {
criterion = (item: QueryResultMetaNotice) => 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<TQuery extends DataQuery> extends PureComponent<Prop
return null;
}
const key = 'query-' + type + 's';
const colour = type === 'warning' ? 'orange' : 'blue';
const iconName = type === 'warning' ? 'exclamation-triangle' : 'file-landscape-alt';
const serializedWarnings = uniqueWarnings.map((warning) => warning.text).join('\n');
return (
<Badge
key="query-warning"
color="orange"
icon="exclamation-triangle"
key={key}
color={colour}
icon={iconName}
text={
<>
{uniqueWarnings.length} {pluralize('warning', uniqueWarnings.length)}
{uniqueWarnings.length} {pluralize(type, uniqueWarnings.length)}
</>
}
tooltip={serializedWarnings}
@ -450,7 +464,8 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
)
.filter(Boolean);
extraActions.push(this.renderWarnings());
extraActions.push(this.renderWarnings('info'));
extraActions.push(this.renderWarnings('warning'));
return extraActions;
};

Loading…
Cancel
Save