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
11 changes: 9 additions & 2 deletions pyroscope/src/components/FilterItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ import { Operator } from './Operator';

export interface FilterItemProps {
datasource: PyroscopeDatasourceSelector;
labelSelector: string;
value: LabelFilter;
onChange?: (value: LabelFilter) => void;
deleteItem?: () => void; // this function is used to delete the current filter
}

export function FilterItem(props: FilterItemProps): ReactElement {
const { datasource, value, onChange, deleteItem } = props;
const { datasource, labelSelector, value, onChange, deleteItem } = props;

const handleLabelNameChange = (label: string): void => {
onChange?.({ labelName: label, labelValue: '', operator: value.operator });
Expand All @@ -55,14 +56,20 @@ export function FilterItem(props: FilterItemProps): ReactElement {
>
<Grid container sx={{ width: '100%' }}>
<Grid size={{ xs: 9.5, md: 4.5 }}>
<LabelName datasource={datasource} value={value.labelName} onChange={handleLabelNameChange} />
<LabelName
datasource={datasource}
labelSelector={labelSelector}
value={value.labelName}
onChange={handleLabelNameChange}
/>
</Grid>
<Grid size={{ xs: 2.5, md: 1.5 }}>
<Operator value={value.operator} onChange={handleOperatorChange} />
</Grid>
<Grid size={{ xs: 10, md: 5 }}>
<LabelValue
datasource={datasource}
labelSelector={labelSelector}
value={value.labelValue}
labelName={value.labelName}
onChange={handleLabelValueChange}
Expand Down
7 changes: 6 additions & 1 deletion pyroscope/src/components/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,22 @@ import type { ReactElement } from 'react';

import type { PyroscopeDatasourceSelector } from '../model';
import type { LabelFilter } from '../utils/types';
import { computeLabelScopeSelector } from '../utils/types';
import { AddFilterItem } from './AddFilterItem';
import { FilterItem } from './FilterItem';

export interface FiltersProps {
datasource: PyroscopeDatasourceSelector;
value: LabelFilter[];
service?: string;
profileType?: string;
onChange?: (value: LabelFilter[]) => void;
}

export function Filters(props: FiltersProps): ReactElement {
const theme = useTheme();
const { datasource, value, onChange } = props;
const { datasource, value, service, profileType, onChange } = props;
const labelSelector = computeLabelScopeSelector(service, profileType);

const addFilterItem = (): void => {
const newItem: LabelFilter = { labelName: '', labelValue: '', operator: '=' };
Expand Down Expand Up @@ -86,6 +90,7 @@ export function Filters(props: FiltersProps): ReactElement {
<FilterItem
key={`${filter.labelName}:${filter.operator}:${filter.labelValue}`}
datasource={datasource}
labelSelector={labelSelector}
value={filter}
onChange={(newValue) => updateFilter(index, newValue)}
deleteItem={() => deleteFilter(index)}
Expand Down
5 changes: 3 additions & 2 deletions pyroscope/src/components/LabelName.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import { useLabelNames, filterLabelNamesOptions } from '../utils/use-query';

export interface LabelNameProps {
datasource: PyroscopeDatasourceSelector;
labelSelector: string;
value: string;
onChange?(value: string): void;
}

export function LabelName(props: LabelNameProps): ReactElement {
const { datasource, value, onChange } = props;
const { datasource, labelSelector, value, onChange } = props;

const { data: labelNamesOptions, isLoading: isLabelNamesOptionsLoading } = useLabelNames(datasource);
const { data: labelNamesOptions, isLoading: isLabelNamesOptionsLoading } = useLabelNames(datasource, labelSelector);

const filteredLabelNamesOptions = useMemo(
() => filterLabelNamesOptions(labelNamesOptions?.names ?? []),
Expand Down
9 changes: 7 additions & 2 deletions pyroscope/src/components/LabelValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@ import { useLabelValues } from '../utils/use-query';

export interface LabelValueProps {
datasource: PyroscopeDatasourceSelector;
labelSelector: string;
value: string;
labelName: string;
onChange?(value: string): void;
}

export function LabelValue(props: LabelValueProps): ReactElement {
const { datasource, value, labelName, onChange } = props;
const { datasource, labelSelector, value, labelName, onChange } = props;

const { data: labelValuesOptions, isLoading: isLabelValuesOptionsLoading } = useLabelValues(datasource, labelName);
const { data: labelValuesOptions, isLoading: isLabelValuesOptionsLoading } = useLabelValues(
datasource,
labelName,
labelSelector,
);

return (
<Autocomplete
Expand Down
4 changes: 2 additions & 2 deletions pyroscope/src/model/pyroscope-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export interface PyroscopeClient extends DatasourceClient {
searchLabelNames(
params: SearchLabelNamesParameters,
headers: RequestHeaders,
body: Record<string, string | number>,
body: Record<string, string | number | string[]>,
): Promise<SearchLabelNamesResponse>;
searchLabelValues(
params: SearchLabelValuesParameters,
Expand Down Expand Up @@ -158,7 +158,7 @@ export function searchProfileTypes(
export function searchLabelNames(
params: SearchLabelNamesParameters,
queryOptions: QueryOptions,
body: Record<string, string | number>,
body: Record<string, string | number | string[]>,
): Promise<SearchLabelNamesResponse> {
return fetchWithPost<SearchLabelNamesParameters, SearchLabelNamesResponse>(
'/querier.v1.QuerierService/LabelNames',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,13 @@ export function PyroscopeProfileQueryEditor(props: ProfileQueryEditorProps): Rea
slotProps={{ htmlInput: { step: 1 } }}
/>
</Stack>
<Filters datasource={selectedDatasource} value={filters} onChange={handleFiltersChange} />
<Filters
datasource={selectedDatasource}
value={filters}
service={service}
profileType={profileType}
onChange={handleFiltersChange}
/>
</Stack>
);
}
17 changes: 17 additions & 0 deletions pyroscope/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,20 @@ export function computeFilterExpr(filters: LabelFilter[]): string {
.map((filter) => `${filter.labelName}${filter.operator}"${filter.labelValue}"`)
.join(',')}`;
}

/**
* Builds the selector used to scope label name/value lookups, e.g. `{service_name="app",__profile_type__="..."}`.
* Returns an empty string when no service is set: Pyroscope only narrows the blocks it reads on `service_name`,
* so an unscoped lookup scans every block in the time range and can exceed the query-backend concurrency limit.
*/
export function computeLabelScopeSelector(service?: string, profileType?: string): string {
if (!service) {
return '';
}
// JSON.stringify quotes and escapes the value so names containing `"` or `\` stay valid
const selectors = [`service_name=${JSON.stringify(service)}`];
if (profileType) {
selectors.push(`__profile_type__=${JSON.stringify(profileType)}`);
}
return `{${selectors.join(',')}}`;
}
19 changes: 12 additions & 7 deletions pyroscope/src/utils/use-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,23 @@ 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> {
// labelSelector comes from computeLabelScopeSelector; an empty selector disables the lookup.
export function useLabelNames(
datasource: DatasourceSelector,
labelSelector: string,
): 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', start, end, client],
enabled: !!client && labelSelector !== '',
queryKey: ['searchLabelNames', labelSelector, start, end, client],
queryFn: async () => {
return await client!.searchLabelNames(
{},
{ 'content-type': 'application/json' },
{ start: start * MILLISECONDS, end: end * MILLISECONDS },
{ matchers: [labelSelector], start: start * MILLISECONDS, end: end * MILLISECONDS },
);
},
});
Expand All @@ -49,19 +53,20 @@ export function useLabelNames(datasource: DatasourceSelector): UseQueryResult<Se
export function useLabelValues(
datasource: DatasourceSelector,
labelName: string,
labelSelector: string,
): UseQueryResult<SearchLabelValuesResponse, StatusError> {
const { data: client } = useDatasourceClient<PyroscopeClient>(datasource);
const { absoluteTimeRange } = useTimeRange();
const { start, end } = getUnixTimeRange(absoluteTimeRange);

return useQuery<SearchLabelValuesResponse, StatusError>({
enabled: !!client && labelName !== '', // do not trigger query if no labelName is set
queryKey: ['searchLabelValues', labelName, start, end, client],
enabled: !!client && labelName !== '' && labelSelector !== '', // do not trigger query if no labelName or service is set
queryKey: ['searchLabelValues', labelName, labelSelector, start, end, client],
queryFn: async () => {
return await client!.searchLabelValues(
{},
{ 'content-type': 'application/json' },
{ name: labelName, start: start * MILLISECONDS, end: end * MILLISECONDS },
{ name: labelName, matchers: [labelSelector], start: start * MILLISECONDS, end: end * MILLISECONDS },
);
},
});
Expand Down
Loading