Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions prometheus/rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default createConfigForPlugin({
'./PrometheusLabelNamesVariable': './src/plugins/PrometheusLabelNamesVariable.tsx',
'./PrometheusPromQLVariable': './src/plugins/PrometheusPromQLVariable.tsx',
'./PrometheusExplorer': './src/explore/PrometheusExplorer.tsx',
'./PrometheusPromQLAnnotation': './src/annotations/PrometheusPromQLAnnotation.tsx',
},
shared: {
react: { requiredVersion: '18.2.0', singleton: true },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package model

import (
"strings"
promDs "github.com/perses/plugins/prometheus/schemas/datasource:model"
)

kind: "PrometheusPromQLAnnotation"
spec: close({
promDs.#selector
expr: strings.MinRunes(1)
title?: string
legend?: string
tags?: [string]
})
29 changes: 29 additions & 0 deletions prometheus/src/annotations/PrometheusPromQLAnnotation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the \"License\");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an \"AS IS\" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { AnnotationPlugin, AnnotationQueryQueryPluginDependencies, parseVariables } from '@perses-dev/plugin-system';
import { PrometheusPromQLAnnotationOptions } from '../plugins';
import { PrometheusPromQLAnnotationOptionEditor } from './PrometheusPromQLAnnotationOptionEditor';
import { getAnnotationData } from './get-annotation-data';

export const PrometheusPromQLAnnotation: AnnotationPlugin<PrometheusPromQLAnnotationOptions> = {
getAnnotationData: getAnnotationData,
dependsOn: (spec: PrometheusPromQLAnnotationOptions): AnnotationQueryQueryPluginDependencies => {
const queryVariables = parseVariables(spec.expr);
return {
variables: [...queryVariables],
};
},
OptionsEditorComponent: PrometheusPromQLAnnotationOptionEditor,
createInitialOptions: () => ({ expr: '' }),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the \"License\");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an \"AS IS\" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { ReactElement } from 'react';
import { useId } from '@perses-dev/components';
import { produce } from 'immer';
import { Autocomplete, FormControl, Stack, TextField } from '@mui/material';
import {
DatasourceSelect,
DatasourceSelectProps,
DatasourceSelectValue,
OptionsEditorProps,
useDatasourceClient,
useDatasourceSelectValueToSelector,
} from '@perses-dev/plugin-system';
import {
DEFAULT_PROM,
isDefaultPromSelector,
isPrometheusDatasourceSelector,
PROM_DATASOURCE_KIND,
PrometheusClient,
PrometheusDatasourceSelector,
} from '../model';

import { PromQLEditor } from '../components';

export interface PrometheusAnnotationsQuerySpec {
expr: string;
datasource?: DatasourceSelectValue<PrometheusDatasourceSelector>;
title?: string;
legend?: string;
tags?: string[];
}

export type PrometheusAnnotationsQueryEditorProps = OptionsEditorProps<PrometheusAnnotationsQuerySpec>;

export function PrometheusPromQLAnnotationOptionEditor(props: PrometheusAnnotationsQueryEditorProps): ReactElement {
const {
onChange,
value,
value: { expr, datasource, title, legend, tags },
isReadonly,
} = props;

const datasourceSelectValue = datasource ?? DEFAULT_PROM;

const datasourceSelectLabelID = useId('prom-datasource-label'); // for panels with multiple queries, this component is rendered multiple times on the same page

const selectedDatasource = useDatasourceSelectValueToSelector(
datasourceSelectValue,
PROM_DATASOURCE_KIND
) as PrometheusDatasourceSelector;

const { data: client } = useDatasourceClient<PrometheusClient>(selectedDatasource);
const promURL = client?.options.datasourceUrl;

const handleDatasourceChange: DatasourceSelectProps['onChange'] = (next) => {
if (isPrometheusDatasourceSelector(next)) {
/* Good to know: The usage of onchange here causes an immediate spec update which eventually updates the panel
This was probably intentional to allow for quick switching between datasources.
Could have been triggered only with Run Query button as well.
*/
onChange(
produce(value, (draft) => {
// If they're using the default, just omit the datasource prop (i.e. set to undefined)
const nextDatasource = isDefaultPromSelector(next) ? undefined : next;
draft.datasource = nextDatasource;
})
);
return;
}

throw new Error('Got unexpected non-Prometheus datasource selector');
};

const handleExprChange = (next: string): void => {
onChange(
produce(value, (draft) => {
draft.expr = next;
})
);
};

const handleTitleChange = (next: string): void => {
onChange(
produce(value, (draft) => {
draft.title = next || undefined;
})
);
};

const handleLegendChange = (next: string): void => {
onChange(
produce(value, (draft) => {
draft.legend = next || undefined;
})
);
};

const handleTagsChange = (next: string[]): void => {
onChange(
produce(value, (draft) => {
draft.tags = next.length > 0 ? next : undefined;
})
);
};

return (
<Stack spacing={2}>
<FormControl margin="dense" fullWidth={false}>
<DatasourceSelect
datasourcePluginKind={PROM_DATASOURCE_KIND}
value={datasourceSelectValue}
onChange={handleDatasourceChange}
labelId={datasourceSelectLabelID}
label="Prometheus Datasource"
notched
readOnly={isReadonly}
/>
</FormControl>
<PromQLEditor
completeConfig={{ remote: { url: promURL } }}
value={expr}
datasource={selectedDatasource}
onChange={handleExprChange}
isReadOnly={isReadonly}
treeViewMetadata={undefined}
/>
<TextField
fullWidth
label="Title"
placeholder="Example: 'Deployment {{service}}'"
helperText="Title displayed in the annotation tooltip. Use {{label_name}} to interpolate label values."
value={title ?? ''}
onChange={(e) => handleTitleChange(e.target.value)}
slotProps={{
inputLabel: { shrink: isReadonly ? true : undefined },
input: { readOnly: isReadonly },
}}
/>
<TextField
fullWidth
label="Legend"
placeholder="Example: '{{instance}}' will generate annotation legends like 'webserver-123'..."
helperText="Text displayed below the title in the annotation tooltip. Use {{label_name}} to interpolate label values."
value={legend ?? ''}
onChange={(e) => handleLegendChange(e.target.value)}
slotProps={{
inputLabel: { shrink: isReadonly ? true : undefined },
input: { readOnly: isReadonly },
}}
/>
<Autocomplete
multiple
freeSolo
options={[]}
value={tags ?? []}
onChange={(_, next) => handleTagsChange(next as string[])}
readOnly={isReadonly}
renderInput={(params) => (
<TextField
{...params}
label="Tags"
placeholder="Add label names..."
helperText="Label names to display as tags in the annotation tooltip. Leave empty to show all labels. Press Enter to add."
/>
)}
/>
</Stack>
);
}
110 changes: 110 additions & 0 deletions prometheus/src/annotations/get-annotation-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the \"License\");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an \"AS IS\" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { AnnotationData } from '@perses-dev/spec';
import { AnnotationContext, datasourceSelectValueToSelector, replaceVariables } from '@perses-dev/plugin-system';
import { DatasourceSpec, parseDurationString } from '@perses-dev/core';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid using core.
All types have been placed either in spec or in shared packages.

import { milliseconds } from 'date-fns';
import { DEFAULT_SCRAPE_INTERVAL, PrometheusDatasourceSpec, PrometheusPromQLAnnotationOptions } from '../plugins';
import { DEFAULT_PROM, getPrometheusTimeRange, getRangeStep, PROM_DATASOURCE_KIND, PrometheusClient } from '../model';
import { formatSeriesName } from '../utils';

export const getAnnotationData = async (
spec: PrometheusPromQLAnnotationOptions,
context: AnnotationContext,
abortSignal?: AbortSignal
): Promise<AnnotationData[]> => {
if (!spec.expr) {
return [];
}

const listDatasourceSelectItems = await context.datasourceStore.listDatasourceSelectItems(PROM_DATASOURCE_KIND);

const datasourceSelector =
datasourceSelectValueToSelector(
spec.datasource ?? DEFAULT_PROM,
context.variableState,
listDatasourceSelectItems
) ?? DEFAULT_PROM;

const client: PrometheusClient = await context.datasourceStore.getDatasourceClient(datasourceSelector);

const datasource = (await context.datasourceStore.getDatasource(
datasourceSelector
)) as DatasourceSpec<PrometheusDatasourceSpec>;

const datasourceScrapeInterval = Math.trunc(
milliseconds(parseDurationString(datasource.plugin.spec.scrapeInterval ?? DEFAULT_SCRAPE_INTERVAL)) / 1000
);

const timeRange = getPrometheusTimeRange(context.absoluteTimeRange);

const step = getRangeStep(timeRange, datasourceScrapeInterval);

const utcOffsetSec = new Date().getTimezoneOffset() * 60;

const alignedStart = Math.floor((timeRange.start + utcOffsetSec) / step) * step - utcOffsetSec;
let alignedEnd = Math.floor((timeRange.end + utcOffsetSec) / step) * step - utcOffsetSec;

/* Ensure end is always greater than start:
If the step is greater than equal to the diff of end and start,
both start, and end will eventually be rounded to the same value,
Consequently, the time range will be zero, which does not return any valid value
*/
if (alignedStart === alignedEnd) {
alignedEnd = alignedStart + step;
console.warn(`Step (${step}) was larger than the time range! end of time range was set accordingly.`);
}

const { data } = await client.rangeQuery(
{
query: replaceVariables(spec.expr, context.variableState),
start: alignedStart,
end: alignedEnd,
step: step,
},
undefined,
abortSignal
);

const result: AnnotationData[] = [];
for (const series of data?.result ?? []) {
const start = series.values[0]?.[0];
const end = series.values[series.values.length - 1]?.[0];

if (start !== undefined && end !== undefined) {
const labels = series.metric ?? {};
const title = spec.title ? formatSeriesName(spec.title, labels) : undefined;
const legend = spec.legend ? formatSeriesName(spec.legend, labels) : undefined;
// If spec.tags is provided, only expose the selected label names as tags.
// Otherwise, expose all labels.
const tags =
spec.tags && spec.tags.length > 0
? spec.tags.reduce<Record<string, string>>((acc, name) => {
const v = labels[name];
if (v !== undefined) acc[name] = v;
return acc;
}, {})
: labels;
result.push({
start: start * 1000,
end: end * 1000,
title: title,
legend: legend,
tags: tags,
});
}
}

return result;
};
14 changes: 14 additions & 0 deletions prometheus/src/annotations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the \"License\");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an \"AS IS\" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

export * from './PrometheusPromQLAnnotation';
1 change: 1 addition & 0 deletions prometheus/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

export * from './annotations';
export * from './components';
export * from './explore';
export { getPluginModule } from './getPluginModule';
Expand Down
8 changes: 8 additions & 0 deletions prometheus/src/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,11 @@ export type PrometheusPromQLVariableOptions = PrometheusVariableOptionsBase & {
// Note: This field is not part of the Prometheus API.
labelName: string;
};

export interface PrometheusPromQLAnnotationOptions {
datasource?: DatasourceSelectValue<PrometheusDatasourceSelector>;
expr: string;
title?: string;
legend?: string;
tags?: string[];
}
Loading
Loading