Compare commits
11 Commits
var-url-fo
...
dyn-var-ur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c80b9e4da4 | ||
|
|
f84a926310 | ||
|
|
8e39637019 | ||
|
|
c0219c6659 | ||
|
|
62bee5f003 | ||
|
|
6de8b1c2e8 | ||
|
|
b47e703a6d | ||
|
|
1d1557e79a | ||
|
|
274fd8b51f | ||
|
|
57c8381f68 | ||
|
|
a3f32b3d85 |
@@ -257,6 +257,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
s.config.APIServer.Timeout.Max,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
r.Use(middleware.NewComment().Wrap)
|
||||
|
||||
apiHandler.RegisterRoutes(r, am)
|
||||
apiHandler.RegisterLogsRoutes(r, am)
|
||||
|
||||
@@ -27,7 +27,6 @@ import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
|
||||
import {
|
||||
ALL_SELECTED_VALUE,
|
||||
filterOptionsBySearch,
|
||||
handleScrollToBottom,
|
||||
prioritizeOrAddOptionForMultiSelect,
|
||||
@@ -39,6 +38,8 @@ enum ToggleTagValue {
|
||||
All = 'All',
|
||||
}
|
||||
|
||||
const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
|
||||
|
||||
const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
placeholder = 'Search...',
|
||||
className,
|
||||
@@ -1832,7 +1833,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
onSearch={handleSearch}
|
||||
value={displayValue}
|
||||
onChange={(newValue): void => {
|
||||
console.log('newValue', newValue);
|
||||
handleInternalChange(newValue, false);
|
||||
}}
|
||||
onClear={onClearHandler}
|
||||
|
||||
@@ -3,8 +3,6 @@ import { OptionData } from './types';
|
||||
|
||||
export const SPACEKEY = ' ';
|
||||
|
||||
export const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
|
||||
|
||||
export const prioritizeOrAddOptionForSingleSelect = (
|
||||
options: OptionData[],
|
||||
value: string,
|
||||
|
||||
@@ -49,5 +49,4 @@ export enum QueryParams {
|
||||
tab = 'tab',
|
||||
thresholds = 'thresholds',
|
||||
selectedExplorerView = 'selectedExplorerView',
|
||||
variables = 'variables',
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Row } from 'antd';
|
||||
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||
import './DashboardVariableSelection.styles.scss';
|
||||
|
||||
import { Alert, Row } from 'antd';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -29,8 +28,6 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
setVariablesToGetUpdated,
|
||||
} = useDashboard();
|
||||
|
||||
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
|
||||
|
||||
const { data } = selectedDashboard || {};
|
||||
|
||||
const { variables } = data || {};
|
||||
@@ -64,11 +61,8 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
tableRowData.sort((a, b) => a.order - b.order);
|
||||
|
||||
setVariablesTableData(tableRowData);
|
||||
|
||||
// Initialize variables with default values if not in URL
|
||||
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
|
||||
}
|
||||
}, [getUrlVariables, updateUrlVariable, variables]);
|
||||
}, [variables]);
|
||||
|
||||
useEffect(() => {
|
||||
if (variablesTableData.length > 0) {
|
||||
@@ -117,12 +111,6 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
if (id) {
|
||||
updateLocalStorageDashboardVariables(name, value, allSelected);
|
||||
|
||||
if (allSelected) {
|
||||
updateUrlVariable(name || id, ALL_SELECTED_VALUE);
|
||||
} else {
|
||||
updateUrlVariable(name || id, value);
|
||||
}
|
||||
|
||||
if (selectedDashboard) {
|
||||
setSelectedDashboard((prev) => {
|
||||
if (prev) {
|
||||
|
||||
@@ -135,6 +135,17 @@ function VariableItem({
|
||||
) {
|
||||
const value = variableData.selectedValue;
|
||||
let allSelected = false;
|
||||
// The default value for multi-select is ALL and first value for
|
||||
// single select
|
||||
// console.log(valueNotInList);
|
||||
// if (valueNotInList) {
|
||||
// if (variableData.multiSelect) {
|
||||
// value = newOptionsData;
|
||||
// allSelected = true;
|
||||
// } else {
|
||||
// [value] = newOptionsData;
|
||||
// }
|
||||
// } else
|
||||
|
||||
if (variableData.multiSelect) {
|
||||
const { selectedValue } = variableData;
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import useVariablesFromUrl from '../useVariablesFromUrl';
|
||||
|
||||
describe('useVariablesFromUrl', () => {
|
||||
it('should initialize with empty variables when no URL params exist', () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/'],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVariablesFromUrl(), {
|
||||
wrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<Router history={history}>{children}</Router>
|
||||
),
|
||||
});
|
||||
|
||||
expect(result.current.getUrlVariables()).toEqual({});
|
||||
});
|
||||
|
||||
it('should correctly parse variables from URL', () => {
|
||||
const mockVariables = {
|
||||
var1: { selectedValue: 'value1', allSelected: false },
|
||||
var2: { selectedValue: ['value2', 'value3'], allSelected: true },
|
||||
};
|
||||
|
||||
const encodedVariables = encodeURIComponent(JSON.stringify(mockVariables));
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: [`/?${QueryParams.variableConfigs}=${encodedVariables}`],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVariablesFromUrl(), {
|
||||
wrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<Router history={history}>{children}</Router>
|
||||
),
|
||||
});
|
||||
|
||||
expect(result.current.getUrlVariables()).toEqual(mockVariables);
|
||||
});
|
||||
|
||||
it('should handle malformed URL parameters gracefully', () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: [`/?${QueryParams.variableConfigs}=invalid-json`],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVariablesFromUrl(), {
|
||||
wrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<Router history={history}>{children}</Router>
|
||||
),
|
||||
});
|
||||
|
||||
// Should return empty object when JSON parsing fails
|
||||
expect(result.current.getUrlVariables()).toEqual({});
|
||||
});
|
||||
|
||||
it('should set variables to URL correctly', () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/'],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVariablesFromUrl(), {
|
||||
wrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<Router history={history}>{children}</Router>
|
||||
),
|
||||
});
|
||||
|
||||
const mockVariables = {
|
||||
var1: { selectedValue: 'value1', allSelected: false },
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.setUrlVariables(mockVariables);
|
||||
});
|
||||
|
||||
// Check if the URL was updated correctly
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
const urlVariables = searchParams.get(QueryParams.variableConfigs);
|
||||
|
||||
expect(urlVariables).toBeTruthy();
|
||||
expect(JSON.parse(decodeURIComponent(urlVariables || ''))).toEqual(
|
||||
mockVariables,
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove variables param from URL when empty object is provided', () => {
|
||||
const mockVariables = {
|
||||
var1: { selectedValue: 'value1', allSelected: false },
|
||||
};
|
||||
|
||||
const encodedVariables = encodeURIComponent(JSON.stringify(mockVariables));
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: [`/?${QueryParams.variableConfigs}=${encodedVariables}`],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVariablesFromUrl(), {
|
||||
wrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<Router history={history}>{children}</Router>
|
||||
),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setUrlVariables({});
|
||||
});
|
||||
|
||||
// Check if the URL param was removed
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.has(QueryParams.variableConfigs)).toBe(false);
|
||||
});
|
||||
|
||||
it('should update a specific variable correctly', () => {
|
||||
const initialVariables = {
|
||||
var1: { selectedValue: 'value1', allSelected: false },
|
||||
var2: { selectedValue: ['value2'], allSelected: true },
|
||||
};
|
||||
|
||||
const encodedVariables = encodeURIComponent(JSON.stringify(initialVariables));
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: [`/?${QueryParams.variableConfigs}=${encodedVariables}`],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVariablesFromUrl(), {
|
||||
wrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<Router history={history}>{children}</Router>
|
||||
),
|
||||
});
|
||||
|
||||
const newValue: IDashboardVariable['selectedValue'] = 'updated-value';
|
||||
|
||||
act(() => {
|
||||
result.current.updateUrlVariable('var1', newValue, true);
|
||||
});
|
||||
|
||||
// Check if only the specified variable was updated
|
||||
const updatedVariables = result.current.getUrlVariables();
|
||||
expect(updatedVariables.var1).toEqual({
|
||||
selectedValue: newValue,
|
||||
allSelected: true,
|
||||
});
|
||||
expect(updatedVariables.var2).toEqual(initialVariables.var2);
|
||||
});
|
||||
|
||||
it('should preserve other URL parameters when updating variables', () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/?otherParam=value'],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useVariablesFromUrl(), {
|
||||
wrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<Router history={history}>{children}</Router>
|
||||
),
|
||||
});
|
||||
|
||||
const mockVariables = {
|
||||
var1: { selectedValue: 'value1', allSelected: false },
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.setUrlVariables(mockVariables);
|
||||
});
|
||||
|
||||
// Check if other params are preserved
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
expect(searchParams.get('otherParam')).toBe('value');
|
||||
expect(searchParams.has(QueryParams.variableConfigs)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
interface LocalStoreDashboardVariables {
|
||||
[name: string]:
|
||||
| IDashboardVariable['selectedValue'][]
|
||||
| IDashboardVariable['selectedValue'];
|
||||
}
|
||||
|
||||
interface UseVariablesFromUrlReturn {
|
||||
getUrlVariables: () => LocalStoreDashboardVariables;
|
||||
setUrlVariables: (variables: LocalStoreDashboardVariables) => void;
|
||||
updateUrlVariable: (
|
||||
name: string,
|
||||
selectedValue: IDashboardVariable['selectedValue'],
|
||||
) => void;
|
||||
clearUrlVariables: () => void;
|
||||
}
|
||||
|
||||
const useVariablesFromUrl = (): UseVariablesFromUrlReturn => {
|
||||
const urlQuery = useUrlQuery();
|
||||
const history = useHistory();
|
||||
|
||||
const getUrlVariables = useCallback((): LocalStoreDashboardVariables => {
|
||||
const variablesParam = urlQuery.get(QueryParams.variables);
|
||||
|
||||
if (!variablesParam) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(variablesParam));
|
||||
} catch (error) {
|
||||
console.error('Failed to parse variables from URL:', error);
|
||||
return {};
|
||||
}
|
||||
}, [urlQuery]);
|
||||
|
||||
const setUrlVariables = useCallback(
|
||||
(variables: LocalStoreDashboardVariables): void => {
|
||||
const params = new URLSearchParams(urlQuery.toString());
|
||||
|
||||
if (Object.keys(variables).length === 0) {
|
||||
params.delete(QueryParams.variables);
|
||||
} else {
|
||||
try {
|
||||
const encodedVariables = encodeURIComponent(JSON.stringify(variables));
|
||||
params.set(QueryParams.variables, encodedVariables);
|
||||
} catch (error) {
|
||||
console.error('Failed to serialize variables for URL:', error);
|
||||
}
|
||||
}
|
||||
|
||||
history.replace({
|
||||
search: params.toString(),
|
||||
});
|
||||
},
|
||||
[history, urlQuery],
|
||||
);
|
||||
|
||||
const clearUrlVariables = useCallback((): void => {
|
||||
const params = new URLSearchParams(urlQuery.toString());
|
||||
params.delete(QueryParams.variables);
|
||||
params.delete('options');
|
||||
|
||||
history.replace({
|
||||
search: params.toString(),
|
||||
});
|
||||
}, [history, urlQuery]);
|
||||
|
||||
const updateUrlVariable = useCallback(
|
||||
(name: string, selectedValue: IDashboardVariable['selectedValue']): void => {
|
||||
const currentVariables = getUrlVariables();
|
||||
|
||||
const updatedVariables = {
|
||||
...currentVariables,
|
||||
[name]: selectedValue,
|
||||
};
|
||||
|
||||
setUrlVariables(updatedVariables as LocalStoreDashboardVariables);
|
||||
},
|
||||
[getUrlVariables, setUrlVariables],
|
||||
);
|
||||
|
||||
return {
|
||||
getUrlVariables,
|
||||
setUrlVariables,
|
||||
updateUrlVariable,
|
||||
clearUrlVariables,
|
||||
};
|
||||
};
|
||||
|
||||
export default useVariablesFromUrl;
|
||||
@@ -2,13 +2,11 @@
|
||||
import { Modal } from 'antd';
|
||||
import getDashboard from 'api/v1/dashboards/id/get';
|
||||
import locked from 'api/v1/dashboards/id/lock';
|
||||
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
@@ -18,7 +16,6 @@ import isEqual from 'lodash-es/isEqual';
|
||||
import isUndefined from 'lodash-es/isUndefined';
|
||||
import omitBy from 'lodash-es/omitBy';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import {
|
||||
createContext,
|
||||
@@ -201,12 +198,6 @@ export function DashboardProvider({
|
||||
updateLocalStorageDashboardVariables,
|
||||
} = useDashboardVariablesFromLocalStorage(dashboardId);
|
||||
|
||||
const {
|
||||
getUrlVariables,
|
||||
updateUrlVariable,
|
||||
clearUrlVariables,
|
||||
} = useVariablesFromUrl();
|
||||
|
||||
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
|
||||
const modalRef = useRef<any>(null);
|
||||
|
||||
@@ -217,14 +208,6 @@ export function DashboardProvider({
|
||||
|
||||
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
|
||||
|
||||
// Clear variable configs when not on dashboard pages
|
||||
useEffect(() => {
|
||||
const isOnDashboardPage = !!isDashboardPage || !!isDashboardWidgetPage;
|
||||
if (!isOnDashboardPage) {
|
||||
clearUrlVariables();
|
||||
}
|
||||
}, [isDashboardPage, isDashboardWidgetPage, clearUrlVariables]);
|
||||
|
||||
const mergeDBWithLocalStorage = (
|
||||
data: Dashboard,
|
||||
localStorageVariables: any,
|
||||
@@ -234,28 +217,11 @@ export function DashboardProvider({
|
||||
const updatedVariables = data.data.variables;
|
||||
Object.keys(data.data.variables).forEach((variable) => {
|
||||
const variableData = data.data.variables[variable];
|
||||
|
||||
const variablesFromUrl = getUrlVariables();
|
||||
// values from url
|
||||
const urlVariable = variableData?.name
|
||||
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
|
||||
: variablesFromUrl[variableData.id];
|
||||
|
||||
let updatedVariable = {
|
||||
const updatedVariable = {
|
||||
...data.data.variables[variable],
|
||||
...localStorageVariables[variableData.name as any],
|
||||
};
|
||||
|
||||
// respect the url variable if it is set, override the others
|
||||
if (urlVariable) {
|
||||
updatedVariable = {
|
||||
...updatedVariable,
|
||||
...(urlVariable !== ALL_SELECTED_VALUE &&
|
||||
updatedVariable?.showALLOption && { selectedValue: urlVariable }),
|
||||
...(urlVariable === ALL_SELECTED_VALUE && { allSelected: true }),
|
||||
};
|
||||
}
|
||||
|
||||
updatedVariables[variable] = updatedVariable;
|
||||
});
|
||||
updatedData.data.variables = updatedVariables;
|
||||
@@ -314,7 +280,7 @@ export function DashboardProvider({
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
throw error;
|
||||
return;
|
||||
} finally {
|
||||
setIsDashboardFetching(false);
|
||||
}
|
||||
@@ -323,17 +289,9 @@ export function DashboardProvider({
|
||||
onError: (error) => {
|
||||
showErrorModal(error as APIError);
|
||||
},
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
onSuccess: (data) => {
|
||||
// if the url variable is not set for any variable, set it to the default value
|
||||
const variables = data?.data.data?.variables;
|
||||
if (variables) {
|
||||
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
|
||||
}
|
||||
|
||||
if (!data?.data) return;
|
||||
const updatedDashboardData = transformDashboardVariables(data.data);
|
||||
const updatedDate = dayjs(updatedDashboardData.updatedAt);
|
||||
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
|
||||
const updatedDashboardData = transformDashboardVariables(data?.data);
|
||||
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
|
||||
|
||||
setIsDashboardLocked(updatedDashboardData?.locked || false);
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { commaValuesParser } from '../../lib/dashbaordVariables/customCommaValuesParser';
|
||||
|
||||
interface UrlVariables {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes default values for dashboard variables if not already in URL
|
||||
* Handles cases where variables might be keyed by either id or name
|
||||
*
|
||||
* @param variables Dashboard variables object
|
||||
* @param getUrlVariables Function to get variables from URL
|
||||
* @param updateUrlVariable Function to update URL with variable values
|
||||
*/
|
||||
export const initializeDefaultVariables = (
|
||||
variables: Record<string, IDashboardVariable>,
|
||||
getUrlVariables: () => UrlVariables | undefined,
|
||||
updateUrlVariable: (
|
||||
name: string,
|
||||
selectedValue: IDashboardVariable['selectedValue'],
|
||||
) => void,
|
||||
): void => {
|
||||
if (!variables) return;
|
||||
|
||||
Object.values(variables).forEach((variable) => {
|
||||
const { id, name, allSelected, showALLOption } = variable;
|
||||
const urlVariables = getUrlVariables();
|
||||
|
||||
// Check if either id or name is available in URL variables
|
||||
const existsInUrl =
|
||||
(id && urlVariables?.[id]) || (name && urlVariables?.[name]);
|
||||
|
||||
const value =
|
||||
variable.type === 'CUSTOM'
|
||||
? commaValuesParser(variable?.customValue || '')
|
||||
: variable?.selectedValue || variable?.defaultValue;
|
||||
|
||||
if (!existsInUrl) {
|
||||
updateUrlVariable(
|
||||
name || id,
|
||||
allSelected && showALLOption ? ALL_SELECTED_VALUE : value,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -97,7 +98,12 @@ func (a *APIKey) Wrap(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
comment := ctxtypes.CommentFromContext(ctx)
|
||||
comment.Set("auth_type", "api_key")
|
||||
comment.Set("user_id", claims.UserID)
|
||||
comment.Set("org_id", claims.OrgID)
|
||||
|
||||
r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment))
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -50,7 +51,12 @@ func (a *Auth) Wrap(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
comment := ctxtypes.CommentFromContext(ctx)
|
||||
comment.Set("auth_type", "jwt")
|
||||
comment.Set("user_id", claims.UserID)
|
||||
comment.Set("org_id", claims.OrgID)
|
||||
|
||||
r = r.WithContext(ctxtypes.NewContextWithComment(ctx, comment))
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
24
pkg/http/middleware/comment.go
Normal file
24
pkg/http/middleware/comment.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
)
|
||||
|
||||
type Comment struct{}
|
||||
|
||||
func NewComment() *Comment {
|
||||
return &Comment{}
|
||||
}
|
||||
|
||||
func (middleware *Comment) Wrap(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
comment := ctxtypes.CommentFromContext(req.Context())
|
||||
comment.Merge(ctxtypes.CommentFromHTTPRequest(req))
|
||||
|
||||
req = req.WithContext(ctxtypes.NewContextWithComment(req.Context(), comment))
|
||||
next.ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
@@ -2,16 +2,11 @@ package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/gorilla/mux"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
)
|
||||
@@ -55,9 +50,6 @@ func (middleware *Logging) Wrap(next http.Handler) http.Handler {
|
||||
string(semconv.HTTPRouteKey), path,
|
||||
}
|
||||
|
||||
logCommentKVs := middleware.getLogCommentKVs(req)
|
||||
req = req.WithContext(context.WithValue(req.Context(), common.LogCommentKey, logCommentKVs))
|
||||
|
||||
badResponseBuffer := new(bytes.Buffer)
|
||||
writer := newBadResponseLoggingWriter(rw, badResponseBuffer)
|
||||
next.ServeHTTP(writer, req)
|
||||
@@ -85,67 +77,3 @@ func (middleware *Logging) Wrap(next http.Handler) http.Handler {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (middleware *Logging) getLogCommentKVs(r *http.Request) map[string]string {
|
||||
referrer := r.Header.Get("Referer")
|
||||
|
||||
var path, dashboardID, alertID, page, client, viewName, tab string
|
||||
|
||||
if referrer != "" {
|
||||
referrerURL, _ := url.Parse(referrer)
|
||||
client = "browser"
|
||||
path = referrerURL.Path
|
||||
|
||||
if strings.Contains(path, "/dashboard") {
|
||||
// Split the path into segments
|
||||
pathSegments := strings.Split(referrerURL.Path, "/")
|
||||
// The dashboard ID should be the segment after "/dashboard/"
|
||||
// Loop through pathSegments to find "dashboard" and then take the next segment as the ID
|
||||
for i, segment := range pathSegments {
|
||||
if segment == "dashboard" && i < len(pathSegments)-1 {
|
||||
// Return the next segment, which should be the dashboard ID
|
||||
dashboardID = pathSegments[i+1]
|
||||
}
|
||||
}
|
||||
page = "dashboards"
|
||||
} else if strings.Contains(path, "/alerts") {
|
||||
urlParams := referrerURL.Query()
|
||||
alertID = urlParams.Get("ruleId")
|
||||
page = "alerts"
|
||||
} else if strings.Contains(path, "logs") && strings.Contains(path, "explorer") {
|
||||
page = "logs-explorer"
|
||||
viewName = referrerURL.Query().Get("viewName")
|
||||
} else if strings.Contains(path, "/trace") || strings.Contains(path, "traces-explorer") {
|
||||
page = "traces-explorer"
|
||||
viewName = referrerURL.Query().Get("viewName")
|
||||
} else if strings.Contains(path, "/services") {
|
||||
page = "services"
|
||||
tab = referrerURL.Query().Get("tab")
|
||||
if tab == "" {
|
||||
tab = "OVER_METRICS"
|
||||
}
|
||||
} else if strings.Contains(path, "/metrics") {
|
||||
page = "metrics-explorer"
|
||||
}
|
||||
} else {
|
||||
client = "api"
|
||||
}
|
||||
|
||||
var email string
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err == nil {
|
||||
email = claims.Email
|
||||
}
|
||||
|
||||
kvs := map[string]string{
|
||||
"path": path,
|
||||
"dashboardID": dashboardID,
|
||||
"alertID": alertID,
|
||||
"source": page,
|
||||
"client": client,
|
||||
"viewName": viewName,
|
||||
"servicesTab": tab,
|
||||
"email": email,
|
||||
}
|
||||
return kvs
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/variables"
|
||||
@@ -166,49 +166,9 @@ func (a *API) logEvent(ctx context.Context, referrer string, event *qbtypes.QBEv
|
||||
return
|
||||
}
|
||||
|
||||
properties["referrer"] = referrer
|
||||
|
||||
logsExplorerMatched, _ := regexp.MatchString(`/logs/logs-explorer(?:\?.*)?$`, referrer)
|
||||
traceExplorerMatched, _ := regexp.MatchString(`/traces-explorer(?:\?.*)?$`, referrer)
|
||||
metricsExplorerMatched, _ := regexp.MatchString(`/metrics-explorer/explorer(?:\?.*)?$`, referrer)
|
||||
dashboardMatched, _ := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer)
|
||||
alertMatched, _ := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer)
|
||||
|
||||
switch {
|
||||
case dashboardMatched:
|
||||
properties["module_name"] = "dashboard"
|
||||
case alertMatched:
|
||||
properties["module_name"] = "rule"
|
||||
case metricsExplorerMatched:
|
||||
properties["module_name"] = "metrics-explorer"
|
||||
case logsExplorerMatched:
|
||||
properties["module_name"] = "logs-explorer"
|
||||
case traceExplorerMatched:
|
||||
properties["module_name"] = "traces-explorer"
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if dashboardMatched {
|
||||
if dashboardIDRegex, err := regexp.Compile(`/dashboard/([a-f0-9\-]+)/`); err == nil {
|
||||
if matches := dashboardIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
properties["dashboard_id"] = matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
if widgetIDRegex, err := regexp.Compile(`widgetId=([a-f0-9\-]+)`); err == nil {
|
||||
if matches := widgetIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
properties["widget_id"] = matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if alertMatched {
|
||||
if alertIDRegex, err := regexp.Compile(`ruleId=(\d+)`); err == nil {
|
||||
if matches := alertIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
properties["rule_id"] = matches[1]
|
||||
}
|
||||
}
|
||||
comments := ctxtypes.CommentFromContext(ctx).Map()
|
||||
for key, value := range comments {
|
||||
properties[key] = value
|
||||
}
|
||||
|
||||
if !event.HasData {
|
||||
|
||||
@@ -3640,28 +3640,8 @@ func readRowsForTimeSeriesResult(rows driver.Rows, vars []interface{}, columnNam
|
||||
return seriesList, getPersonalisedError(rows.Err())
|
||||
}
|
||||
|
||||
func logCommentKVs(ctx context.Context) map[string]string {
|
||||
kv := ctx.Value(common.LogCommentKey)
|
||||
if kv == nil {
|
||||
return nil
|
||||
}
|
||||
logCommentKVs, ok := kv.(map[string]string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return logCommentKVs
|
||||
}
|
||||
|
||||
// GetTimeSeriesResultV3 runs the query and returns list of time series
|
||||
func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query string) ([]*v3.Series, error) {
|
||||
|
||||
ctxArgs := map[string]interface{}{"query": query}
|
||||
for k, v := range logCommentKVs(ctx) {
|
||||
ctxArgs[k] = v
|
||||
}
|
||||
|
||||
defer utils.Elapsed("GetTimeSeriesResultV3", ctxArgs)()
|
||||
|
||||
// Hook up query progress reporting if requested.
|
||||
queryId := ctx.Value("queryId")
|
||||
if queryId != nil {
|
||||
@@ -3725,20 +3705,12 @@ func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query stri
|
||||
|
||||
// GetListResultV3 runs the query and returns list of rows
|
||||
func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([]*v3.Row, error) {
|
||||
|
||||
ctxArgs := map[string]interface{}{"query": query}
|
||||
for k, v := range logCommentKVs(ctx) {
|
||||
ctxArgs[k] = v
|
||||
}
|
||||
|
||||
defer utils.Elapsed("GetListResultV3", ctxArgs)()
|
||||
|
||||
rows, err := r.db.Query(ctx, query)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("error while reading time series result", zap.Error(err))
|
||||
return nil, errors.New(err.Error())
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
var (
|
||||
|
||||
@@ -220,6 +220,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
|
||||
).Wrap)
|
||||
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
|
||||
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||
r.Use(middleware.NewComment().Wrap)
|
||||
|
||||
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger())
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package common
|
||||
|
||||
type LogCommentContextKeyType string
|
||||
|
||||
const LogCommentKey LogCommentContextKeyType = "logComment"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
@@ -369,12 +369,10 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) {
|
||||
rule.SetEvaluationTimestamp(t)
|
||||
}(time.Now())
|
||||
|
||||
kvs := map[string]string{
|
||||
"alertID": rule.ID(),
|
||||
"source": "alerts",
|
||||
"client": "query-service",
|
||||
}
|
||||
ctx = context.WithValue(ctx, common.LogCommentKey, kvs)
|
||||
comment := ctxtypes.CommentFromContext(ctx)
|
||||
comment.Set("rule_id", rule.ID())
|
||||
comment.Set("auth_type", "internal")
|
||||
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
||||
|
||||
_, err := rule.Eval(ctx, ts)
|
||||
if err != nil {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
@@ -352,12 +352,10 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
|
||||
rule.SetEvaluationTimestamp(t)
|
||||
}(time.Now())
|
||||
|
||||
kvs := map[string]string{
|
||||
"alertID": rule.ID(),
|
||||
"source": "alerts",
|
||||
"client": "query-service",
|
||||
}
|
||||
ctx = context.WithValue(ctx, common.LogCommentKey, kvs)
|
||||
comment := ctxtypes.CommentFromContext(ctx)
|
||||
comment.Set("rule_id", rule.ID())
|
||||
comment.Set("auth_type", "internal")
|
||||
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
||||
|
||||
_, err := rule.Eval(ctx, ts)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,13 +2,12 @@ package telemetrystorehook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
@@ -32,11 +31,7 @@ func NewSettings(ctx context.Context, providerSettings factory.ProviderSettings,
|
||||
func (h *provider) BeforeQuery(ctx context.Context, _ *telemetrystore.QueryEvent) context.Context {
|
||||
settings := clickhouse.Settings{}
|
||||
|
||||
// Apply default settings
|
||||
logComment := h.getLogComment(ctx)
|
||||
if logComment != "" {
|
||||
settings["log_comment"] = logComment
|
||||
}
|
||||
settings["log_comment"] = ctxtypes.CommentFromContext(ctx).String()
|
||||
|
||||
if ctx.Value("enforce_max_result_rows") != nil {
|
||||
settings["max_result_rows"] = h.settings.MaxResultRows
|
||||
@@ -91,22 +86,4 @@ func (h *provider) BeforeQuery(ctx context.Context, _ *telemetrystore.QueryEvent
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (h *provider) AfterQuery(ctx context.Context, event *telemetrystore.QueryEvent) {
|
||||
}
|
||||
|
||||
func (h *provider) getLogComment(ctx context.Context) string {
|
||||
// Get the key-value pairs from context for log comment
|
||||
kv := ctx.Value(common.LogCommentKey)
|
||||
if kv == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
logCommentKVs, ok := kv.(map[string]string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
logComment, _ := json.Marshal(logCommentKVs)
|
||||
|
||||
return string(logComment)
|
||||
}
|
||||
func (h *provider) AfterQuery(ctx context.Context, event *telemetrystore.QueryEvent) {}
|
||||
|
||||
163
pkg/types/ctxtypes/comment.go
Normal file
163
pkg/types/ctxtypes/comment.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package ctxtypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
logsExplorerRegex = regexp.MustCompile(`/logs/logs-explorer(?:\?.*)?$`)
|
||||
traceExplorerRegex = regexp.MustCompile(`/traces-explorer(?:\?.*)?$`)
|
||||
metricsExplorerRegex = regexp.MustCompile(`/metrics-explorer/explorer(?:\?.*)?$`)
|
||||
dashboardRegex = regexp.MustCompile(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`)
|
||||
dashboardIDRegex = regexp.MustCompile(`/dashboard/([a-f0-9\-]+)/`)
|
||||
widgetIDRegex = regexp.MustCompile(`widgetId=([a-f0-9\-]+)`)
|
||||
ruleRegex = regexp.MustCompile(`/alerts/(new|edit)(?:\?.*)?$`)
|
||||
ruleIDRegex = regexp.MustCompile(`ruleId=(\d+)`)
|
||||
)
|
||||
|
||||
type commentCtxKey struct{}
|
||||
|
||||
type Comment struct {
|
||||
vals map[string]string
|
||||
mtx sync.RWMutex
|
||||
}
|
||||
|
||||
func NewContextWithComment(ctx context.Context, comment *Comment) context.Context {
|
||||
return context.WithValue(ctx, commentCtxKey{}, comment)
|
||||
}
|
||||
|
||||
func CommentFromContext(ctx context.Context) *Comment {
|
||||
comment, ok := ctx.Value(commentCtxKey{}).(*Comment)
|
||||
if !ok {
|
||||
return NewComment()
|
||||
}
|
||||
|
||||
// Return a deep copy of the comment to prevent mutations from affecting the original
|
||||
copy := NewComment()
|
||||
copy.Merge(comment.Map())
|
||||
return copy
|
||||
}
|
||||
|
||||
func CommentFromHTTPRequest(req *http.Request) map[string]string {
|
||||
comments := map[string]string{}
|
||||
|
||||
referrer := req.Header.Get("Referer")
|
||||
if referrer == "" {
|
||||
return comments
|
||||
}
|
||||
|
||||
referrerURL, err := url.Parse(referrer)
|
||||
if err != nil {
|
||||
return comments
|
||||
}
|
||||
|
||||
logsExplorerMatched := logsExplorerRegex.MatchString(referrer)
|
||||
traceExplorerMatched := traceExplorerRegex.MatchString(referrer)
|
||||
metricsExplorerMatched := metricsExplorerRegex.MatchString(referrer)
|
||||
dashboardMatched := dashboardRegex.MatchString(referrer)
|
||||
ruleMatched := ruleRegex.MatchString(referrer)
|
||||
|
||||
switch {
|
||||
case dashboardMatched:
|
||||
comments["module_name"] = "dashboard"
|
||||
case ruleMatched:
|
||||
comments["module_name"] = "rule"
|
||||
case metricsExplorerMatched:
|
||||
comments["module_name"] = "metrics-explorer"
|
||||
case logsExplorerMatched:
|
||||
comments["module_name"] = "logs-explorer"
|
||||
case traceExplorerMatched:
|
||||
comments["module_name"] = "traces-explorer"
|
||||
default:
|
||||
return comments
|
||||
}
|
||||
|
||||
if dashboardMatched {
|
||||
if matches := dashboardIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
comments["dashboard_id"] = matches[1]
|
||||
}
|
||||
|
||||
if matches := widgetIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
comments["widget_id"] = matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
if ruleMatched {
|
||||
if matches := ruleIDRegex.FindStringSubmatch(referrer); len(matches) > 1 {
|
||||
comments["rule_id"] = matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
comments["http_path"] = referrerURL.Path
|
||||
|
||||
return comments
|
||||
}
|
||||
|
||||
// NewComment creates a new Comment with an empty map. It is safe to use concurrently.
|
||||
func NewComment() *Comment {
|
||||
return &Comment{vals: map[string]string{}}
|
||||
}
|
||||
|
||||
func (comment *Comment) Set(key, value string) {
|
||||
comment.mtx.Lock()
|
||||
defer comment.mtx.Unlock()
|
||||
|
||||
comment.vals[key] = value
|
||||
}
|
||||
|
||||
func (comment *Comment) Merge(vals map[string]string) {
|
||||
comment.mtx.Lock()
|
||||
defer comment.mtx.Unlock()
|
||||
|
||||
// If vals is nil, do nothing. Comment should not panic.
|
||||
if vals == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for key, value := range vals {
|
||||
comment.vals[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func (comment *Comment) Map() map[string]string {
|
||||
comment.mtx.RLock()
|
||||
defer comment.mtx.RUnlock()
|
||||
|
||||
copyOfVals := make(map[string]string)
|
||||
for key, value := range comment.vals {
|
||||
copyOfVals[key] = value
|
||||
}
|
||||
|
||||
return copyOfVals
|
||||
}
|
||||
|
||||
func (comment *Comment) String() string {
|
||||
comment.mtx.RLock()
|
||||
defer comment.mtx.RUnlock()
|
||||
|
||||
commentJSON, err := json.Marshal(comment.vals)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
|
||||
return string(commentJSON)
|
||||
}
|
||||
|
||||
func (comment *Comment) Equal(other *Comment) bool {
|
||||
if len(comment.vals) != len(other.vals) {
|
||||
return false
|
||||
}
|
||||
|
||||
for key, value := range comment.vals {
|
||||
if val, ok := other.vals[key]; !ok || val != value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
123
pkg/types/ctxtypes/comment_test.go
Normal file
123
pkg/types/ctxtypes/comment_test.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package ctxtypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommentFromHTTPRequest(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
req *http.Request
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "EmptyReferer",
|
||||
req: &http.Request{Header: http.Header{"Referer": {""}}},
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "ControlCharacterInReferer",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/logs/logs-explorer\x00"}}},
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "LogsExplorer",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/logs/logs-explorer"}}},
|
||||
expected: map[string]string{"http_path": "/logs/logs-explorer", "module_name": "logs-explorer"},
|
||||
},
|
||||
{
|
||||
name: "TracesExplorer",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/traces-explorer"}}},
|
||||
expected: map[string]string{"http_path": "/traces-explorer", "module_name": "traces-explorer"},
|
||||
},
|
||||
{
|
||||
name: "MetricsExplorer",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/metrics-explorer/explorer"}}},
|
||||
expected: map[string]string{"http_path": "/metrics-explorer/explorer", "module_name": "metrics-explorer"},
|
||||
},
|
||||
{
|
||||
name: "DashboardWithID",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/dashboard/123/new"}}},
|
||||
expected: map[string]string{"http_path": "/dashboard/123/new", "module_name": "dashboard", "dashboard_id": "123"},
|
||||
},
|
||||
{
|
||||
name: "Rule",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/alerts/new"}}},
|
||||
expected: map[string]string{"http_path": "/alerts/new", "module_name": "rule"},
|
||||
},
|
||||
{
|
||||
name: "RuleWithID",
|
||||
req: &http.Request{Header: http.Header{"Referer": {"https://signoz.io/alerts/edit?ruleId=123"}}},
|
||||
expected: map[string]string{"http_path": "/alerts/edit", "module_name": "rule", "rule_id": "123"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := CommentFromHTTPRequest(tc.req)
|
||||
|
||||
assert.True(t, (&Comment{vals: tc.expected}).Equal(&Comment{vals: actual}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentFromContext(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
comment1 := CommentFromContext(ctx)
|
||||
assert.True(t, NewComment().Equal(comment1))
|
||||
|
||||
comment1.Set("k1", "v1")
|
||||
ctx = NewContextWithComment(ctx, comment1)
|
||||
actual1 := CommentFromContext(ctx)
|
||||
assert.True(t, comment1.Equal(actual1))
|
||||
|
||||
// Get the comment from the context, mutate it, but this time do not set it back in the context
|
||||
comment2 := CommentFromContext(ctx)
|
||||
comment2.Set("k2", "v2")
|
||||
|
||||
actual2 := CommentFromContext(ctx)
|
||||
// Since comment2 was not set back in the context, it should not affect the original comment1
|
||||
assert.True(t, comment1.Equal(actual2))
|
||||
assert.False(t, comment2.Equal(actual2))
|
||||
assert.False(t, comment1.Equal(comment2))
|
||||
}
|
||||
|
||||
func TestCommentFromContextConcurrent(t *testing.T) {
|
||||
comment := NewComment()
|
||||
comment.Set("k1", "v1")
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = NewContextWithComment(ctx, comment)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
ctxs := make([]context.Context, 10)
|
||||
var mtx sync.Mutex
|
||||
wg.Add(10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
comment := CommentFromContext(ctx)
|
||||
comment.Set("k2", fmt.Sprintf("v%d", i))
|
||||
newCtx := NewContextWithComment(ctx, comment)
|
||||
mtx.Lock()
|
||||
ctxs[i] = newCtx
|
||||
mtx.Unlock()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for i, ctx := range ctxs {
|
||||
comment := CommentFromContext(ctx)
|
||||
assert.Equal(t, len(comment.vals), 2)
|
||||
assert.Equal(t, comment.vals["k1"], "v1")
|
||||
assert.Equal(t, comment.vals["k2"], fmt.Sprintf("v%d", i))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user