Files
grafana/packages/grafana-prometheus/src/annotations.ts
T
ismail simsekandGitHub 9156149960 Prometheus: Move to new annotation support as the old one is deprecated (#104578)
* remove deprecated annotation support and introduce the new one

* create annotations.test.ts

* convert the old annotations to new format

* don't override query if it has necessary fields

* a better implementation

* remove comment

* fix

* fix react errors

* unit tests for annotation query editor

* two more tests
2025-05-08 13:32:42 +02:00

134 lines
4.3 KiB
TypeScript

import { Observable, of } from 'rxjs';
import {
AnnotationEvent,
AnnotationQuery,
AnnotationSupport,
DataFrame,
rangeUtil,
renderLegendFormat,
} from '@grafana/data';
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
import { PrometheusDatasource } from './datasource';
import { PromQuery } from './types';
const ANNOTATION_QUERY_STEP_DEFAULT = '60s';
export const PrometheusAnnotationSupport = (ds: PrometheusDatasource): AnnotationSupport<PromQuery> => {
return {
QueryEditor: AnnotationQueryEditor,
prepareAnnotation(json: AnnotationQuery<PromQuery>): AnnotationQuery<PromQuery> {
// Initialize target if it doesn't exist
if (!json.target) {
json.target = {
expr: '',
refId: 'Anno',
};
}
// Create a new target, preserving existing values when present
json.target = {
...json.target,
refId: json.target.refId || json.refId || 'Anno',
expr: json.target.expr || json.expr || '',
interval: json.target.interval || json.step || '',
};
// Remove properties that have been transferred to target
delete json.expr;
delete json.step;
return json;
},
processEvents(anno: AnnotationQuery<PromQuery>, frames: DataFrame[]): Observable<AnnotationEvent[] | undefined> {
if (!frames.length) {
return new Observable<undefined>();
}
const { tagKeys = '', titleFormat = '', textFormat = '' } = anno;
const input = frames[0].meta?.executedQueryString || '';
const regex = /Step:\s*([\d\w]+)/;
const match = input.match(regex);
const stepValue = match ? match[1] : null;
const step = rangeUtil.intervalToSeconds(stepValue || ANNOTATION_QUERY_STEP_DEFAULT) * 1000;
const tagKeysArray = tagKeys.split(',');
const eventList: AnnotationEvent[] = [];
for (const frame of frames) {
if (frame.fields.length === 0) {
continue;
}
const timeField = frame.fields[0];
const valueField = frame.fields[1];
const labels = valueField?.labels || {};
const tags = Object.keys(labels)
.filter((label) => tagKeysArray.includes(label))
.map((label) => labels[label]);
const timeValueTuple: Array<[number, number]> = [];
let idx = 0;
valueField.values.forEach((value: string) => {
let timeStampValue: number;
let valueValue: number;
const time = timeField.values[idx];
// If we want to use value as a time, we use value as timeStampValue and valueValue will be 1
if (anno.useValueForTime) {
timeStampValue = Math.floor(parseFloat(value));
valueValue = 1;
} else {
timeStampValue = Math.floor(parseFloat(time));
valueValue = parseFloat(value);
}
idx++;
timeValueTuple.push([timeStampValue, valueValue]);
});
const activeValues = timeValueTuple.filter((value) => value[1] > 0);
const activeValuesTimestamps = activeValues.map((value) => value[0]);
// Instead of creating singular annotation for each active event we group events into region if they are less
// or equal to `step` apart.
let latestEvent: AnnotationEvent | null = null;
for (const timestamp of activeValuesTimestamps) {
// 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 ?? 0) + step >= timestamp) {
latestEvent.timeEnd = timestamp;
continue;
}
// Event exists but new one is outside of the `step` so we add it to eventList.
if (latestEvent) {
eventList.push(latestEvent);
}
// We start a new region.
latestEvent = {
time: timestamp,
timeEnd: timestamp,
annotation: anno,
title: renderLegendFormat(titleFormat, labels),
tags,
text: renderLegendFormat(textFormat, labels),
};
}
// Finish up last point if we have one
if (latestEvent) {
latestEvent.timeEnd = activeValuesTimestamps[activeValuesTimestamps.length - 1];
eventList.push(latestEvent);
}
}
return of(eventList);
},
};
};