Phlare: Support both Phlare and Pyroscope backends (#66989)

This commit is contained in:
Andrej Ocenas
2023-04-25 16:08:18 +02:00
committed by GitHub
parent 2f1a08511a
commit 63777ea368
26 changed files with 1249 additions and 496 deletions
@@ -1,15 +1,52 @@
import React from 'react';
import { useAsyncFn, useDebounce } from 'react-use';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { DataSourceHttpSettings, EventsWithValidation, LegacyForms, regexValidation } from '@grafana/ui';
import { DataSourcePluginOptionsEditorProps, SelectableValue } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { Alert, DataSourceHttpSettings, EventsWithValidation, LegacyForms, regexValidation } from '@grafana/ui';
import { config } from 'app/core/config';
import { PhlareDataSourceOptions } from './types';
import { PhlareDataSource } from './datasource';
import { BackendType, PhlareDataSourceOptions } from './types';
interface Props extends DataSourcePluginOptionsEditorProps<PhlareDataSourceOptions> {}
export const ConfigEditor = (props: Props) => {
const { options, onOptionsChange } = props;
const [mismatchedBackendType, setMismatchedBackendType] = React.useState<BackendType | undefined>();
const dataSourceSrv = getDataSourceSrv();
const [, getBackendType] = useAsyncFn(async () => {
if (!options.url) {
return;
}
const ds = await dataSourceSrv.get({ type: options.type, uid: options.uid });
if (!(ds instanceof PhlareDataSource)) {
// Should not happen, makes TS happy
throw new Error('Datasource is not a PhlareDataSource');
}
const { backendType } = await ds.getBackendType(options.url);
if (backendType === 'unknown') {
setMismatchedBackendType(undefined);
return;
}
// If user already has something selected don't overwrite but show warning.
if (options.jsonData.backendType) {
if (backendType !== options.jsonData.backendType) {
setMismatchedBackendType(backendType);
} else {
setMismatchedBackendType(undefined);
}
return;
}
onOptionsChange({ ...options, jsonData: { ...options.jsonData, backendType } });
}, [options]);
useDebounce(getBackendType, 500, [options]);
return (
<>
@@ -57,7 +94,50 @@ export const ConfigEditor = (props: Props) => {
/>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<LegacyForms.FormField
label="Backend type"
labelWidth={13}
inputEl={
<LegacyForms.Select<BackendType>
allowCustomValue={false}
value={options.jsonData.backendType ? backendTypeOptions[options.jsonData.backendType] : undefined}
options={Object.values(backendTypeOptions)}
onChange={(option) => {
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
backendType: option.value,
},
});
}}
/>
}
tooltip="Select what type of backend you use. This datasource supports both Phlare and Pyroscope backends."
/>
</div>
</div>
{mismatchedBackendType && (
<Alert
title={`"${options.jsonData.backendType}" option is selected but it seems like you are using "${mismatchedBackendType}" backend.`}
severity="warning"
/>
)}
</div>
</>
);
};
const backendTypeOptions: Record<BackendType, SelectableValue<BackendType>> = {
phlare: {
label: 'Phlare',
value: 'phlare',
},
pyroscope: {
label: 'Pyroscope',
value: 'pyroscope',
},
};
@@ -1,11 +1,10 @@
import { css } from '@emotion/css';
import React, { useEffect, useRef } from 'react';
import { useLatest } from 'react-use';
import { useAsync, useLatest } from 'react-use';
import { CodeEditor, Monaco, useStyles2, monacoTypes } from '@grafana/ui';
import { languageDefinition } from '../phlareql';
import { SeriesMessage } from '../types';
import { CompletionProvider } from './autocomplete';
@@ -13,11 +12,12 @@ interface Props {
value: string;
onChange: (val: string) => void;
onRunQuery: (value: string) => void;
series?: SeriesMessage;
labels?: string[];
getLabelValues: (label: string) => Promise<string[]>;
}
export function LabelsEditor(props: Props) {
const setupAutocompleteFn = useAutocomplete(props.series);
const setupAutocompleteFn = useAutocomplete(props.getLabelValues, props.labels);
const styles = useStyles2(getStyles);
const onRunQueryRef = useLatest(props.onRunQuery);
@@ -92,15 +92,17 @@ const EDITOR_HEIGHT_OFFSET = 2;
/**
* Hook that returns function that will set up monaco autocomplete for the label selector
*/
function useAutocomplete(series?: SeriesMessage) {
const providerRef = useRef<CompletionProvider>(new CompletionProvider());
function useAutocomplete(getLabelValues: (label: string) => Promise<string[]>, labels?: string[]) {
const providerRef = useRef<CompletionProvider>();
if (providerRef.current === undefined) {
providerRef.current = new CompletionProvider();
}
useEffect(() => {
if (series) {
// When we have the value we will pass it to the CompletionProvider
providerRef.current.setSeries(series);
useAsync(async () => {
if (providerRef.current) {
providerRef.current.init(labels || [], getLabelValues);
}
}, [series]);
}, [labels, getLabelValues]);
const autocompleteDisposeFun = useRef<(() => void) | null>(null);
useEffect(() => {
@@ -112,11 +114,13 @@ function useAutocomplete(series?: SeriesMessage) {
// This should be run in monaco onEditorDidMount
return (editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => {
providerRef.current.editor = editor;
providerRef.current.monaco = monaco;
if (providerRef.current) {
providerRef.current.editor = editor;
providerRef.current.monaco = monaco;
const { dispose } = monaco.languages.registerCompletionItemProvider(langId, providerRef.current);
autocompleteDisposeFun.current = dispose;
const { dispose } = monaco.languages.registerCompletionItemProvider(langId, providerRef.current);
autocompleteDisposeFun.current = dispose;
}
};
}
@@ -138,7 +142,7 @@ const getStyles = () => {
return {
queryField: css`
flex: 1;
// Not exactly sure but without this the editor doe not shrink after resizing (so you can make it bigger but not
// Not exactly sure but without this the editor does not shrink after resizing (so you can make it bigger but not
// smaller). At the same time this does not actually make the editor 100px because it has flex 1 so I assume
// this should sort of act as a flex-basis (but flex-basis does not work for this). So yeah CSS magic.
width: 100px;
@@ -76,20 +76,12 @@ function setup(options: { props: Partial<Props> } = { props: {} }) {
ds.getProfileTypes = jest.fn().mockResolvedValue([
{
name: 'process_cpu',
ID: 'process_cpu:cpu',
period_type: 'day',
period_unit: 's',
sample_unit: 'ms',
sample_type: 'cpu',
label: 'process_cpu - cpu',
id: 'process_cpu:cpu',
},
{
name: 'memory',
ID: 'memory:memory',
period_type: 'day',
period_unit: 's',
sample_unit: 'ms',
sample_type: 'memory',
label: 'memory',
id: 'memory:memory',
},
] as ProfileTypeMessage[]);
@@ -1,13 +1,13 @@
import { defaults } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useAsync } from 'react-use';
import { CoreApp, QueryEditorProps } from '@grafana/data';
import { CoreApp, QueryEditorProps, TimeRange } from '@grafana/data';
import { ButtonCascader, CascaderOption } from '@grafana/ui';
import { defaultPhlare, defaultPhlareQueryType, Phlare } from '../dataquery.gen';
import { defaultGrafanaPyroscope, defaultPhlareQueryType, GrafanaPyroscope } from '../dataquery.gen';
import { PhlareDataSource } from '../datasource';
import { PhlareDataSourceOptions, ProfileTypeMessage, Query } from '../types';
import { BackendType, PhlareDataSourceOptions, ProfileTypeMessage, Query } from '../types';
import { EditorRow } from './EditorRow';
import { EditorRows } from './EditorRows';
@@ -16,44 +16,32 @@ import { QueryOptions } from './QueryOptions';
export type Props = QueryEditorProps<PhlareDataSource, Query, PhlareDataSourceOptions>;
export const defaultQuery: Partial<Phlare> = {
...defaultPhlare,
export const defaultQuery: Partial<GrafanaPyroscope> = {
...defaultGrafanaPyroscope,
queryType: defaultPhlareQueryType,
};
export function QueryEditor(props: Props) {
const profileTypes = useProfileTypes(props.datasource);
function onProfileTypeChange(value: string[], selectedOptions: CascaderOption[]) {
if (selectedOptions.length === 0) {
return;
}
const id = selectedOptions[selectedOptions.length - 1].value;
if (typeof id !== 'string') {
throw new Error('id is not string');
}
props.onChange({ ...props.query, profileTypeId: id });
}
function onLabelSelectorChange(value: string) {
props.onChange({ ...props.query, labelSelector: value });
}
let query = normalizeQuery(props.query, props.app);
function handleRunQuery(value: string) {
props.onChange({ ...props.query, labelSelector: value });
props.onRunQuery();
}
const seriesResult = useAsync(() => {
return props.datasource.getSeries();
}, [props.datasource]);
const { profileTypes, onProfileTypeChange, selectedProfileName } = useProfileTypes(
props.datasource,
props.query,
props.onChange,
props.datasource.backendType
);
const { labels, getLabelValues, onLabelSelectorChange } = useLabels(
props.range,
props.datasource,
props.query,
props.onChange
);
const cascaderOptions = useCascaderOptions(profileTypes);
const selectedProfileName = useProfileName(profileTypes, props.query.profileTypeId);
let query = normalizeQuery(props.query, props.app);
return (
<EditorRows>
@@ -65,61 +53,144 @@ export function QueryEditor(props: Props) {
value={query.labelSelector}
onChange={onLabelSelectorChange}
onRunQuery={handleRunQuery}
series={seriesResult.value}
labels={labels}
getLabelValues={getLabelValues}
/>
</EditorRow>
<EditorRow>
<QueryOptions query={query} onQueryChange={props.onChange} app={props.app} series={seriesResult.value} />
<QueryOptions query={query} onQueryChange={props.onChange} app={props.app} labels={labels} />
</EditorRow>
</EditorRows>
);
}
function useLabels(
range: TimeRange | undefined,
datasource: PhlareDataSource,
query: Query,
onChange: (value: Query) => void
) {
// Round to nearest 5 seconds. If the range is something like last 1h then every render the range values change slightly
// and what ever has range as dependency is rerun. So this effectively debounces the queries.
const unpreciseRange = {
to: Math.ceil((range?.to.valueOf() || 0) / 5000) * 5000,
from: Math.floor((range?.from.valueOf() || 0) / 5000) * 5000,
};
const labelsResult = useAsync(() => {
return datasource.getLabelNames(query.profileTypeId + query.labelSelector, unpreciseRange.from, unpreciseRange.to);
}, [datasource, query.profileTypeId, query.labelSelector, unpreciseRange.to, unpreciseRange.from]);
// Create a function with range and query already baked in so we don't have to send those everywhere
const getLabelValues = useCallback(
(label: string) => {
return datasource.getLabelValues(
query.profileTypeId + query.labelSelector,
label,
unpreciseRange.from,
unpreciseRange.to
);
},
[query, datasource, unpreciseRange.to, unpreciseRange.from]
);
const onLabelSelectorChange = useCallback(
(value: string) => {
onChange({ ...query, labelSelector: value });
},
[onChange, query]
);
return { labels: labelsResult.value, getLabelValues, onLabelSelectorChange };
}
// Turn profileTypes into cascader options
function useCascaderOptions(profileTypes: ProfileTypeMessage[]) {
return useMemo(() => {
let mainTypes = new Map<string, CascaderOption>();
// Classify profile types by name then sample type.
for (let profileType of profileTypes) {
if (!mainTypes.has(profileType.name)) {
mainTypes.set(profileType.name, {
label: profileType.name,
value: profileType.ID,
let parts: string[];
// Phlare uses : as delimiter while Pyro uses .
if (profileType.id.indexOf(':') > -1) {
parts = profileType.id.split(':');
} else {
parts = profileType.id.split('.');
const last = parts.pop()!;
parts = [parts.join('.'), last];
}
const [name, type] = parts;
if (!mainTypes.has(name)) {
mainTypes.set(name, {
label: name,
value: profileType.id,
children: [],
});
}
mainTypes.get(profileType.name)?.children?.push({
label: profileType.sample_type,
value: profileType.ID,
mainTypes.get(name)?.children?.push({
label: type,
value: profileType.id,
});
}
return Array.from(mainTypes.values());
}, [profileTypes]);
}
function useProfileTypes(datasource: PhlareDataSource) {
function useProfileTypes(
datasource: PhlareDataSource,
query: Query,
onChange: (value: Query) => void,
backendType: BackendType = 'phlare'
) {
const [profileTypes, setProfileTypes] = useState<ProfileTypeMessage[]>([]);
useEffect(() => {
(async () => {
const profileTypes = await datasource.getProfileTypes();
setProfileTypes(profileTypes);
})();
}, [datasource]);
return profileTypes;
const onProfileTypeChange = useCallback(
(value: string[], selectedOptions: CascaderOption[]) => {
if (selectedOptions.length === 0) {
return;
}
const id = selectedOptions[selectedOptions.length - 1].value;
// Probably cannot happen but makes TS happy
if (typeof id !== 'string') {
throw new Error('id is not string');
}
onChange({ ...query, profileTypeId: id });
},
[onChange, query]
);
const selectedProfileName = useProfileName(profileTypes, query.profileTypeId, backendType);
return { profileTypes, onProfileTypeChange, selectedProfileName };
}
function useProfileName(profileTypes: ProfileTypeMessage[], profileTypeId: string) {
function useProfileName(profileTypes: ProfileTypeMessage[], profileTypeId: string, backendType: BackendType) {
return useMemo(() => {
if (!profileTypes) {
return 'Loading';
}
const profile = profileTypes.find((type) => type.ID === profileTypeId);
const profile = profileTypes.find((type) => type.id === profileTypeId);
if (!profile) {
if (backendType === 'pyroscope') {
return 'Select application';
}
return 'Select a profile type';
}
return profile.name + ' - ' + profile.sample_type;
}, [profileTypeId, profileTypes]);
return profile.label;
}, [profileTypeId, profileTypes, backendType]);
}
export function normalizeQuery(query: Query, app?: CoreApp | string) {
@@ -5,7 +5,7 @@ import { useToggle } from 'react-use';
import { CoreApp, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Icon, useStyles2, RadioButtonGroup, MultiSelect } from '@grafana/ui';
import { Query, SeriesMessage } from '../types';
import { Query } from '../types';
import { EditorField } from './EditorField';
import { Stack } from './Stack';
@@ -14,7 +14,7 @@ export interface Props {
query: Query;
onQueryChange: (query: Query) => void;
app?: CoreApp;
series?: SeriesMessage;
labels?: string[];
}
const typeOptions: Array<{ value: Query['queryType']; label: string; description: string }> = [
@@ -30,28 +30,19 @@ function getTypeOptions(app?: CoreApp) {
return typeOptions.filter((option) => option.value !== 'both');
}
function getGroupByOptions(series?: SeriesMessage) {
let options: SelectableValue[] = [];
if (series) {
const labels = series.flatMap((val) => {
return val.labels.map((l) => l.name);
});
options = Array.from(new Set(labels)).map((l) => ({
label: l,
value: l,
}));
}
return options;
}
/**
* Base on QueryOptionGroup component from grafana/ui but that is not available yet.
*/
export function QueryOptions({ query, onQueryChange, app, series }: Props) {
export function QueryOptions({ query, onQueryChange, app, labels }: Props) {
const [isOpen, toggleOpen] = useToggle(false);
const styles = useStyles2(getStyles);
const typeOptions = getTypeOptions(app);
const groupByOptions = getGroupByOptions(series);
const groupByOptions = labels
? labels.map((l) => ({
label: l,
value: l,
}))
: [];
return (
<Stack gap={0} direction="column">
@@ -1,56 +1,62 @@
import { monacoTypes, Monaco } from '@grafana/ui';
import { SeriesMessage } from '../types';
import { CompletionProvider } from './autocomplete';
describe('CompletionProvider', () => {
it('suggests labels', () => {
it('suggests labels', async () => {
const { provider, model } = setup('{}', 1, defaultLabels);
const result = provider.provideCompletionItems(model, {} as monacoTypes.Position);
const result = await provider.provideCompletionItems(model, {} as monacoTypes.Position);
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
expect.objectContaining({ label: 'foo', insertText: 'foo' }),
]);
});
it('suggests label names with quotes', () => {
it('suggests label names with quotes', async () => {
const { provider, model } = setup('{foo=}', 6, defaultLabels);
const result = provider.provideCompletionItems(model, {} as monacoTypes.Position);
const result = await provider.provideCompletionItems(model, {} as monacoTypes.Position);
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
expect.objectContaining({ label: 'bar', insertText: '"bar"' }),
]);
});
it('suggests label names without quotes', () => {
it('suggests label names without quotes', async () => {
const { provider, model } = setup('{foo="}', 7, defaultLabels);
const result = provider.provideCompletionItems(model, {} as monacoTypes.Position);
const result = await provider.provideCompletionItems(model, {} as monacoTypes.Position);
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
expect.objectContaining({ label: 'bar', insertText: 'bar' }),
]);
});
it('suggests nothing without labels', () => {
it('suggests nothing without labels', async () => {
const { provider, model } = setup('{foo="}', 7, []);
const result = provider.provideCompletionItems(model, {} as monacoTypes.Position);
const result = await provider.provideCompletionItems(model, {} as monacoTypes.Position);
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([]);
});
it('suggests labels on empty input', () => {
it('suggests labels on empty input', async () => {
const { provider, model } = setup('', 0, defaultLabels);
const result = provider.provideCompletionItems(model, {} as monacoTypes.Position);
const result = await provider.provideCompletionItems(model, {} as monacoTypes.Position);
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
expect.objectContaining({ label: 'foo', insertText: '{foo="' }),
]);
});
});
const defaultLabels = [{ labels: [{ name: 'foo', value: 'bar' }] }];
const defaultLabels = ['foo'];
function setup(value: string, offset: number, series?: SeriesMessage) {
function setup(value: string, offset: number, labels: string[] = []) {
const provider = new CompletionProvider();
if (series) {
provider.setSeries(series);
}
provider.init(labels, (label) => {
if (labels.length === 0) {
return Promise.resolve([]);
}
const val = { foo: 'bar' }[label];
const result = [];
if (val) {
result.push(val);
}
return Promise.resolve(result);
});
const model = makeModel(value, offset);
provider.monaco = {
Range: {
@@ -1,7 +1,5 @@
import { monacoTypes, Monaco } from '@grafana/ui';
import { SeriesMessage } from '../types';
/**
* Class that implements CompletionItemProvider interface and allows us to provide suggestion for the Monaco
* autocomplete system.
@@ -16,7 +14,13 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
monaco: Monaco | undefined;
editor: monacoTypes.editor.IStandaloneCodeEditor | undefined;
private labels: { [label: string]: Set<string> } = {};
private labels: string[] = [];
private getLabelValues: (label: string) => Promise<string[]> = () => Promise.resolve([]);
init(labels: string[], getLabelValues: (label: string) => Promise<string[]>) {
this.labels = labels;
this.getLabelValues = getLabelValues;
}
provideCompletionItems(
model: monacoTypes.editor.ITextModel,
@@ -35,39 +39,21 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
const { range, offset } = getRangeAndOffset(this.monaco, model, position);
const situation = getSituation(model.getValue(), offset);
const completionItems = this.getCompletions(situation);
// monaco by-default alphabetically orders the items.
// to stop it, we use a number-as-string sortkey,
// so that monaco keeps the order we use
const maxIndexDigits = completionItems.length.toString().length;
const suggestions: monacoTypes.languages.CompletionItem[] = completionItems.map((item, index) => ({
kind: getMonacoCompletionItemKind(item.type, this.monaco!),
label: item.label,
insertText: item.insertText,
sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have
range,
}));
return { suggestions };
}
/**
* We expect the data directly from the request and transform it here. We do some deduplication and turn them into
* object for quicker search as we usually need either a list of label names or values or particular label.
*/
setSeries(series: SeriesMessage) {
this.labels = series.reduce<{ [label: string]: Set<string> }>((acc, serie) => {
const seriesLabels = serie.labels.reduce<{ [label: string]: Set<string> }>((acc, labelValue) => {
acc[labelValue.name] = acc[labelValue.name] || new Set();
acc[labelValue.name].add(labelValue.value);
return acc;
}, {});
for (const label of Object.keys(seriesLabels)) {
acc[label] = new Set([...(acc[label] || []), ...seriesLabels[label]]);
}
return acc;
}, {});
return this.getCompletions(situation).then((completionItems) => {
// monaco by-default alphabetically orders the items.
// to stop it, we use a number-as-string sortkey,
// so that monaco keeps the order we use
const maxIndexDigits = completionItems.length.toString().length;
const suggestions: monacoTypes.languages.CompletionItem[] = completionItems.map((item, index) => ({
kind: getMonacoCompletionItemKind(item.type, this.monaco!),
label: item.label,
insertText: item.insertText,
sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have
range,
}));
return { suggestions };
});
}
/**
@@ -75,17 +61,14 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
* @param situation
* @private
*/
private getCompletions(situation: Situation): Completion[] {
if (!Object.keys(this.labels).length) {
return [];
}
private async getCompletions(situation: Situation): Promise<Completion[]> {
switch (situation.type) {
// Not really sure what would make sense to suggest in this case so just leave it
case 'UNKNOWN': {
return [];
}
case 'EMPTY': {
return Object.keys(this.labels).map((key) => {
return this.labels.map((key) => {
return {
label: key,
insertText: `{${key}="`,
@@ -94,7 +77,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
});
}
case 'IN_LABEL_NAME':
return Object.keys(this.labels).map((key) => {
return this.labels.map((key) => {
return {
label: key,
insertText: key,
@@ -102,7 +85,8 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
};
});
case 'IN_LABEL_VALUE':
return Array.from(this.labels[situation.labelName].values()).map((key) => {
let values = await this.getLabelValues(situation.labelName);
return values.map((key) => {
return {
label: key,
insertText: situation.betweenQuotes ? key : `"${key}"`,
@@ -16,7 +16,7 @@ export type PhlareQueryType = ('metrics' | 'profile' | 'both');
export const defaultPhlareQueryType: PhlareQueryType = 'both';
export interface Phlare extends common.DataQuery {
export interface GrafanaPyroscope extends common.DataQuery {
/**
* Allows to group the results.
*/
@@ -31,7 +31,7 @@ export interface Phlare extends common.DataQuery {
profileTypeId: string;
}
export const defaultPhlare: Partial<Phlare> = {
export const defaultGrafanaPyroscope: Partial<GrafanaPyroscope> = {
groupBy: [],
labelSelector: '{}',
};
@@ -13,14 +13,17 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run
import { extractLabelMatchers, toPromLikeExpr } from '../prometheus/language_utils';
import { normalizeQuery } from './QueryEditor/QueryEditor';
import { PhlareDataSourceOptions, Query, ProfileTypeMessage, SeriesMessage } from './types';
import { PhlareDataSourceOptions, Query, ProfileTypeMessage, BackendType } from './types';
export class PhlareDataSource extends DataSourceWithBackend<Query, PhlareDataSourceOptions> {
backendType: BackendType;
constructor(
instanceSettings: DataSourceInstanceSettings<PhlareDataSourceOptions>,
private readonly templateSrv: TemplateSrv = getTemplateSrv()
) {
super(instanceSettings);
this.backendType = instanceSettings.jsonData.backendType ?? 'phlare';
}
query(request: DataQueryRequest<Query>): Observable<DataQueryResponse> {
@@ -49,13 +52,17 @@ export class PhlareDataSource extends DataSourceWithBackend<Query, PhlareDataSou
return await super.getResource('profileTypes');
}
async getSeries(): Promise<SeriesMessage> {
// For now, we send empty matcher to get all the series
return await super.getResource('series', { matchers: ['{}'] });
async getLabelNames(query: string, start: number, end: number): Promise<string[]> {
return await super.getResource('labelNames', { query, start, end });
}
async getLabelNames(): Promise<string[]> {
return await super.getResource('labelNames');
async getLabelValues(query: string, label: string, start: number, end: number): Promise<string[]> {
return await super.getResource('labelValues', { label, query, start, end });
}
// We need the URL here because it may not be saved on the backend yet when used from config page.
async getBackendType(url: string): Promise<{ backendType: BackendType | 'unknown' }> {
return await super.getResource('backendType', { url });
}
applyTemplateVariables(query: Query, scopedVars: ScopedVars): Query {
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 140.07 151.15"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-8);}.cls-3{fill:url(#linear-gradient-3);}.cls-4{fill:url(#linear-gradient-4);}.cls-5{fill:url(#linear-gradient-2);}.cls-6{fill:url(#linear-gradient-6);}.cls-7{fill:url(#linear-gradient-7);}.cls-8{fill:url(#linear-gradient-5);}</style><linearGradient id="linear-gradient" x1="556.29" y1="168.71" x2="674.41" y2="28.91" gradientTransform="translate(-556.16) skewX(-8)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffef00"/><stop offset="1" stop-color="#ed5a27"/></linearGradient><linearGradient id="linear-gradient-2" x1="524.45" y1="141.81" x2="642.57" y2="2.01" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-3" x1="546.04" y1="160.05" x2="664.16" y2="20.25" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-4" x1="561.98" y1="173.52" x2="680.1" y2="33.71" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-5" x1="535.6" y1="121.34" x2="655.87" y2="118.41" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-6" x1="536.16" y1="144.33" x2="656.43" y2="141.41" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-7" x1="613.28" y1="33.92" x2="517.45" y2="-5.31" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-8" x1="613.28" y1="33.92" x2="517.45" y2="-5.31" xlink:href="#linear-gradient"/></defs><g id="Layer_1-2"><g><g><path class="cls-1" d="M126.18,82.3c6.34-7.66,10.91-16.78,12.86-26.62h-39.74c-2.18,5.73-7.94,10.01-14.09,10.01H13.33l-2.34,16.61H126.18Z"/><path class="cls-5" d="M42.55,13.99l6.66,2.7h81.13C122.18,6.5,109.49,.12,94.43,.12H29.32c2.23,6.34,6.94,11.32,13.23,13.87Z"/><path class="cls-3" d="M48.24,22.87l-7.32,2.66c-7.06,2.57-13.22,7.62-17.23,14.04H88.89c6.12,0,10.65,4.23,11.26,9.92h39.75c.8-9.84-1.22-18.96-5.43-26.62H48.24Z"/><path class="cls-4" d="M10.13,88.48l-2.11,15.01-.23,1.65H79.67c15.1,0,29.63-6.41,40.66-16.66H10.13Z"/><polygon class="cls-8" points="44.06 128.3 46.45 111.32 6.92 111.32 4.53 128.3 44.06 128.3"/><polygon class="cls-6" points="3.66 134.48 1.32 151.15 40.85 151.15 43.19 134.48 3.66 134.48"/></g><g><path class="cls-7" d="M39.52,19.76C30.96,16.29,24.74,9.05,22.54,0,17.8,9.05,9.53,16.29,0,19.76c8.56,3.47,14.78,10.72,16.98,19.76,4.74-9.05,13-16.29,22.54-19.76Z"/><path class="cls-2" d="M22.54,0c2.2,9.05,8.43,16.29,16.98,19.76-9.53,3.47-17.8,10.72-22.54,19.76C14.78,30.48,8.56,23.23,0,19.76,9.53,16.29,17.8,9.05,22.54,0"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -1,6 +1,6 @@
{
"type": "datasource",
"name": "Phlare",
"name": "Grafana Pyroscope",
"id": "phlare",
"category": "profiling",
@@ -13,15 +13,15 @@
"backend": true,
"info": {
"description": "Horizontally-scalable, highly-available, multi-tenant continuous profiling aggregation system. OSS profiling solution from Grafana Labs.",
"description": "Supports Phlare and Pyroscope backends, horizontally-scalable, highly-available, multi-tenant continuous profiling aggregation systems.",
"author": {
"name": "Grafana Labs",
"url": "https://www.grafana.com"
},
"keywords": ["grafana", "datasource", "phlare", "flamegraph"],
"keywords": ["grafana", "datasource", "phlare", "flamegraph", "profiling", "continuous profiling", "pyroscope"],
"logos": {
"small": "img/phlare_icon_color.svg",
"large": "img/phlare_icon_color.svg"
"small": "img/grafana_pyroscope_icon.svg",
"large": "img/grafana_pyroscope_icon.svg"
},
"links": [
{
+7 -10
View File
@@ -1,25 +1,22 @@
import { DataSourceJsonData } from '@grafana/data';
import { Phlare as PhlareBase, PhlareQueryType } from './dataquery.gen';
import { GrafanaPyroscope, PhlareQueryType } from './dataquery.gen';
export interface Query extends PhlareBase {
export interface Query extends GrafanaPyroscope {
queryType: PhlareQueryType;
}
export interface ProfileTypeMessage {
ID: string;
name: string;
period_type: string;
period_unit: string;
sample_type: string;
sample_unit: string;
id: string;
label: string;
}
export type SeriesMessage = Array<{ labels: Array<{ name: string; value: string }> }>;
/**
* These are options configured for each DataSource instance.
*/
export interface PhlareDataSourceOptions extends DataSourceJsonData {
minStep?: string;
backendType?: BackendType; // if not set we assume it's phlare
}
export type BackendType = 'phlare' | 'pyroscope';