mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-08 12:30:42 +01:00
Compare commits
3 Commits
feat/user-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe8c9d8cf4 | ||
|
|
2ad3057fa6 | ||
|
|
9fc73fc0b1 |
@@ -397,7 +397,6 @@ identn:
|
||||
# headers to use for tokenizer identN resolver
|
||||
headers:
|
||||
- Authorization
|
||||
- Sec-WebSocket-Protocol
|
||||
apikey:
|
||||
# toggle apikey identN
|
||||
enabled: true
|
||||
|
||||
@@ -184,7 +184,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||
apiHandler.RegisterWebSocketPaths(r, am)
|
||||
apiHandler.RegisterMessagingQueuesRoutes(r, am)
|
||||
apiHandler.RegisterThirdPartyApiRoutes(r, am)
|
||||
apiHandler.RegisterTraceFunnelsRoutes(r, am)
|
||||
@@ -197,7 +196,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control"},
|
||||
})
|
||||
|
||||
handler := c.Handler(r)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/quickFilters/getCustomFilters';
|
||||
|
||||
const getCustomFilters = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
const { signal } = props;
|
||||
try {
|
||||
const response = await axios.get(`/orgs/me/filters/${signal}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getCustomFilters;
|
||||
@@ -1,13 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { UpdateCustomFiltersProps } from 'types/api/quickFilters/updateCustomFilters';
|
||||
|
||||
const updateCustomFiltersAPI = async (
|
||||
props: UpdateCustomFiltersProps,
|
||||
): Promise<SuccessResponse<void> | AxiosError> =>
|
||||
axios.put(`/orgs/me/filters`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
export default updateCustomFiltersAPI;
|
||||
@@ -22,7 +22,10 @@ interface UseFieldValuesReturn {
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
const DATA_SOURCE_TO_SIGNAL: Record<DataSource, TelemetrytypesSignalDTO> = {
|
||||
export const DATA_SOURCE_TO_SIGNAL: Record<
|
||||
DataSource,
|
||||
TelemetrytypesSignalDTO
|
||||
> = {
|
||||
[DataSource.METRICS]: TelemetrytypesSignalDTO.metrics,
|
||||
[DataSource.TRACES]: TelemetrytypesSignalDTO.traces,
|
||||
[DataSource.LOGS]: TelemetrytypesSignalDTO.logs,
|
||||
|
||||
@@ -17,7 +17,7 @@ import { CSS } from '@dnd-kit/utilities';
|
||||
import { Button } from 'antd';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { GripVertical } from '@signozhq/icons';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
function SortableFilter({
|
||||
filter,
|
||||
@@ -25,13 +25,13 @@ function SortableFilter({
|
||||
allowDrag,
|
||||
allowRemove,
|
||||
}: {
|
||||
filter: FilterType;
|
||||
onRemove: (filter: FilterType) => void;
|
||||
filter: TelemetryFieldKey;
|
||||
onRemove: (filter: TelemetryFieldKey) => void;
|
||||
allowDrag: boolean;
|
||||
allowRemove: boolean;
|
||||
}): JSX.Element {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: filter.key });
|
||||
useSortable({ id: filter.key as string });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@@ -46,14 +46,14 @@ function SortableFilter({
|
||||
>
|
||||
<div {...attributes} {...listeners} className="drag-handle">
|
||||
{allowDrag && <GripVertical size={16} />}
|
||||
{filter.key}
|
||||
{filter.name}
|
||||
</div>
|
||||
{allowRemove && (
|
||||
<Button
|
||||
className="remove-filter-btn periscope-btn"
|
||||
size="small"
|
||||
onClick={(): void => {
|
||||
onRemove(filter as FilterType);
|
||||
onRemove(filter);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
@@ -69,8 +69,8 @@ function AddedFilters({
|
||||
setAddedFilters,
|
||||
}: {
|
||||
inputValue: string;
|
||||
addedFilters: FilterType[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
|
||||
addedFilters: TelemetryFieldKey[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
|
||||
}): JSX.Element {
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
@@ -90,12 +90,12 @@ function AddedFilters({
|
||||
const filteredAddedFilters = useMemo(
|
||||
() =>
|
||||
addedFilters.filter((filter) =>
|
||||
filter.key.toLowerCase().includes(inputValue.toLowerCase()),
|
||||
filter.name.toLowerCase().includes(inputValue.toLowerCase()),
|
||||
),
|
||||
[addedFilters, inputValue],
|
||||
);
|
||||
|
||||
const handleRemoveFilter = (filter: FilterType): void => {
|
||||
const handleRemoveFilter = (filter: TelemetryFieldKey): void => {
|
||||
setAddedFilters((prev) => prev.filter((f) => f.key !== filter.key));
|
||||
};
|
||||
|
||||
@@ -116,7 +116,7 @@ function AddedFilters({
|
||||
<div className="no-values-found">No values found</div>
|
||||
) : (
|
||||
<SortableContext
|
||||
items={addedFilters.map((f) => f.key)}
|
||||
items={addedFilters.map((f) => f.key as string)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
disabled={!allowDrag}
|
||||
>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { useGetFieldsKeys } from 'api/generated/services/fields';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'components/QuickFilters/FilterRenderers/Checkbox/v2/useFieldValues';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import {
|
||||
FieldContext,
|
||||
FieldDataType,
|
||||
TelemetryFieldKey,
|
||||
} from 'types/api/v5/queryRange';
|
||||
|
||||
function OtherFiltersSkeleton(): JSX.Element {
|
||||
return (
|
||||
@@ -37,106 +37,48 @@ function OtherFilters({
|
||||
}: {
|
||||
signal: SignalType | undefined;
|
||||
inputValue: string;
|
||||
addedFilters: FilterType[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
|
||||
addedFilters: TelemetryFieldKey[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
|
||||
}): JSX.Element {
|
||||
const isLogDataSource = useMemo(
|
||||
() => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS,
|
||||
[signal],
|
||||
);
|
||||
const isMeterDataSource = useMemo(
|
||||
() => signal && signal === SignalType.METER_EXPLORER,
|
||||
[signal],
|
||||
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
|
||||
|
||||
const { data, isFetching } = useGetFieldsKeys(
|
||||
{
|
||||
searchText: inputValue,
|
||||
signal: signal
|
||||
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
|
||||
: undefined,
|
||||
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
|
||||
},
|
||||
{ query: { enabled: !!signal } },
|
||||
);
|
||||
|
||||
const { data: suggestionsData, isFetching: isFetchingSuggestions } =
|
||||
useGetAttributeSuggestions(
|
||||
{
|
||||
searchText: inputValue,
|
||||
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
|
||||
filters: {} as TagFilter,
|
||||
},
|
||||
{
|
||||
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
||||
enabled: !!signal && isLogDataSource,
|
||||
},
|
||||
);
|
||||
const otherFilters = useMemo<TelemetryFieldKey[]>(() => {
|
||||
const rawSuggestions = Object.values(data?.data?.keys ?? {}).flat();
|
||||
// Normalize: synthesize the composite `key` once so downstream reads (dedupe,
|
||||
// add, render) can trust it.
|
||||
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
|
||||
name: attr.name,
|
||||
signal: attr.signal as TelemetryFieldKey['signal'],
|
||||
fieldContext: attr.fieldContext as FieldContext,
|
||||
fieldDataType: attr.fieldDataType as FieldDataType,
|
||||
key: buildCompositeKey(attr.name, attr.fieldContext, attr.fieldDataType),
|
||||
}));
|
||||
|
||||
const { data: aggregateKeysData, isFetching: isFetchingAggregateKeys } =
|
||||
useGetAggregateKeys(
|
||||
{
|
||||
searchText: inputValue,
|
||||
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
||||
enabled: !!signal && !isLogDataSource && !isMeterDataSource,
|
||||
},
|
||||
const addedKeys = new Set(
|
||||
addedFilters.map((filter) =>
|
||||
buildCompositeKey(filter.name, filter.fieldContext, filter.fieldDataType),
|
||||
),
|
||||
);
|
||||
return suggestions.filter((attr) => !addedKeys.has(attr.key as string));
|
||||
}, [data, addedFilters]);
|
||||
|
||||
const { data: fieldKeysData, isLoading: isLoadingFieldKeys } =
|
||||
useGetQueryKeySuggestions(
|
||||
{
|
||||
searchText: inputValue,
|
||||
signal: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
|
||||
signalSource: 'meter',
|
||||
},
|
||||
{
|
||||
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
||||
enabled: !!signal && isMeterDataSource,
|
||||
},
|
||||
);
|
||||
|
||||
const otherFilters = useMemo(() => {
|
||||
let filterAttributes;
|
||||
if (isLogDataSource) {
|
||||
filterAttributes = suggestionsData?.payload?.attributes || [];
|
||||
} else if (isMeterDataSource) {
|
||||
const fieldKeys: QueryKeyDataSuggestionsProps[] = Object.values(
|
||||
fieldKeysData?.data?.data?.keys || {},
|
||||
)?.flat();
|
||||
filterAttributes = fieldKeys.map(
|
||||
(attr) =>
|
||||
({
|
||||
key: attr.name,
|
||||
dataType: attr.fieldDataType,
|
||||
type: attr.fieldContext,
|
||||
signal: attr.signal,
|
||||
}) as BaseAutocompleteData,
|
||||
);
|
||||
} else {
|
||||
filterAttributes = aggregateKeysData?.payload?.attributeKeys || [];
|
||||
}
|
||||
return filterAttributes?.filter(
|
||||
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
|
||||
);
|
||||
}, [
|
||||
suggestionsData,
|
||||
aggregateKeysData,
|
||||
addedFilters,
|
||||
isLogDataSource,
|
||||
fieldKeysData,
|
||||
isMeterDataSource,
|
||||
]);
|
||||
|
||||
const handleAddFilter = (filter: FilterType): void => {
|
||||
setAddedFilters((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: filter.key,
|
||||
dataType: filter.dataType,
|
||||
type: filter.type,
|
||||
},
|
||||
]);
|
||||
const handleAddFilter = (filter: TelemetryFieldKey): void => {
|
||||
setAddedFilters((prev) => [...prev, filter]);
|
||||
};
|
||||
|
||||
const renderFilters = (): React.ReactNode => {
|
||||
const isLoading =
|
||||
isFetchingSuggestions || isFetchingAggregateKeys || isLoadingFieldKeys;
|
||||
if (isLoading) {
|
||||
if (isFetching) {
|
||||
return <OtherFiltersSkeleton />;
|
||||
}
|
||||
if (!otherFilters?.length) {
|
||||
@@ -145,11 +87,11 @@ function OtherFilters({
|
||||
|
||||
return otherFilters.map((filter) => (
|
||||
<div key={filter.key} className="qf-filter-item other-filters-item">
|
||||
<div className="qf-filter-key">{filter.key}</div>
|
||||
<div className="qf-filter-key">{filter.name}</div>
|
||||
<Button
|
||||
className="add-filter-btn periscope-btn"
|
||||
size="small"
|
||||
onClick={(): void => handleAddFilter(filter as FilterType)}
|
||||
onClick={(): void => handleAddFilter(filter)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { SignalType } from '../types';
|
||||
import AddedFilters from './AddedFilters';
|
||||
@@ -19,7 +18,7 @@ function QuickFiltersSettings({
|
||||
}: {
|
||||
signal: SignalType | undefined;
|
||||
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
|
||||
customFilters: FilterType[];
|
||||
customFilters: TelemetryFieldKey[];
|
||||
refetchCustomFilters: () => void;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
@@ -28,6 +27,7 @@ function QuickFiltersSettings({
|
||||
addedFilters,
|
||||
setAddedFilters,
|
||||
handleSaveChanges,
|
||||
hasUnsavedChanges,
|
||||
isUpdatingCustomFilters,
|
||||
inputValue,
|
||||
handleInputChange,
|
||||
@@ -39,18 +39,6 @@ function QuickFiltersSettings({
|
||||
signal,
|
||||
});
|
||||
|
||||
const hasUnsavedChanges = useMemo(
|
||||
() =>
|
||||
// check if both arrays have the same length and same order of elements
|
||||
!(
|
||||
addedFilters.length === customFilters.length &&
|
||||
addedFilters.every(
|
||||
(filter, index) => filter.key === customFilters[index].key,
|
||||
)
|
||||
),
|
||||
[addedFilters, customFilters],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="qf-header">
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useUpdateQuickFilters } from 'api/generated/services/quick-filter';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import {
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesFieldDataTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
interface UseQuickFilterSettingsProps {
|
||||
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
|
||||
customFilters: FilterType[];
|
||||
customFilters: TelemetryFieldKey[];
|
||||
refetchCustomFilters: () => void;
|
||||
signal?: SignalType;
|
||||
}
|
||||
|
||||
interface UseQuickFilterSettingsReturn {
|
||||
addedFilters: FilterType[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
|
||||
addedFilters: TelemetryFieldKey[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
|
||||
handleSettingsClose: () => void;
|
||||
handleDiscardChanges: () => void;
|
||||
handleSaveChanges: () => void;
|
||||
hasUnsavedChanges: boolean;
|
||||
isUpdatingCustomFilters: boolean;
|
||||
inputValue: string;
|
||||
setInputValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
@@ -37,27 +41,43 @@ const useQuickFilterSettings = ({
|
||||
}: UseQuickFilterSettingsProps): UseQuickFilterSettingsReturn => {
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
const [debouncedInputValue, setDebouncedInputValue] = useState<string>('');
|
||||
const [addedFilters, setAddedFilters] = useState<FilterType[]>(customFilters);
|
||||
const normalizedCustomFilters = useMemo<TelemetryFieldKey[]>(
|
||||
() =>
|
||||
customFilters.map((filter) => ({
|
||||
...filter,
|
||||
key: buildCompositeKey(
|
||||
filter.name,
|
||||
filter.fieldContext,
|
||||
filter.fieldDataType,
|
||||
),
|
||||
})),
|
||||
[customFilters],
|
||||
);
|
||||
const [addedFilters, setAddedFilters] = useState<TelemetryFieldKey[]>(
|
||||
normalizedCustomFilters,
|
||||
);
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { mutate: updateCustomFilters, isLoading: isUpdatingCustomFilters } =
|
||||
useMutation(updateCustomFiltersAPI, {
|
||||
onSuccess: () => {
|
||||
setIsSettingsOpen(false);
|
||||
refetchCustomFilters();
|
||||
logEvent('Quick Filters Settings: changes saved', {
|
||||
addedFilters,
|
||||
});
|
||||
notifications.success({
|
||||
message: 'Quick filters updated successfully',
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
onError: (error: AxiosError) => {
|
||||
notifications.error({
|
||||
message: axios.isAxiosError(error) ? error.message : SOMETHING_WENT_WRONG,
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
useUpdateQuickFilters({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
setIsSettingsOpen(false);
|
||||
refetchCustomFilters();
|
||||
void logEvent('Quick Filters Settings: changes saved', {
|
||||
addedFilters,
|
||||
});
|
||||
notifications.success({
|
||||
message: 'Quick filters updated successfully',
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.error({
|
||||
message: error.message || SOMETHING_WENT_WRONG,
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
const debouncedUpdate = useDebouncedFn((value) => {
|
||||
@@ -78,19 +98,32 @@ const useQuickFilterSettings = ({
|
||||
}, [setIsSettingsOpen]);
|
||||
|
||||
const handleDiscardChanges = useCallback((): void => {
|
||||
setAddedFilters(customFilters);
|
||||
}, [customFilters, setAddedFilters]);
|
||||
setAddedFilters(normalizedCustomFilters);
|
||||
}, [normalizedCustomFilters, setAddedFilters]);
|
||||
|
||||
const hasUnsavedChanges = useMemo(
|
||||
() =>
|
||||
!(
|
||||
addedFilters.length === normalizedCustomFilters.length &&
|
||||
addedFilters.every(
|
||||
(filter, index) => filter.key === normalizedCustomFilters[index].key,
|
||||
)
|
||||
),
|
||||
[addedFilters, normalizedCustomFilters],
|
||||
);
|
||||
|
||||
const handleSaveChanges = useCallback((): void => {
|
||||
if (signal) {
|
||||
updateCustomFilters({
|
||||
pathParams: { source: signal },
|
||||
data: {
|
||||
// Send only the stored TelemetryFieldKey fields; the composite `key`
|
||||
// is UI-only.
|
||||
filters: addedFilters.map((filter) => ({
|
||||
key: filter.key,
|
||||
datatype: filter.dataType,
|
||||
type: filter.type,
|
||||
name: filter.name,
|
||||
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
|
||||
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
|
||||
})),
|
||||
signal,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -102,6 +135,7 @@ const useQuickFilterSettings = ({
|
||||
addedFilters,
|
||||
setAddedFilters,
|
||||
handleSaveChanges,
|
||||
hasUnsavedChanges,
|
||||
isUpdatingCustomFilters,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import getCustomFilters from 'api/quickFilters/getCustomFilters';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { useGetQuickFilters } from 'api/generated/services/quick-filter';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { IQuickFiltersConfig, SignalType } from '../types';
|
||||
import { getFilterConfig } from '../utils';
|
||||
@@ -13,7 +11,7 @@ interface UseFilterConfigProps {
|
||||
}
|
||||
interface UseFilterConfigReturn {
|
||||
filterConfig: IQuickFiltersConfig[];
|
||||
customFilters: FilterType[];
|
||||
customFilters: TelemetryFieldKey[];
|
||||
isCustomFiltersLoading: boolean;
|
||||
isDynamicFilters: boolean;
|
||||
refetchCustomFilters: () => void;
|
||||
@@ -25,17 +23,16 @@ const useFilterConfig = ({
|
||||
}: UseFilterConfigProps): UseFilterConfigReturn => {
|
||||
const {
|
||||
isFetching: isCustomFiltersLoading,
|
||||
data: customFilters = [],
|
||||
data,
|
||||
refetch,
|
||||
} = useQuery<FilterType[], Error>(
|
||||
[REACT_QUERY_KEY.GET_CUSTOM_FILTERS, signal],
|
||||
async () => {
|
||||
const res = await getCustomFilters({ signal: signal || '' });
|
||||
return 'payload' in res && res.payload?.filters ? res.payload.filters : [];
|
||||
},
|
||||
{
|
||||
enabled: !!signal,
|
||||
},
|
||||
} = useGetQuickFilters(
|
||||
{ source: signal ?? '' },
|
||||
{ query: { enabled: !!signal } },
|
||||
);
|
||||
|
||||
const customFilters = useMemo<TelemetryFieldKey[]>(
|
||||
() => (data?.data?.filters ?? []) as TelemetryFieldKey[],
|
||||
[data],
|
||||
);
|
||||
|
||||
const isDynamicFilters = useMemo(
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from 'mocks-server/__mockdata__/customQuickFilters';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
@@ -34,9 +34,9 @@ const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const SIGNAL = SignalType.LOGS;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
|
||||
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
|
||||
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v1/fields/keys`;
|
||||
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
|
||||
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
|
||||
|
||||
@@ -338,6 +338,63 @@ describe('Quick Filters with custom filters', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps same-name fields with different context as distinct entries', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
server.use(
|
||||
rest.get(quickFiltersSuggestionsURL, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: {
|
||||
level: [
|
||||
{
|
||||
name: 'level',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
{
|
||||
name: 'level',
|
||||
fieldContext: 'span',
|
||||
fieldDataType: 'string',
|
||||
signal: 'logs',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
const settingsButton = icon.closest('button') ?? icon;
|
||||
await user.click(settingsButton);
|
||||
|
||||
const otherSection = screen.getByText(OTHER_FILTERS_LABEL).parentElement!;
|
||||
// Both `level` variants are shown despite sharing a name.
|
||||
await waitFor(() =>
|
||||
expect(within(otherSection).getAllByText('level')).toHaveLength(2),
|
||||
);
|
||||
|
||||
// Adding one variant removes only that one; the other stays.
|
||||
const firstLevel = within(otherSection).getAllByText('level')[0];
|
||||
const addButton = firstLevel.parentElement?.querySelector('button');
|
||||
await user.click(addButton as HTMLButtonElement);
|
||||
|
||||
const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!;
|
||||
await waitFor(() => {
|
||||
expect(within(addedSection).getAllByText('level')).toHaveLength(1);
|
||||
expect(within(otherSection).getAllByText('level')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
@@ -456,12 +513,10 @@ describe('Quick Filters with custom filters', () => {
|
||||
});
|
||||
|
||||
const requestBody = putHandler.mock.calls[0][0];
|
||||
expect(requestBody.filters).toStrictEqual(
|
||||
expect.arrayContaining([
|
||||
expect.not.objectContaining({ key: FILTER_OS_DESCRIPTION }),
|
||||
]),
|
||||
expect(requestBody.filters).not.toContainEqual(
|
||||
expect.objectContaining({ name: FILTER_OS_DESCRIPTION }),
|
||||
);
|
||||
expect(requestBody.signal).toBe(SIGNAL);
|
||||
expect(requestBody.filters).toHaveLength(10);
|
||||
});
|
||||
|
||||
it('should render duration slider for duration_nono filter', async () => {
|
||||
@@ -612,9 +667,9 @@ describe('Quick Filters refetch behavior', () => {
|
||||
filters: [
|
||||
...(quickFiltersListResponse.data.filters ?? []),
|
||||
{
|
||||
key: 'new.custom.filter',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
name: 'new.custom.filter',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'resource',
|
||||
} as const,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { fieldDataTypeToDataType } from 'utils/fieldDataType';
|
||||
|
||||
import { FiltersType, IQuickFiltersConfig, SignalType } from './types';
|
||||
|
||||
@@ -12,6 +14,19 @@ const FILTER_TYPE_MAP: Record<string, FiltersType> = {
|
||||
duration_nano: FiltersType.DURATION,
|
||||
};
|
||||
|
||||
// The map below exists only for the old v3 attribute-values fetch
|
||||
// (useCheckboxFilterValues), the sole reader of attributeKey.dataType/type.
|
||||
// Once the values fetch moves to fields/values, remove this and reduce
|
||||
// attributeKey to { id, key }.
|
||||
|
||||
const FIELD_CONTEXT_TO_ATTRIBUTE_TYPE: Record<string, string> = {
|
||||
[TelemetrytypesFieldContextDTO.attribute]: 'tag',
|
||||
[TelemetrytypesFieldContextDTO.resource]: 'resource',
|
||||
};
|
||||
|
||||
const mapFieldContext = (fieldContext?: string): string =>
|
||||
(fieldContext && FIELD_CONTEXT_TO_ATTRIBUTE_TYPE[fieldContext]) || '';
|
||||
|
||||
const getFilterName = (str: string): string => {
|
||||
if (FILTER_TITLE_MAP[str]) {
|
||||
return FILTER_TITLE_MAP[str];
|
||||
@@ -26,16 +41,16 @@ const getFilterName = (str: string): string => {
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
const getFilterType = (att: FilterType): FiltersType => {
|
||||
if (FILTER_TYPE_MAP[att.key]) {
|
||||
return FILTER_TYPE_MAP[att.key];
|
||||
const getFilterType = (att: TelemetryFieldKey): FiltersType => {
|
||||
if (FILTER_TYPE_MAP[att.name]) {
|
||||
return FILTER_TYPE_MAP[att.name];
|
||||
}
|
||||
return FiltersType.CHECKBOX;
|
||||
};
|
||||
|
||||
export const getFilterConfig = (
|
||||
signal?: SignalType,
|
||||
customFilters?: FilterType[],
|
||||
customFilters?: TelemetryFieldKey[],
|
||||
config?: IQuickFiltersConfig[],
|
||||
): IQuickFiltersConfig[] => {
|
||||
if (!customFilters?.length || !signal) {
|
||||
@@ -46,13 +61,13 @@ export const getFilterConfig = (
|
||||
(att, index) =>
|
||||
({
|
||||
type: getFilterType(att),
|
||||
title: getFilterName(att.key),
|
||||
title: getFilterName(att.name),
|
||||
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
|
||||
attributeKey: {
|
||||
id: att.key,
|
||||
key: att.key,
|
||||
dataType: att.dataType,
|
||||
type: att.type,
|
||||
id: att.name,
|
||||
key: att.name,
|
||||
dataType: fieldDataTypeToDataType(att.fieldDataType),
|
||||
type: mapFieldContext(att.fieldContext),
|
||||
},
|
||||
defaultOpen: index < 2,
|
||||
}) as IQuickFiltersConfig,
|
||||
|
||||
@@ -108,7 +108,6 @@ function LogsExplorerViewsContainer({
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
const [requestData, setRequestData] = useState<Query | null>(null);
|
||||
const [queryId, setQueryId] = useState<string>(v4());
|
||||
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
||||
|
||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||
@@ -180,12 +179,7 @@ function LogsExplorerViewsContainer({
|
||||
},
|
||||
undefined,
|
||||
listQueryKeyRef,
|
||||
{
|
||||
...(!isEmpty(queryId) &&
|
||||
selectedPanelType !== PANEL_TYPES.LIST && {
|
||||
'X-SIGNOZ-QUERY-ID': queryId,
|
||||
}),
|
||||
},
|
||||
undefined,
|
||||
// custom selected time interval to prevent recalculating the start and end timestamps before fetching next pages
|
||||
'custom',
|
||||
);
|
||||
@@ -250,10 +244,6 @@ function LogsExplorerViewsContainer({
|
||||
setRequestData(newRequestData);
|
||||
}, [isLimit, logs, listQuery, pageSize, stagedQuery, getRequestData, page]);
|
||||
|
||||
useEffect(() => {
|
||||
setQueryId(v4());
|
||||
}, [data]);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
|
||||
|
||||
@@ -4,114 +4,85 @@ export const quickFiltersListResponse = {
|
||||
signal: 'logs',
|
||||
filters: [
|
||||
{
|
||||
key: 'os.description',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
name: 'os.description',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'service.name',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
name: 'service.name',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'duration_nano',
|
||||
dataType: 'float64',
|
||||
type: 'tag',
|
||||
name: 'duration_nano',
|
||||
fieldDataType: 'float64',
|
||||
fieldContext: 'attribute',
|
||||
},
|
||||
{
|
||||
key: 'quantity',
|
||||
dataType: 'float64',
|
||||
type: 'tag',
|
||||
name: 'quantity',
|
||||
fieldDataType: 'float64',
|
||||
fieldContext: 'attribute',
|
||||
},
|
||||
{
|
||||
key: 'body',
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'body',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: '',
|
||||
},
|
||||
{
|
||||
key: 'deployment.environment',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
name: 'deployment.environment',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'service.namespace',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
name: 'service.namespace',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'k8s.namespace.name',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
name: 'k8s.namespace.name',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'service.instance.id',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
name: 'service.instance.id',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'k8s.pod.name',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
name: 'k8s.pod.name',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'process.owner',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
name: 'process.owner',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'resource',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const otherFilterName = (name: string): { [k: string]: unknown[] } => ({
|
||||
[name]: [
|
||||
{ name, fieldContext: 'resource', fieldDataType: 'string', signal: 'logs' },
|
||||
],
|
||||
});
|
||||
|
||||
export const otherFiltersResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
attributes: [
|
||||
{
|
||||
key: 'service.name',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'k8s.deployment.name',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'deployment.environment',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'service.namespace',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'k8s.namespace.name',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'service.instance.id',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'k8s.pod.name',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'k8s.pod.uid',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'os.description',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
},
|
||||
],
|
||||
complete: true,
|
||||
keys: {
|
||||
...otherFilterName('service.name'),
|
||||
...otherFilterName('k8s.deployment.name'),
|
||||
...otherFilterName('deployment.environment'),
|
||||
...otherFilterName('service.namespace'),
|
||||
...otherFilterName('k8s.namespace.name'),
|
||||
...otherFilterName('service.instance.id'),
|
||||
...otherFilterName('k8s.pod.name'),
|
||||
...otherFilterName('k8s.pod.uid'),
|
||||
...otherFilterName('os.description'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -504,7 +504,7 @@ jest.mock('hooks/useHandleExplorerTabChange', () => ({
|
||||
let capturedPayload: QueryRangePayloadV5;
|
||||
|
||||
describe('TracesExplorer -', () => {
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/traces`;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/traces`;
|
||||
|
||||
const setupServer = (): void => {
|
||||
server.use(
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export interface Filter {
|
||||
key: string;
|
||||
dataType: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
signal: string;
|
||||
}
|
||||
|
||||
export type PayloadProps = {
|
||||
filters: Filter[];
|
||||
signal: string;
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
|
||||
interface FilterType {
|
||||
key: string;
|
||||
datatype: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface UpdateCustomFiltersProps {
|
||||
data: {
|
||||
filters: FilterType[];
|
||||
signal: SignalType;
|
||||
};
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -31,7 +31,6 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/huandu/go-sqlbuilder v1.39.1
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12
|
||||
@@ -134,6 +133,7 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/hashicorp/go-metrics v0.5.4 // indirect
|
||||
github.com/huandu/go-clone v1.7.3 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
|
||||
@@ -45,7 +45,7 @@ func newConfig() factory.Config {
|
||||
return &Config{
|
||||
Tokenizer: TokenizerConfig{
|
||||
Enabled: true,
|
||||
Headers: []string{"Authorization", "Sec-WebSocket-Protocol"},
|
||||
Headers: []string{"Authorization"},
|
||||
},
|
||||
APIKeyConfig: APIKeyConfig{
|
||||
Enabled: true,
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
package queryprogress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
// tracks progress and manages subscriptions for all queries
|
||||
type inMemoryQueryProgressTracker struct {
|
||||
logger *slog.Logger
|
||||
queries map[string]*queryTracker
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (tracker *inMemoryQueryProgressTracker) ReportQueryStarted(
|
||||
queryId string,
|
||||
) (postQueryCleanup func(), apiErr *model.ApiError) {
|
||||
tracker.lock.Lock()
|
||||
defer tracker.lock.Unlock()
|
||||
|
||||
_, exists := tracker.queries[queryId]
|
||||
if exists {
|
||||
return nil, model.BadRequest(fmt.Errorf(
|
||||
"query %s already started", queryId,
|
||||
))
|
||||
}
|
||||
|
||||
tracker.queries[queryId] = newQueryTracker(tracker.logger, queryId)
|
||||
|
||||
return func() {
|
||||
tracker.onQueryFinished(queryId)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tracker *inMemoryQueryProgressTracker) ReportQueryProgress(
|
||||
queryId string, chProgress *clickhouse.Progress,
|
||||
) *model.ApiError {
|
||||
queryTracker, err := tracker.getQueryTracker(queryId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queryTracker.handleProgressUpdate(chProgress)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tracker *inMemoryQueryProgressTracker) SubscribeToQueryProgress(
|
||||
queryId string,
|
||||
) (<-chan model.QueryProgress, func(), *model.ApiError) {
|
||||
queryTracker, err := tracker.getQueryTracker(queryId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return queryTracker.subscribe()
|
||||
}
|
||||
|
||||
func (tracker *inMemoryQueryProgressTracker) onQueryFinished(
|
||||
queryId string,
|
||||
) {
|
||||
tracker.lock.Lock()
|
||||
queryTracker := tracker.queries[queryId]
|
||||
if queryTracker != nil {
|
||||
delete(tracker.queries, queryId)
|
||||
}
|
||||
tracker.lock.Unlock()
|
||||
|
||||
if queryTracker != nil {
|
||||
queryTracker.onFinished()
|
||||
}
|
||||
}
|
||||
|
||||
func (tracker *inMemoryQueryProgressTracker) getQueryTracker(
|
||||
queryId string,
|
||||
) (*queryTracker, *model.ApiError) {
|
||||
tracker.lock.RLock()
|
||||
defer tracker.lock.RUnlock()
|
||||
|
||||
queryTracker := tracker.queries[queryId]
|
||||
if queryTracker == nil {
|
||||
return nil, model.NotFoundError(fmt.Errorf(
|
||||
"query %s doesn't exist", queryId,
|
||||
))
|
||||
}
|
||||
|
||||
return queryTracker, nil
|
||||
}
|
||||
|
||||
// Tracks progress and manages subscriptions for a single query
|
||||
type queryTracker struct {
|
||||
logger *slog.Logger
|
||||
queryId string
|
||||
isFinished bool
|
||||
|
||||
progress *model.QueryProgress
|
||||
subscriptions map[string]*queryProgressSubscription
|
||||
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func newQueryTracker(logger *slog.Logger, queryId string) *queryTracker {
|
||||
return &queryTracker{
|
||||
logger: logger,
|
||||
queryId: queryId,
|
||||
subscriptions: map[string]*queryProgressSubscription{},
|
||||
}
|
||||
}
|
||||
|
||||
func (qt *queryTracker) handleProgressUpdate(p *clickhouse.Progress) {
|
||||
qt.lock.Lock()
|
||||
defer qt.lock.Unlock()
|
||||
|
||||
if qt.isFinished {
|
||||
qt.logger.Warn("received clickhouse progress update for finished query", "queryId", qt.queryId, "progress", p)
|
||||
return
|
||||
}
|
||||
|
||||
if qt.progress == nil {
|
||||
// This is the first update
|
||||
qt.progress = &model.QueryProgress{}
|
||||
}
|
||||
updateQueryProgress(qt.progress, p)
|
||||
|
||||
// broadcast latest state to all subscribers.
|
||||
for _, sub := range maps.Values(qt.subscriptions) {
|
||||
sub.send(*qt.progress)
|
||||
}
|
||||
}
|
||||
|
||||
func (qt *queryTracker) subscribe() (
|
||||
<-chan model.QueryProgress, func(), *model.ApiError,
|
||||
) {
|
||||
qt.lock.Lock()
|
||||
defer qt.lock.Unlock()
|
||||
|
||||
if qt.isFinished {
|
||||
return nil, nil, model.NotFoundError(fmt.Errorf(
|
||||
"query %s already finished", qt.queryId,
|
||||
))
|
||||
}
|
||||
|
||||
subscriberId := uuid.NewString()
|
||||
subscription := newQueryProgressSubscription(qt.logger)
|
||||
qt.subscriptions[subscriberId] = subscription
|
||||
|
||||
if qt.progress != nil {
|
||||
subscription.send(*qt.progress)
|
||||
}
|
||||
|
||||
return subscription.ch, func() {
|
||||
qt.unsubscribe(subscriberId)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (qt *queryTracker) unsubscribe(subscriberId string) {
|
||||
qt.lock.Lock()
|
||||
defer qt.lock.Unlock()
|
||||
|
||||
if qt.isFinished {
|
||||
qt.logger.Debug("received unsubscribe request after query finished", "subscriber", subscriberId, "queryId", qt.queryId)
|
||||
return
|
||||
}
|
||||
|
||||
subscription := qt.subscriptions[subscriberId]
|
||||
if subscription != nil {
|
||||
subscription.close()
|
||||
delete(qt.subscriptions, subscriberId)
|
||||
}
|
||||
}
|
||||
|
||||
func (qt *queryTracker) onFinished() {
|
||||
qt.lock.Lock()
|
||||
defer qt.lock.Unlock()
|
||||
|
||||
if qt.isFinished {
|
||||
qt.logger.Warn("receiver query finish report after query finished", "queryId", qt.queryId)
|
||||
return
|
||||
}
|
||||
|
||||
for subId, sub := range qt.subscriptions {
|
||||
sub.close()
|
||||
delete(qt.subscriptions, subId)
|
||||
}
|
||||
|
||||
qt.isFinished = true
|
||||
}
|
||||
|
||||
type queryProgressSubscription struct {
|
||||
logger *slog.Logger
|
||||
ch chan model.QueryProgress
|
||||
isClosed bool
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func newQueryProgressSubscription(logger *slog.Logger) *queryProgressSubscription {
|
||||
ch := make(chan model.QueryProgress, 1000)
|
||||
return &queryProgressSubscription{
|
||||
logger: logger,
|
||||
ch: ch,
|
||||
}
|
||||
}
|
||||
|
||||
// Must not block or panic in any scenario
|
||||
func (ch *queryProgressSubscription) send(progress model.QueryProgress) {
|
||||
ch.lock.Lock()
|
||||
defer ch.lock.Unlock()
|
||||
|
||||
if ch.isClosed {
|
||||
ch.logger.Error("can't send query progress: channel already closed.", "progress", progress)
|
||||
return
|
||||
}
|
||||
|
||||
// subscription channels are expected to have big enough buffers to ensure
|
||||
// blocking while sending doesn't happen in the happy path
|
||||
select {
|
||||
case ch.ch <- progress:
|
||||
ch.logger.Debug("published query progress", "progress", progress)
|
||||
default:
|
||||
ch.logger.Error("couldn't publish query progress. dropping update.", "progress", progress)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *queryProgressSubscription) close() {
|
||||
ch.lock.Lock()
|
||||
defer ch.lock.Unlock()
|
||||
|
||||
if !ch.isClosed {
|
||||
close(ch.ch)
|
||||
ch.isClosed = true
|
||||
}
|
||||
}
|
||||
|
||||
func updateQueryProgress(qp *model.QueryProgress, chProgress *clickhouse.Progress) {
|
||||
qp.ReadRows += chProgress.Rows
|
||||
qp.ReadBytes += chProgress.Bytes
|
||||
qp.ElapsedMs += uint64(chProgress.Elapsed.Milliseconds())
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package queryprogress
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
)
|
||||
|
||||
type QueryProgressTracker interface {
|
||||
// Tells the tracker that query with id `queryId` has started.
|
||||
// Progress can only be reported for and tracked for a query that is in progress.
|
||||
// Returns a cleanup function that must be called after the query finishes.
|
||||
ReportQueryStarted(queryId string) (postQueryCleanup func(), apiErr *model.ApiError)
|
||||
|
||||
// Report progress stats received from clickhouse for `queryId`
|
||||
ReportQueryProgress(queryId string, chProgress *clickhouse.Progress) *model.ApiError
|
||||
|
||||
// Subscribe to progress updates for `queryId`
|
||||
// The returned channel will produce `QueryProgress` instances representing
|
||||
// the latest state of query progress stats. Also returns a function that
|
||||
// can be called to unsubscribe before the query finishes, if needed.
|
||||
SubscribeToQueryProgress(queryId string) (ch <-chan model.QueryProgress, unsubscribe func(), apiErr *model.ApiError)
|
||||
}
|
||||
|
||||
func NewQueryProgressTracker(logger *slog.Logger) QueryProgressTracker {
|
||||
// InMemory tracker is useful only for single replica query service setups.
|
||||
// Multi replica setups must use a centralized store for tracking and subscribing to query progress
|
||||
return &inMemoryQueryProgressTracker{
|
||||
logger: logger,
|
||||
queries: map[string]*queryTracker{},
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package queryprogress
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQueryProgressTracking(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
tracker := NewQueryProgressTracker(slog.Default())
|
||||
|
||||
testQueryId := "test-query"
|
||||
|
||||
testProgress := &clickhouse.Progress{}
|
||||
err := tracker.ReportQueryProgress(testQueryId, testProgress)
|
||||
require.NotNil(err, "shouldn't be able to report query progress before query has been started")
|
||||
require.Equal(err.Type(), model.ErrorNotFound)
|
||||
|
||||
ch, unsubscribe, err := tracker.SubscribeToQueryProgress(testQueryId)
|
||||
require.NotNil(err, "shouldn't be able to subscribe for progress updates before query has been started")
|
||||
require.Equal(err.Type(), model.ErrorNotFound)
|
||||
require.Nil(ch)
|
||||
require.Nil(unsubscribe)
|
||||
|
||||
reportQueryFinished, err := tracker.ReportQueryStarted(testQueryId)
|
||||
require.Nil(err, "should be able to report start of a query to be tracked")
|
||||
|
||||
testProgress1 := &clickhouse.Progress{
|
||||
Rows: 10,
|
||||
Bytes: 20,
|
||||
TotalRows: 100,
|
||||
Elapsed: 20 * time.Millisecond,
|
||||
}
|
||||
err = tracker.ReportQueryProgress(testQueryId, testProgress1)
|
||||
require.Nil(err, "should be able to report progress after query has started")
|
||||
|
||||
ch, unsubscribe, err = tracker.SubscribeToQueryProgress(testQueryId)
|
||||
require.Nil(err, "should be able to subscribe to query progress updates after query started")
|
||||
require.NotNil(ch)
|
||||
require.NotNil(unsubscribe)
|
||||
|
||||
expectedProgress := model.QueryProgress{}
|
||||
updateQueryProgress(&expectedProgress, testProgress1)
|
||||
require.Equal(expectedProgress.ReadRows, testProgress1.Rows)
|
||||
select {
|
||||
case qp := <-ch:
|
||||
require.Equal(qp, expectedProgress)
|
||||
default:
|
||||
require.Fail("should receive latest query progress state immediately after subscription")
|
||||
}
|
||||
select {
|
||||
case _ = <-ch:
|
||||
require.Fail("should have had only one pending update at this point")
|
||||
default:
|
||||
}
|
||||
|
||||
testProgress2 := &clickhouse.Progress{
|
||||
Rows: 20,
|
||||
Bytes: 40,
|
||||
TotalRows: 100,
|
||||
Elapsed: 40 * time.Millisecond,
|
||||
}
|
||||
err = tracker.ReportQueryProgress(testQueryId, testProgress2)
|
||||
require.Nil(err, "should be able to report progress multiple times while query is in progress")
|
||||
|
||||
updateQueryProgress(&expectedProgress, testProgress2)
|
||||
select {
|
||||
case qp := <-ch:
|
||||
require.Equal(qp, expectedProgress)
|
||||
default:
|
||||
require.Fail("should receive updates whenever new progress updates get reported to tracker")
|
||||
}
|
||||
select {
|
||||
case _ = <-ch:
|
||||
require.Fail("should have had only one pending update at this point")
|
||||
default:
|
||||
}
|
||||
|
||||
reportQueryFinished()
|
||||
select {
|
||||
case _, isSubscriptionChannelOpen := <-ch:
|
||||
require.False(isSubscriptionChannelOpen, "subscription channels should get closed after query finishes")
|
||||
default:
|
||||
require.Fail("subscription channels should get closed after query finishes")
|
||||
}
|
||||
|
||||
err = tracker.ReportQueryProgress(testQueryId, testProgress)
|
||||
require.NotNil(err, "shouldn't be able to report query progress after query has finished")
|
||||
require.Equal(err.Type(), model.ErrorNotFound)
|
||||
|
||||
ch, unsubscribe, err = tracker.SubscribeToQueryProgress(testQueryId)
|
||||
require.NotNil(err, "shouldn't be able to subscribe for progress updates after query has finished")
|
||||
require.Equal(err.Type(), model.ErrorNotFound)
|
||||
require.Nil(ch)
|
||||
require.Nil(unsubscribe)
|
||||
}
|
||||
@@ -44,7 +44,6 @@ import (
|
||||
|
||||
"log/slog"
|
||||
|
||||
queryprogress "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader/query_progress"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/resource"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/services"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/traces/smart"
|
||||
@@ -145,7 +144,6 @@ type ClickHouseReader struct {
|
||||
logsResourceKeys string
|
||||
logsTagAttributeTableV2 string
|
||||
logger *slog.Logger
|
||||
queryProgressTracker queryprogress.QueryProgressTracker
|
||||
|
||||
logsTableV2 string
|
||||
logsLocalTableV2 string
|
||||
@@ -214,7 +212,6 @@ func NewReader(
|
||||
logsTagAttributeTableV2: options.primary.LogsTagAttributeTableV2,
|
||||
liveTailRefreshSeconds: options.primary.LiveTailRefreshSeconds,
|
||||
cluster: cluster,
|
||||
queryProgressTracker: queryprogress.NewQueryProgressTracker(logger),
|
||||
logsTableV2: options.primary.LogsTableV2,
|
||||
logsLocalTableV2: options.primary.LogsLocalTableV2,
|
||||
logsResourceTableV2: options.primary.LogsResourceTableV2,
|
||||
@@ -4024,27 +4021,6 @@ func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query stri
|
||||
instrumentationtypes.CodeNamespace: "clickhouse-reader",
|
||||
instrumentationtypes.CodeFunctionName: "GetTimeSeriesResultV3",
|
||||
})
|
||||
// Hook up query progress reporting if requested.
|
||||
queryId := ctx.Value("queryId")
|
||||
if queryId != nil {
|
||||
qid, ok := queryId.(string)
|
||||
if !ok {
|
||||
r.logger.Error("GetTimeSeriesResultV3: queryId in ctx not a string as expected", "queryId", queryId)
|
||||
|
||||
} else {
|
||||
ctx = clickhouse.Context(ctx, clickhouse.WithProgress(
|
||||
func(p *clickhouse.Progress) {
|
||||
go func() {
|
||||
err := r.queryProgressTracker.ReportQueryProgress(qid, p)
|
||||
if err != nil {
|
||||
r.logger.Error("Couldn't report query progress", "queryId", qid, errorsV2.Attr(err))
|
||||
}
|
||||
}()
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := r.db.Query(ctx, query)
|
||||
|
||||
if err != nil {
|
||||
@@ -5005,18 +4981,6 @@ func (r *ClickHouseReader) GetMinAndMaxTimestampForTraceID(ctx context.Context,
|
||||
return minTime.UnixNano(), maxTime.UnixNano(), nil
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) ReportQueryStartForProgressTracking(
|
||||
queryId string,
|
||||
) (func(), *model.ApiError) {
|
||||
return r.queryProgressTracker.ReportQueryStarted(queryId)
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) SubscribeToQueryProgress(
|
||||
queryId string,
|
||||
) (<-chan model.QueryProgress, func(), *model.ApiError) {
|
||||
return r.queryProgressTracker.SubscribeToQueryProgress(queryId)
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) UpdateMetricsMetadata(ctx context.Context, orgID valuer.UUID, req *model.UpdateMetricsMetadata) *model.ApiError {
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalMetrics.StringValue(),
|
||||
|
||||
@@ -36,7 +36,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
@@ -115,9 +114,6 @@ type APIHandler struct {
|
||||
// is registers.
|
||||
SetupCompleted bool
|
||||
|
||||
// Websocket connection upgrader
|
||||
Upgrader *websocket.Upgrader
|
||||
|
||||
QueryParserAPI *queryparser.API
|
||||
|
||||
Signoz *signoz.SigNoz
|
||||
@@ -207,12 +203,6 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
|
||||
aH.SetupCompleted = true
|
||||
}
|
||||
|
||||
aH.Upgrader = &websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
return aH, nil
|
||||
}
|
||||
|
||||
@@ -354,18 +344,10 @@ func (aH *APIHandler) RegisterQueryRangeV3Routes(router *mux.Router, am *middlew
|
||||
|
||||
subRouter.HandleFunc("/filter_suggestions", am.ViewAccess(aH.getQueryBuilderSuggestions)).Methods(http.MethodGet)
|
||||
|
||||
// TODO(Raj): Remove this handler after /ws based path has been completely rolled out.
|
||||
subRouter.HandleFunc("/query_progress", am.ViewAccess(aH.GetQueryProgressUpdates)).Methods(http.MethodGet)
|
||||
|
||||
// live logs
|
||||
subRouter.HandleFunc("/logs/livetail", am.ViewAccess(aH.Signoz.Handlers.QuerierHandler.QueryRawStream)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) RegisterWebSocketPaths(router *mux.Router, am *middleware.AuthZ) {
|
||||
subRouter := router.PathPrefix("/ws").Subrouter()
|
||||
subRouter.HandleFunc("/query_progress", am.ViewAccess(aH.GetQueryProgressUpdates)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) RegisterQueryRangeV4Routes(router *mux.Router, am *middleware.AuthZ) {
|
||||
subRouter := router.PathPrefix("/api/v4").Subrouter()
|
||||
subRouter.HandleFunc("/query_range", am.ViewAccess(aH.QueryRangeV4)).Methods(http.MethodPost)
|
||||
@@ -3545,27 +3527,6 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que
|
||||
}
|
||||
}
|
||||
|
||||
// Hook up query progress tracking if requested
|
||||
queryIdHeader := r.Header.Get("X-SIGNOZ-QUERY-ID")
|
||||
if len(queryIdHeader) > 0 {
|
||||
onQueryFinished, apiErr := aH.reader.ReportQueryStartForProgressTracking(queryIdHeader)
|
||||
|
||||
if apiErr != nil {
|
||||
aH.logger.ErrorContext(ctx, "failed to report query start for progress tracking",
|
||||
"query_id", queryIdHeader, errors.Attr(apiErr),
|
||||
)
|
||||
|
||||
} else {
|
||||
// Adding queryId to the context signals clickhouse queries to report progress
|
||||
//lint:ignore SA1029 ignore for now
|
||||
ctx = context.WithValue(ctx, "queryId", queryIdHeader)
|
||||
|
||||
defer func() {
|
||||
go onQueryFinished()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
instrumentationtypes.CodeNamespace: "app",
|
||||
instrumentationtypes.CodeFunctionName: "QueryRange",
|
||||
@@ -3747,73 +3708,6 @@ func (aH *APIHandler) QueryRangeV3(w http.ResponseWriter, r *http.Request) {
|
||||
aH.queryRangeV3(r.Context(), queryRangeParams, w, r)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) GetQueryProgressUpdates(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade connection to websocket, sending back the requested protocol
|
||||
// value for sec-websocket-protocol
|
||||
//
|
||||
// Since js websocket API doesn't allow setting headers, this header is often
|
||||
// used for passing auth tokens. As per websocket spec the connection will only
|
||||
// succeed if the requested `Sec-Websocket-Protocol` is sent back as a header
|
||||
// in the upgrade response (signifying that the protocol is supported by the server).
|
||||
upgradeResponseHeaders := http.Header{}
|
||||
requestedProtocol := r.Header.Get("Sec-WebSocket-Protocol")
|
||||
if len(requestedProtocol) > 0 {
|
||||
upgradeResponseHeaders.Add("Sec-WebSocket-Protocol", requestedProtocol)
|
||||
}
|
||||
|
||||
c, err := aH.Upgrader.Upgrade(w, r, upgradeResponseHeaders)
|
||||
if err != nil {
|
||||
RespondError(w, model.InternalError(fmt.Errorf(
|
||||
"couldn't upgrade connection: %w", err,
|
||||
)), nil)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
// Websocket upgrade complete. Subscribe to query progress and send updates to client
|
||||
//
|
||||
// Note: we handle any subscription problems (queryId query param missing or query already complete etc)
|
||||
// after the websocket connection upgrade by closing the channel.
|
||||
// The other option would be to handle the errors before websocket upgrade by sending an
|
||||
// error response instead of the upgrade response, but that leads to a generic websocket
|
||||
// connection failure on the client.
|
||||
|
||||
queryId := r.URL.Query().Get("q")
|
||||
|
||||
progressCh, unsubscribe, apiErr := aH.reader.SubscribeToQueryProgress(queryId)
|
||||
if apiErr != nil {
|
||||
// Shouldn't happen unless query progress requested after query finished
|
||||
aH.logger.WarnContext(r.Context(), "failed to subscribe to query progress",
|
||||
"query_id", queryId, errors.Attr(apiErr),
|
||||
)
|
||||
return
|
||||
}
|
||||
defer func() { go unsubscribe() }()
|
||||
|
||||
for queryProgress := range progressCh {
|
||||
msg, err := json.Marshal(queryProgress)
|
||||
if err != nil {
|
||||
aH.logger.ErrorContext(r.Context(), "failed to serialize progress message",
|
||||
"query_id", queryId, "progress", queryProgress, errors.Attr(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
err = c.WriteMessage(websocket.TextMessage, msg)
|
||||
if err != nil {
|
||||
aH.logger.ErrorContext(r.Context(), "failed to write progress message to websocket",
|
||||
"query_id", queryId, "msg", string(msg), errors.Attr(err),
|
||||
)
|
||||
break
|
||||
|
||||
} else {
|
||||
aH.logger.DebugContext(r.Context(), "wrote progress message to websocket",
|
||||
"query_id", queryId, "msg", string(msg),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getMetricMetadata(w http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
|
||||
@@ -164,7 +164,6 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
|
||||
api.RegisterLogsRoutes(r, am)
|
||||
api.RegisterIntegrationRoutes(r, am)
|
||||
api.RegisterQueryRangeV3Routes(r, am)
|
||||
api.RegisterWebSocketPaths(r, am)
|
||||
api.RegisterQueryRangeV4Routes(r, am)
|
||||
api.RegisterMessagingQueuesRoutes(r, am)
|
||||
api.RegisterThirdPartyApiRoutes(r, am)
|
||||
@@ -178,7 +177,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control"},
|
||||
})
|
||||
|
||||
handler := c.Handler(r)
|
||||
|
||||
@@ -91,10 +91,6 @@ type Reader interface {
|
||||
|
||||
GetMinAndMaxTimestampForTraceID(ctx context.Context, traceID []string) (int64, int64, error)
|
||||
|
||||
// Query Progress tracking helpers.
|
||||
ReportQueryStartForProgressTracking(queryId string) (reportQueryFinished func(), apiErr *model.ApiError)
|
||||
SubscribeToQueryProgress(queryId string) (<-chan model.QueryProgress, func(), *model.ApiError)
|
||||
|
||||
//trace
|
||||
GetTraceFields(ctx context.Context) (*model.GetFieldsResponse, *model.ApiError)
|
||||
UpdateTraceField(ctx context.Context, field *model.UpdateField) *model.ApiError
|
||||
|
||||
@@ -7,14 +7,6 @@ import (
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
)
|
||||
|
||||
type QueryProgress struct {
|
||||
ReadRows uint64 `json:"read_rows"`
|
||||
|
||||
ReadBytes uint64 `json:"read_bytes"`
|
||||
|
||||
ElapsedMs uint64 `json:"elapsed_ms"`
|
||||
}
|
||||
|
||||
func GetLogFieldsV3(ctx context.Context, queryRangeParams *v3.QueryRangeParamsV3, fields *GetFieldsResponse) map[string]v3.AttributeKey {
|
||||
data := map[string]v3.AttributeKey{}
|
||||
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
|
||||
|
||||
@@ -70,9 +70,9 @@ require (
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/goleak v1.3.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
@@ -86,8 +86,8 @@ require (
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/api v0.272.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/grpc v1.82.1 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/grpc v1.83.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -355,16 +355,16 @@ go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0/go.mod h1:gO9smoZe9KnZcJCqcB0lMmQ4Z5VEifYmjMTpnwtTSuQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -405,12 +405,12 @@ gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E
|
||||
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
|
||||
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
|
||||
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
|
||||
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
Reference in New Issue
Block a user