diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index 7550e596f17..03345e0fc03 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -735,7 +735,7 @@ describe('PrometheusDatasource', () => { }); }); - describe('When performing annotationQuery', () => { + describe('annotationQuery', () => { let results: any; const options: any = { annotation: { @@ -905,6 +905,83 @@ describe('PrometheusDatasource', () => { expect(req.url).toContain(`step=${step}`); }); }); + + describe('region annotations for sectors', () => { + const options: any = { + annotation: { + expr: 'ALERTS{alertstate="firing"}', + tagKeys: 'job', + titleFormat: '{{alertname}}', + textFormat: '{{instance}}', + }, + range: { + from: time({ seconds: 63 }), + to: time({ seconds: 900 }), + }, + }; + + async function runAnnotationQuery(resultValues: Array<[number, string]>) { + const response = { + status: 'success', + data: { + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + values: resultValues, + }, + ], + }, + }, + }; + + options.annotation.useValueForTime = false; + datasourceRequestMock.mockImplementation(() => Promise.resolve(response)); + + return ds.annotationQuery(options); + } + + it('should handle gaps and inactive values', async () => { + const results = await runAnnotationQuery([ + [2 * 60, '1'], + [3 * 60, '1'], + // gap + [5 * 60, '1'], + [6 * 60, '1'], + [7 * 60, '1'], + [8 * 60, '0'], // false --> create new block + [9 * 60, '1'], + ]); + expect(results.map(result => [result.time, result.timeEnd])).toEqual([ + [120000, 180000], + [300000, 420000], + [540000, 540000], + ]); + }); + + it('should handle single region', async () => { + const results = await runAnnotationQuery([ + [2 * 60, '1'], + [3 * 60, '1'], + ]); + expect(results.map(result => [result.time, result.timeEnd])).toEqual([[120000, 180000]]); + }); + + it('should handle 0 active regions', async () => { + const results = await runAnnotationQuery([ + [2 * 60, '0'], + [3 * 60, '0'], + [5 * 60, '0'], + ]); + expect(results.length).toBe(0); + }); + + it('should handle single active value', async () => { + const results = await runAnnotationQuery([[2 * 60, '1']]); + expect(results.map(result => [result.time, result.timeEnd])).toEqual([[120000, 120000]]); + }); + }); }); describe('createAnnotationQueryOptions', () => { diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 996f50ac40e..d73e0679e18 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -538,7 +538,7 @@ export class PrometheusDatasource extends DataSourceApi }; }; - async annotationQuery(options: any) { + async annotationQuery(options: any): Promise { const annotation = options.annotation; const { expr = '', tagKeys = '', titleFormat = '', textFormat = '' } = annotation; @@ -570,35 +570,57 @@ export class PrometheusDatasource extends DataSourceApi return []; } + const step = Math.floor(query.step) * 1000; + response?.data?.data?.result?.forEach(series => { const tags = Object.entries(series.metric) .filter(([k]) => splitKeys.includes(k)) .map(([_k, v]: [string, string]) => v); - const dupCheck: Record = {}; - for (const value of series.values) { - const valueIsTrue = value[1] === '1'; // e.g. ALERTS - if (valueIsTrue || annotation.useValueForTime) { - const event: AnnotationEvent = { - annotation, - title: self.resultTransformer.renderTemplate(titleFormat, series.metric), - tags, - text: self.resultTransformer.renderTemplate(textFormat, series.metric), - }; - - if (annotation.useValueForTime) { - const timestampValue = Math.floor(parseFloat(value[1])); - if (dupCheck[timestampValue]) { - continue; - } - dupCheck[timestampValue] = true; - event.time = timestampValue; - } else { - event.time = Math.floor(parseFloat(value[0])) * 1000; - } - - eventList.push(event); + series.values.forEach((value: any[]) => { + let timestampValue; + // rewrite timeseries to a common format + if (annotation.useValueForTime) { + timestampValue = Math.floor(parseFloat(value[1])); + value[1] = 1; + } else { + timestampValue = Math.floor(parseFloat(value[0])) * 1000; } + value[0] = timestampValue; + }); + + const activeValues = series.values.filter((value: Record) => parseFloat(value[1]) >= 1); + const activeValuesTimestamps = activeValues.map((value: number[]) => value[0]); + + // Instead of creating singular annotation for each active event we group events into region if they are less + // then `step` apart. + let latestEvent: AnnotationEvent = null; + activeValuesTimestamps.forEach((timestamp: number) => { + // We already have event `open` and we have new event that is inside the `step` so we just update the end. + if (latestEvent && latestEvent.timeEnd + step >= timestamp) { + latestEvent.timeEnd = timestamp; + return; + } + + // Event exists but new one is outside of the `step` so we "finish" the current region. + if (latestEvent) { + eventList.push(latestEvent); + } + + // We start a new region. + latestEvent = { + time: timestamp, + timeEnd: timestamp, + annotation, + title: self.resultTransformer.renderTemplate(titleFormat, series.metric), + tags, + text: self.resultTransformer.renderTemplate(textFormat, series.metric), + }; + }); + if (latestEvent) { + // finish up last point if we have one + latestEvent.timeEnd = activeValuesTimestamps[activeValuesTimestamps.length - 1]; + eventList.push(latestEvent); } });