Skip to content
Merged
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
3 changes: 2 additions & 1 deletion pyroscope/schemas/pyroscope-profile-query/query.cue
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
package model

import (
"strings"
ds "github.com/perses/plugins/pyroscope/schemas/datasource:model"
)

kind: "PyroscopeProfileQuery"
spec: close({
ds.#selector
maxNodes?: number
profileType: string
profileType: strings.MinRunes(1)
filters?: [...{
labelName: string
labelValue: string
Expand Down
4 changes: 1 addition & 3 deletions pyroscope/src/components/Service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ export interface ServiceProps {
onChange?(value: string): void;
}

export function Service(props: ServiceProps): ReactElement {
const { datasource, value, onChange } = props;

export function Service({ datasource, value, onChange }: ServiceProps): ReactElement {
const { data: servicesOptions, isLoading: isServicesOptionsLoading } = useServices(datasource);

function handleServiceChange(_event: SyntheticEvent, newValue: string | null): void {
Expand Down
24 changes: 18 additions & 6 deletions pyroscope/src/explore/PyroscopeExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { Box, Stack } from '@mui/material';
import { Alert, Box, Stack } from '@mui/material';
import { ReactElement, useState } from 'react';
import { DataQueriesProvider, MultiQueryEditor } from '@perses-dev/plugin-system';
import { Panel } from '@perses-dev/dashboards';
import { useExplorerManagerContext } from '@perses-dev/explore';
import { QueryDefinition } from '@perses-dev/spec';
import { isProfileQueryComplete, PyroscopeProfileQuerySpec } from '../model';

interface ProfilesExplorerQueryParams {
queries?: QueryDefinition[];
Expand Down Expand Up @@ -53,6 +54,11 @@ export function PyroscopeExplorer(): ReactElement {

const [queryDefinitions, setQueryDefinitions] = useState<QueryDefinition[]>(queries);

// A profile query is only executable once a service and a profile type are selected.
const hasIncompleteQuery = queries.some(
(query) => !isProfileQueryComplete((query.spec?.plugin?.spec ?? {}) as Partial<PyroscopeProfileQuerySpec>)
);

return (
<Stack gap={2} sx={{ width: '100%' }}>
<MultiQueryEditor
Expand All @@ -61,11 +67,17 @@ export function PyroscopeExplorer(): ReactElement {
queries={queryDefinitions}
onQueryRun={() => setData({ queries: queryDefinitions })}
/>
<DataQueriesProvider definitions={queries}>
<Box height={980}>
<FlameGraphPanel queries={queries} />
</Box>
</DataQueriesProvider>
{hasIncompleteQuery ? (
<Alert severity="warning">
The query is not complete. Please select a service and a profile type to run the query.
</Alert>
) : (
<DataQueriesProvider definitions={queries}>
<Box height={980}>
<FlameGraphPanel queries={queries} />
</Box>
</DataQueriesProvider>
)}
</Stack>
);
}
10 changes: 10 additions & 0 deletions pyroscope/src/model/profile-query-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ export interface PyroscopeProfileQuerySpec {
filters?: LabelFilter[];
service?: string;
}

/**
* A Pyroscope profile query needs at least a service and a profile type to be executable.
* When either is missing, the query is considered incomplete and should not be run.
*/
export function isProfileQueryComplete(
spec: Partial<Pick<PyroscopeProfileQuerySpec, 'service' | 'profileType'>>
): boolean {
return !!spec.service && !!spec.profileType;
}
Comment thread
Gladorme marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { DatasourceSelect, DatasourceSelectProps } from '@perses-dev/plugin-system';
import { DatasourceSelect, DatasourceSelectProps, useDatasourceSelectValueToSelector } from '@perses-dev/plugin-system';
import { useId } from '@perses-dev/components';
import { produce } from 'immer';
import { FormControl, InputLabel, Stack, TextField, useTheme } from '@mui/material';
Expand All @@ -21,6 +21,7 @@ import {
isDefaultPyroscopeSelector,
isPyroscopeDatasourceSelector,
PYROSCOPE_DATASOURCE_KIND,
PyroscopeDatasourceSelector,
} from '../../model';
import { ProfileTypeSelector, Service, Filters } from '../../components';
import {
Expand All @@ -34,7 +35,11 @@ import {
export function PyroscopeProfileQueryEditor(props: ProfileQueryEditorProps): ReactElement {
const { onChange, value } = props;
const { datasource } = value;
const selectedDatasource = datasource ?? DEFAULT_PYROSCOPE;
const datasourceSelectValue = datasource ?? DEFAULT_PYROSCOPE;
const selectedDatasource = useDatasourceSelectValueToSelector(
datasourceSelectValue,
PYROSCOPE_DATASOURCE_KIND
) as PyroscopeDatasourceSelector;
const datasourceSelectLabelID = useId('pyroscope-datasource-label');

const { maxNodes, handleMaxNodesChange, maxNodesHasError } = useMaxNodesState(props);
Expand Down Expand Up @@ -67,7 +72,7 @@ export function PyroscopeProfileQueryEditor(props: ProfileQueryEditorProps): Rea
</InputLabel>
<DatasourceSelect
datasourcePluginKind={PYROSCOPE_DATASOURCE_KIND}
value={selectedDatasource}
value={datasourceSelectValue}
onChange={handleDatasourceChange}
labelId={datasourceSelectLabelID}
label="Pyroscope Datasource"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@
import { ProfileQueryPlugin } from '@perses-dev/plugin-system';
import { getUnixTime } from 'date-fns';
import { AbsoluteTimeRange, ProfileData, StackTrace } from '@perses-dev/spec';
import { PyroscopeProfileQuerySpec, PYROSCOPE_DATASOURCE_KIND, PyroscopeDatasourceSelector } from '../../model';
import { PyroscopeClient } from '../../model/pyroscope-client';
import { SearchProfilesParameters, SearchProfilesResponse } from '../../model/api-types';
import {
PyroscopeProfileQuerySpec,
PYROSCOPE_DATASOURCE_KIND,
PyroscopeDatasourceSelector,
PyroscopeClient,
SearchProfilesParameters,
SearchProfilesResponse,
} from '../../model';
import { computeFilterExpr } from '../../utils/types';

export function getUnixTimeRange(timeRange: AbsoluteTimeRange): { start: number; end: number } {
Expand Down
33 changes: 25 additions & 8 deletions pyroscope/src/utils/use-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,23 @@ import {
} from '../model';
import { getUnixTimeRange } from '../plugins';

// Pyroscope need timestamp in milliseconds, but the time range from Perses is in seconds.
const MILLISECONDS = 1_000;

export function useLabelNames(datasource: DatasourceSelector): UseQueryResult<SearchLabelNamesResponse, StatusError> {
const { data: client } = useDatasourceClient<PyroscopeClient>(datasource);
const { absoluteTimeRange } = useTimeRange();
const { start, end } = getUnixTimeRange(absoluteTimeRange);

return useQuery<SearchLabelNamesResponse, StatusError>({
enabled: !!client,
queryKey: ['searchLabelNames', 'datasource', datasource],
queryKey: ['searchLabelNames', client],
queryFn: async () => {
return await client!.searchLabelNames({}, { 'content-type': 'application/json' }, {});
return await client!.searchLabelNames(
{},
{ 'content-type': 'application/json' },
{ start: start * MILLISECONDS, end: end * MILLISECONDS }
);
Comment thread
Gladorme marked this conversation as resolved.
},
});
}
Expand All @@ -46,12 +55,12 @@ export function useLabelValues(

return useQuery<SearchLabelValuesResponse, StatusError>({
enabled: !!client,
queryKey: ['searchLabelValues', labelName, 'datasource', datasource],
queryKey: ['searchLabelValues', labelName, client],
queryFn: async () => {
return await client!.searchLabelValues(
{},
{ 'content-type': 'application/json' },
{ name: labelName, from: start, until: end }
{ name: labelName, start: start * MILLISECONDS, end: end * MILLISECONDS }
);
},
});
Expand All @@ -66,9 +75,13 @@ export function useProfileTypes(

return useQuery<SearchProfileTypesResponse, StatusError>({
enabled: !!client,
queryKey: ['searchProfileTypes', 'datasource', datasource],
queryKey: ['searchProfileTypes', client],
queryFn: async () => {
return await client!.searchProfileTypes({}, { 'content-type': 'application/json' }, { from: start, until: end });
return await client!.searchProfileTypes(
{},
{ 'content-type': 'application/json' },
{ start: start * MILLISECONDS, end: end * MILLISECONDS }
);
},
});
}
Expand All @@ -80,9 +93,13 @@ export function useServices(datasource: DatasourceSelector): UseQueryResult<Sear

return useQuery<SearchLabelValuesResponse, StatusError>({
enabled: !!client,
queryKey: ['searchServices', 'datasource', datasource],
queryKey: ['searchServices', client],
queryFn: async () => {
return await client!.searchServices({}, { 'content-type': 'application/json' }, { from: start, until: end });
return await client!.searchServices(
{},
{ 'content-type': 'application/json' },
{ start: start * MILLISECONDS, end: end * MILLISECONDS }
);
},
});
}
Expand Down
Loading