Commit 4cf76583 by Sebastian Poxhofer Committed by GitHub

Prometheus: Implement region annotation (#22225)

parent 6ad83752
...@@ -735,7 +735,7 @@ describe('PrometheusDatasource', () => { ...@@ -735,7 +735,7 @@ describe('PrometheusDatasource', () => {
}); });
}); });
describe('When performing annotationQuery', () => { describe('annotationQuery', () => {
let results: any; let results: any;
const options: any = { const options: any = {
annotation: { annotation: {
...@@ -905,6 +905,83 @@ describe('PrometheusDatasource', () => { ...@@ -905,6 +905,83 @@ describe('PrometheusDatasource', () => {
expect(req.url).toContain(`step=${step}`); 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', () => { describe('createAnnotationQueryOptions', () => {
......
...@@ -538,7 +538,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions> ...@@ -538,7 +538,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
}; };
}; };
async annotationQuery(options: any) { async annotationQuery(options: any): Promise<AnnotationEvent[]> {
const annotation = options.annotation; const annotation = options.annotation;
const { expr = '', tagKeys = '', titleFormat = '', textFormat = '' } = annotation; const { expr = '', tagKeys = '', titleFormat = '', textFormat = '' } = annotation;
...@@ -570,35 +570,57 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions> ...@@ -570,35 +570,57 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
return []; return [];
} }
const step = Math.floor(query.step) * 1000;
response?.data?.data?.result?.forEach(series => { response?.data?.data?.result?.forEach(series => {
const tags = Object.entries(series.metric) const tags = Object.entries(series.metric)
.filter(([k]) => splitKeys.includes(k)) .filter(([k]) => splitKeys.includes(k))
.map(([_k, v]: [string, string]) => v); .map(([_k, v]: [string, string]) => v);
const dupCheck: Record<number, boolean> = {}; series.values.forEach((value: any[]) => {
for (const value of series.values) { let timestampValue;
const valueIsTrue = value[1] === '1'; // e.g. ALERTS // rewrite timeseries to a common format
if (valueIsTrue || annotation.useValueForTime) { if (annotation.useValueForTime) {
const event: AnnotationEvent = { timestampValue = Math.floor(parseFloat(value[1]));
annotation, value[1] = 1;
title: self.resultTransformer.renderTemplate(titleFormat, series.metric), } else {
tags, timestampValue = Math.floor(parseFloat(value[0])) * 1000;
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);
} }
value[0] = timestampValue;
});
const activeValues = series.values.filter((value: Record<number, string>) => 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);
} }
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment