diff --git a/pyroscope/src/components/FilterItem.tsx b/pyroscope/src/components/FilterItem.tsx index 48cc73bd5..20469e4f5 100644 --- a/pyroscope/src/components/FilterItem.tsx +++ b/pyroscope/src/components/FilterItem.tsx @@ -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 }); @@ -55,7 +56,12 @@ export function FilterItem(props: FilterItemProps): ReactElement { > - + @@ -63,6 +69,7 @@ export function FilterItem(props: FilterItemProps): ReactElement { 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: '=' }; @@ -86,6 +90,7 @@ export function Filters(props: FiltersProps): ReactElement { updateFilter(index, newValue)} deleteItem={() => deleteFilter(index)} diff --git a/pyroscope/src/components/LabelName.tsx b/pyroscope/src/components/LabelName.tsx index e1477e5de..0926b5970 100644 --- a/pyroscope/src/components/LabelName.tsx +++ b/pyroscope/src/components/LabelName.tsx @@ -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 ?? []), diff --git a/pyroscope/src/components/LabelValue.tsx b/pyroscope/src/components/LabelValue.tsx index 99cd9fb0b..f38e47391 100644 --- a/pyroscope/src/components/LabelValue.tsx +++ b/pyroscope/src/components/LabelValue.tsx @@ -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 ( , + body: Record, ): Promise; searchLabelValues( params: SearchLabelValuesParameters, @@ -158,7 +158,7 @@ export function searchProfileTypes( export function searchLabelNames( params: SearchLabelNamesParameters, queryOptions: QueryOptions, - body: Record, + body: Record, ): Promise { return fetchWithPost( '/querier.v1.QuerierService/LabelNames', diff --git a/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQueryEditor.tsx b/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQueryEditor.tsx index 87fdaa8b8..91269ab98 100644 --- a/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQueryEditor.tsx +++ b/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQueryEditor.tsx @@ -104,7 +104,13 @@ export function PyroscopeProfileQueryEditor(props: ProfileQueryEditorProps): Rea slotProps={{ htmlInput: { step: 1 } }} /> - + ); } diff --git a/pyroscope/src/utils/types.ts b/pyroscope/src/utils/types.ts index 4bd0b3395..dd726576c 100644 --- a/pyroscope/src/utils/types.ts +++ b/pyroscope/src/utils/types.ts @@ -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(',')}}`; +} diff --git a/pyroscope/src/utils/use-query.ts b/pyroscope/src/utils/use-query.ts index ea29a1f0b..3a39d2290 100644 --- a/pyroscope/src/utils/use-query.ts +++ b/pyroscope/src/utils/use-query.ts @@ -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 { +// labelSelector comes from computeLabelScopeSelector; an empty selector disables the lookup. +export function useLabelNames( + datasource: DatasourceSelector, + labelSelector: string, +): UseQueryResult { const { data: client } = useDatasourceClient(datasource); const { absoluteTimeRange } = useTimeRange(); const { start, end } = getUnixTimeRange(absoluteTimeRange); return useQuery({ - 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 }, ); }, }); @@ -49,19 +53,20 @@ export function useLabelNames(datasource: DatasourceSelector): UseQueryResult { const { data: client } = useDatasourceClient(datasource); const { absoluteTimeRange } = useTimeRange(); const { start, end } = getUnixTimeRange(absoluteTimeRange); return useQuery({ - 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 }, ); }, });