mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: Implement region annotation (#22225)
This commit is contained in:
parent
6ad8375293
commit
4cf765839a
@ -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', () => {
|
|||||||
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>
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
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>
|
|||||||
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user