Compare commits

..

5 Commits

Author SHA1 Message Date
Piyush Singariya
5cadff125f Merge branch 'main' into fix/array-json 2026-04-07 17:04:31 +05:30
Vinicius Lourenço
d4dbbceab7 fix(app-routes): use redirect component to avoid flash old routes due to useEffect (#10802)
Some checks are pending
build-staging / staging (push) Blocked by required conditions
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-04-07 11:10:48 +00:00
Piyush Singariya
ec0409284c Merge branch 'main' into fix/array-json 2026-04-07 16:33:21 +05:30
Piyush Singariya
2c2e0cbb37 ci: lint fix 2026-04-07 16:32:42 +05:30
Vikrant Gupta
918cf4dfe5 fix(authz): improve error messages (#10856) 2026-04-07 08:34:53 +00:00
15 changed files with 242 additions and 1997 deletions

View File

@@ -52,7 +52,6 @@ jobs:
- ingestionkeys
- rootuser
- serviceaccount
- querier_json_body
sqlstore-provider:
- postgres
- sqlite

View File

@@ -1,4 +1,4 @@
import { ReactChild, useCallback, useEffect, useMemo, useState } from 'react';
import { ReactChild, useCallback, useMemo } from 'react';
import { matchPath, Redirect, useLocation } from 'react-router-dom';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
@@ -8,12 +8,10 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
import { OrgPreference } from 'types/api/preferences/preference';
import { Organization } from 'types/api/user/getOrganization';
import { USER_ROLES } from 'types/roles';
import { routePermission } from 'utils/permission';
@@ -25,6 +23,7 @@ import routes, {
SUPPORT_ROUTE,
} from './routes';
// eslint-disable-next-line sonarjs/cognitive-complexity
function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const location = useLocation();
const { pathname } = location;
@@ -57,7 +56,12 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const currentRoute = mapRoutes.get('current');
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
const orgData = useMemo(() => {
if (org && org.length > 0 && org[0].id !== undefined) {
return org[0];
}
return undefined;
}, [org]);
const { data: usersData, isFetching: isFetchingUsers } = useListUsers({
query: {
@@ -75,214 +79,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
return remainingUsers.length === 1;
}, [usersData?.data]);
useEffect(() => {
if (
isCloudUserVal &&
!isFetchingOrgPreferences &&
orgPreferences &&
!isFetchingUsers &&
usersData &&
usersData.data
) {
const isOnboardingComplete = orgPreferences?.find(
(preference: OrgPreference) =>
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
)?.value;
// Don't redirect to onboarding if workspace has issues (blocked, suspended, or restricted)
// User needs access to settings/billing to fix payment issues
const isWorkspaceBlocked = trialInfo?.workSpaceBlock;
const isWorkspaceSuspended = activeLicense?.state === LicenseState.DEFAULTED;
const isWorkspaceAccessRestricted =
activeLicense?.state === LicenseState.TERMINATED ||
activeLicense?.state === LicenseState.EXPIRED ||
activeLicense?.state === LicenseState.CANCELLED;
const hasWorkspaceIssue =
isWorkspaceBlocked || isWorkspaceSuspended || isWorkspaceAccessRestricted;
if (hasWorkspaceIssue) {
return;
}
const isFirstUser = checkFirstTimeUser();
if (
isFirstUser &&
!isOnboardingComplete &&
// if the current route is allowed to be overriden by org onboarding then only do the same
!ROUTES_NOT_TO_BE_OVERRIDEN.includes(pathname)
) {
history.push(ROUTES.ONBOARDING);
}
}
}, [
checkFirstTimeUser,
isCloudUserVal,
isFetchingOrgPreferences,
isFetchingUsers,
orgPreferences,
usersData,
pathname,
trialInfo?.workSpaceBlock,
activeLicense?.state,
]);
const navigateToWorkSpaceBlocked = useCallback((): void => {
const isRouteEnabledForWorkspaceBlockedState =
isAdmin &&
(pathname === ROUTES.SETTINGS ||
pathname === ROUTES.ORG_SETTINGS ||
pathname === ROUTES.MEMBERS_SETTINGS ||
pathname === ROUTES.BILLING ||
pathname === ROUTES.MY_SETTINGS);
if (
pathname &&
pathname !== ROUTES.WORKSPACE_LOCKED &&
!isRouteEnabledForWorkspaceBlockedState
) {
history.push(ROUTES.WORKSPACE_LOCKED);
}
}, [isAdmin, pathname]);
const navigateToWorkSpaceAccessRestricted = useCallback((): void => {
if (pathname && pathname !== ROUTES.WORKSPACE_ACCESS_RESTRICTED) {
history.push(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
}
}, [pathname]);
useEffect(() => {
if (!isFetchingActiveLicense && activeLicense) {
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
const isExpired = activeLicense.state === LicenseState.EXPIRED;
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
const { platform } = activeLicense;
if (isWorkspaceAccessRestricted && platform === LicensePlatform.CLOUD) {
navigateToWorkSpaceAccessRestricted();
}
}
}, [
isFetchingActiveLicense,
activeLicense,
navigateToWorkSpaceAccessRestricted,
]);
useEffect(() => {
if (!isFetchingActiveLicense) {
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
if (
shouldBlockWorkspace &&
activeLicense?.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceBlocked();
}
}
}, [
isFetchingActiveLicense,
trialInfo?.workSpaceBlock,
activeLicense?.platform,
navigateToWorkSpaceBlocked,
]);
const navigateToWorkSpaceSuspended = useCallback((): void => {
if (pathname && pathname !== ROUTES.WORKSPACE_SUSPENDED) {
history.push(ROUTES.WORKSPACE_SUSPENDED);
}
}, [pathname]);
useEffect(() => {
if (!isFetchingActiveLicense && activeLicense) {
const shouldSuspendWorkspace =
activeLicense.state === LicenseState.DEFAULTED;
if (
shouldSuspendWorkspace &&
activeLicense.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceSuspended();
}
}
}, [isFetchingActiveLicense, activeLicense, navigateToWorkSpaceSuspended]);
useEffect(() => {
if (org && org.length > 0 && org[0].id !== undefined) {
setOrgData(org[0]);
}
}, [org]);
// if the feature flag is enabled and the current route is /get-started then redirect to /get-started-with-signoz-cloud
useEffect(() => {
if (
currentRoute?.path === ROUTES.GET_STARTED &&
featureFlags?.find((e) => e.name === FeatureKeys.ONBOARDING_V3)?.active
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
}
}, [currentRoute, featureFlags]);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
// if it is an old route navigate to the new route
if (isOldRoute) {
// this will be handled by the redirect component below
return;
}
// if the current route is public dashboard then don't redirect to login
const isPublicDashboard = currentRoute?.path === ROUTES.PUBLIC_DASHBOARD;
if (isPublicDashboard) {
return;
}
// if the current route
if (currentRoute) {
const { isPrivate, key } = currentRoute;
if (isPrivate) {
if (isLoggedInState) {
const route = routePermission[key];
if (route && route.find((e) => e === user.role) === undefined) {
history.push(ROUTES.UN_AUTHORIZED);
}
} else {
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
history.push(ROUTES.LOGIN);
}
} else if (isLoggedInState) {
const fromPathname = getLocalStorageApi(
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
);
if (fromPathname) {
history.push(fromPathname);
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
} else if (pathname !== ROUTES.SOMETHING_WENT_WRONG) {
history.push(ROUTES.HOME);
}
} else {
// do nothing as the unauthenticated routes are LOGIN and SIGNUP and the LOGIN container takes care of routing to signup if
// setup is not completed
}
} else if (isLoggedInState) {
const fromPathname = getLocalStorageApi(
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
);
if (fromPathname) {
history.push(fromPathname);
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
} else {
history.push(ROUTES.HOME);
}
} else {
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
history.push(ROUTES.LOGIN);
}
}, [isLoggedInState, pathname, user, isOldRoute, currentRoute, location]);
// Handle old routes - redirect to new routes
if (isOldRoute) {
const redirectUrl = oldNewRoutesMapping[pathname];
return (
@@ -296,7 +93,143 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
);
}
// NOTE: disabling this rule as there is no need to have div
// Public dashboard - no redirect needed
const isPublicDashboard = currentRoute?.path === ROUTES.PUBLIC_DASHBOARD;
if (isPublicDashboard) {
return <>{children}</>;
}
// Check for workspace access restriction (cloud only)
const isCloudPlatform = activeLicense?.platform === LicensePlatform.CLOUD;
if (!isFetchingActiveLicense && activeLicense && isCloudPlatform) {
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
const isExpired = activeLicense.state === LicenseState.EXPIRED;
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
if (
isWorkspaceAccessRestricted &&
pathname !== ROUTES.WORKSPACE_ACCESS_RESTRICTED
) {
return <Redirect to={ROUTES.WORKSPACE_ACCESS_RESTRICTED} />;
}
// Check for workspace suspended (DEFAULTED)
const shouldSuspendWorkspace = activeLicense.state === LicenseState.DEFAULTED;
if (shouldSuspendWorkspace && pathname !== ROUTES.WORKSPACE_SUSPENDED) {
return <Redirect to={ROUTES.WORKSPACE_SUSPENDED} />;
}
}
// Check for workspace blocked (trial expired)
if (!isFetchingActiveLicense && isCloudPlatform && trialInfo?.workSpaceBlock) {
const isRouteEnabledForWorkspaceBlockedState =
isAdmin &&
(pathname === ROUTES.SETTINGS ||
pathname === ROUTES.ORG_SETTINGS ||
pathname === ROUTES.MEMBERS_SETTINGS ||
pathname === ROUTES.BILLING ||
pathname === ROUTES.MY_SETTINGS);
if (
pathname !== ROUTES.WORKSPACE_LOCKED &&
!isRouteEnabledForWorkspaceBlockedState
) {
return <Redirect to={ROUTES.WORKSPACE_LOCKED} />;
}
}
// Check for onboarding redirect (cloud users, first user, onboarding not complete)
if (
isCloudUserVal &&
!isFetchingOrgPreferences &&
orgPreferences &&
!isFetchingUsers &&
usersData &&
usersData.data
) {
const isOnboardingComplete = orgPreferences?.find(
(preference: OrgPreference) =>
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
)?.value;
// Don't redirect to onboarding if workspace has issues
const isWorkspaceBlocked = trialInfo?.workSpaceBlock;
const isWorkspaceSuspended = activeLicense?.state === LicenseState.DEFAULTED;
const isWorkspaceAccessRestricted =
activeLicense?.state === LicenseState.TERMINATED ||
activeLicense?.state === LicenseState.EXPIRED ||
activeLicense?.state === LicenseState.CANCELLED;
const hasWorkspaceIssue =
isWorkspaceBlocked || isWorkspaceSuspended || isWorkspaceAccessRestricted;
if (!hasWorkspaceIssue) {
const isFirstUser = checkFirstTimeUser();
if (
isFirstUser &&
!isOnboardingComplete &&
!ROUTES_NOT_TO_BE_OVERRIDEN.includes(pathname) &&
pathname !== ROUTES.ONBOARDING
) {
return <Redirect to={ROUTES.ONBOARDING} />;
}
}
}
// Check for GET_STARTED → GET_STARTED_WITH_CLOUD redirect (feature flag)
if (
currentRoute?.path === ROUTES.GET_STARTED &&
featureFlags?.find((e) => e.name === FeatureKeys.ONBOARDING_V3)?.active
) {
return <Redirect to={ROUTES.GET_STARTED_WITH_CLOUD} />;
}
// Main routing logic
if (currentRoute) {
const { isPrivate, key } = currentRoute;
if (isPrivate) {
if (isLoggedInState) {
const route = routePermission[key];
if (route && route.find((e) => e === user.role) === undefined) {
return <Redirect to={ROUTES.UN_AUTHORIZED} />;
}
} else {
// Save current path and redirect to login
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
return <Redirect to={ROUTES.LOGIN} />;
}
} else if (isLoggedInState) {
// Non-private route, but user is logged in
const fromPathname = getLocalStorageApi(
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
);
if (fromPathname) {
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
return <Redirect to={fromPathname} />;
}
if (pathname !== ROUTES.SOMETHING_WENT_WRONG) {
return <Redirect to={ROUTES.HOME} />;
}
}
// Non-private route, user not logged in - let login/signup pages handle it
} else if (isLoggedInState) {
// Unknown route, logged in
const fromPathname = getLocalStorageApi(
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
);
if (fromPathname) {
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
return <Redirect to={fromPathname} />;
}
return <Redirect to={ROUTES.HOME} />;
} else {
// Unknown route, not logged in
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
return <Redirect to={ROUTES.LOGIN} />;
}
return <>{children}</>;
}

View File

@@ -6,7 +6,6 @@ import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { AppContext } from 'providers/App/App';
import { IAppContext, IUser } from 'providers/App/types';
import {
@@ -22,19 +21,6 @@ import { ROLES, USER_ROLES } from 'types/roles';
import PrivateRoute from '../Private';
// Mock history module
jest.mock('lib/history', () => ({
__esModule: true,
default: {
push: jest.fn(),
location: { pathname: '/', search: '', hash: '' },
listen: jest.fn(),
createHref: jest.fn(),
},
}));
const mockHistoryPush = history.push as jest.Mock;
// Mock localStorage APIs
const mockLocalStorage: Record<string, string> = {};
jest.mock('api/browser/localstorage/get', () => ({
@@ -239,20 +225,18 @@ function renderPrivateRoute(options: RenderPrivateRouteOptions = {}): void {
}
// Generic assertion helpers for navigation behavior
// Using these allows easier refactoring when switching from history.push to Redirect component
// Using location-based assertions since Private.tsx now uses Redirect component
async function assertRedirectsTo(targetRoute: string): Promise<void> {
await waitFor(() => {
expect(mockHistoryPush).toHaveBeenCalledWith(targetRoute);
expect(screen.getByTestId('location-display')).toHaveTextContent(targetRoute);
});
}
function assertNoRedirect(): void {
expect(mockHistoryPush).not.toHaveBeenCalled();
}
function assertDoesNotRedirectTo(targetRoute: string): void {
expect(mockHistoryPush).not.toHaveBeenCalledWith(targetRoute);
function assertStaysOnRoute(expectedRoute: string): void {
expect(screen.getByTestId('location-display')).toHaveTextContent(
expectedRoute,
);
}
function assertRendersChildren(): void {
@@ -350,7 +334,7 @@ describe('PrivateRoute', () => {
});
assertRendersChildren();
assertNoRedirect();
assertStaysOnRoute('/public/dashboard/abc123');
});
it('should render children for public dashboard route when logged in without redirecting', () => {
@@ -362,7 +346,7 @@ describe('PrivateRoute', () => {
assertRendersChildren();
// Critical: without the isPublicDashboard early return, logged-in users
// would be redirected to HOME due to the non-private route handling
assertNoRedirect();
assertStaysOnRoute('/public/dashboard/abc123');
});
});
@@ -420,7 +404,7 @@ describe('PrivateRoute', () => {
});
assertRendersChildren();
assertNoRedirect();
assertStaysOnRoute(ROUTES.HOME);
});
it('should redirect to unauthorized when VIEWER tries to access admin-only route /alerts/new', async () => {
@@ -529,7 +513,7 @@ describe('PrivateRoute', () => {
appContext: { isLoggedIn: true },
});
assertDoesNotRedirectTo(ROUTES.HOME);
assertStaysOnRoute(ROUTES.SOMETHING_WENT_WRONG);
});
});
@@ -541,7 +525,7 @@ describe('PrivateRoute', () => {
});
// Should not redirect - login page handles its own routing
assertNoRedirect();
assertStaysOnRoute(ROUTES.LOGIN);
});
it('should not redirect when not logged in user visits signup page', () => {
@@ -550,7 +534,7 @@ describe('PrivateRoute', () => {
appContext: { isLoggedIn: false },
});
assertNoRedirect();
assertStaysOnRoute(ROUTES.SIGN_UP);
});
it('should not redirect when not logged in user visits password reset page', () => {
@@ -559,7 +543,7 @@ describe('PrivateRoute', () => {
appContext: { isLoggedIn: false },
});
assertNoRedirect();
assertStaysOnRoute(ROUTES.PASSWORD_RESET);
});
it('should not redirect when not logged in user visits forgot password page', () => {
@@ -568,7 +552,7 @@ describe('PrivateRoute', () => {
appContext: { isLoggedIn: false },
});
assertNoRedirect();
assertStaysOnRoute(ROUTES.FORGOT_PASSWORD);
});
});
@@ -657,7 +641,7 @@ describe('PrivateRoute', () => {
});
// Admin should be able to access settings even when workspace is blocked
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
assertStaysOnRoute(ROUTES.SETTINGS);
});
it('should allow ADMIN to access /settings/billing when workspace is blocked', () => {
@@ -673,7 +657,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
assertStaysOnRoute(ROUTES.BILLING);
});
it('should allow ADMIN to access /settings/org-settings when workspace is blocked', () => {
@@ -689,7 +673,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
assertStaysOnRoute(ROUTES.ORG_SETTINGS);
});
it('should allow ADMIN to access /settings/members when workspace is blocked', () => {
@@ -705,7 +689,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
assertStaysOnRoute(ROUTES.MEMBERS_SETTINGS);
});
it('should allow ADMIN to access /settings/my-settings when workspace is blocked', () => {
@@ -721,7 +705,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
assertStaysOnRoute(ROUTES.MY_SETTINGS);
});
it('should redirect VIEWER to workspace locked even when trying to access settings', async () => {
@@ -832,7 +816,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
assertStaysOnRoute(ROUTES.WORKSPACE_LOCKED);
});
it('should not redirect self-hosted users to workspace locked even when workSpaceBlock is true', () => {
@@ -849,7 +833,7 @@ describe('PrivateRoute', () => {
isCloudUser: false,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
assertStaysOnRoute(ROUTES.HOME);
});
});
@@ -919,7 +903,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
assertStaysOnRoute(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
});
it('should not redirect self-hosted users to workspace access restricted when license is terminated', () => {
@@ -936,7 +920,7 @@ describe('PrivateRoute', () => {
isCloudUser: false,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
assertStaysOnRoute(ROUTES.HOME);
});
it('should not redirect when license is ACTIVE', () => {
@@ -953,7 +937,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
assertStaysOnRoute(ROUTES.HOME);
});
it('should not redirect when license is EVALUATING', () => {
@@ -970,7 +954,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
assertStaysOnRoute(ROUTES.HOME);
});
});
@@ -1006,7 +990,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_SUSPENDED);
assertStaysOnRoute(ROUTES.WORKSPACE_SUSPENDED);
});
it('should not redirect self-hosted users to workspace suspended when license is defaulted', () => {
@@ -1023,7 +1007,7 @@ describe('PrivateRoute', () => {
isCloudUser: false,
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_SUSPENDED);
assertStaysOnRoute(ROUTES.HOME);
});
});
@@ -1043,6 +1027,11 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
// Wait for the users query to complete and trigger re-render
await act(async () => {
await Promise.resolve();
});
await assertRedirectsTo(ROUTES.ONBOARDING);
});
@@ -1058,7 +1047,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
assertStaysOnRoute(ROUTES.HOME);
});
it('should not redirect to onboarding when onboarding is already complete', async () => {
@@ -1084,7 +1073,7 @@ describe('PrivateRoute', () => {
// Critical: if isOnboardingComplete check is broken (always false),
// this test would fail because all other conditions for redirect ARE met
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
assertStaysOnRoute(ROUTES.HOME);
});
it('should not redirect to onboarding for non-cloud users', () => {
@@ -1099,7 +1088,7 @@ describe('PrivateRoute', () => {
isCloudUser: false,
});
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
assertStaysOnRoute(ROUTES.HOME);
});
it('should not redirect to onboarding when on /workspace-locked route', () => {
@@ -1114,7 +1103,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
assertStaysOnRoute(ROUTES.WORKSPACE_LOCKED);
});
it('should not redirect to onboarding when on /workspace-suspended route', () => {
@@ -1129,7 +1118,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
assertStaysOnRoute(ROUTES.WORKSPACE_SUSPENDED);
});
it('should not redirect to onboarding when workspace is blocked and accessing billing', async () => {
@@ -1156,7 +1145,7 @@ describe('PrivateRoute', () => {
});
// Should NOT redirect to onboarding - user needs to access billing to fix payment
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
assertStaysOnRoute(ROUTES.BILLING);
});
it('should not redirect to onboarding when workspace is blocked and accessing settings', async () => {
@@ -1180,7 +1169,7 @@ describe('PrivateRoute', () => {
await Promise.resolve();
});
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
assertStaysOnRoute(ROUTES.SETTINGS);
});
it('should not redirect to onboarding when workspace is suspended (DEFAULTED)', async () => {
@@ -1207,7 +1196,7 @@ describe('PrivateRoute', () => {
});
// Should redirect to WORKSPACE_SUSPENDED, not ONBOARDING
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
await assertRedirectsTo(ROUTES.WORKSPACE_SUSPENDED);
});
it('should not redirect to onboarding when workspace is access restricted (TERMINATED)', async () => {
@@ -1234,7 +1223,7 @@ describe('PrivateRoute', () => {
});
// Should redirect to WORKSPACE_ACCESS_RESTRICTED, not ONBOARDING
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
await assertRedirectsTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
});
it('should not redirect to onboarding when workspace is access restricted (EXPIRED)', async () => {
@@ -1260,7 +1249,7 @@ describe('PrivateRoute', () => {
await Promise.resolve();
});
assertDoesNotRedirectTo(ROUTES.ONBOARDING);
await assertRedirectsTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
});
});
@@ -1302,7 +1291,7 @@ describe('PrivateRoute', () => {
},
});
assertDoesNotRedirectTo(ROUTES.GET_STARTED_WITH_CLOUD);
assertStaysOnRoute(ROUTES.GET_STARTED);
});
it('should not redirect when on GET_STARTED and ONBOARDING_V3 feature flag is not present', () => {
@@ -1314,7 +1303,7 @@ describe('PrivateRoute', () => {
},
});
assertDoesNotRedirectTo(ROUTES.GET_STARTED_WITH_CLOUD);
assertStaysOnRoute(ROUTES.GET_STARTED);
});
it('should not redirect when on different route even if ONBOARDING_V3 is active', () => {
@@ -1334,7 +1323,7 @@ describe('PrivateRoute', () => {
},
});
assertDoesNotRedirectTo(ROUTES.GET_STARTED_WITH_CLOUD);
assertStaysOnRoute(ROUTES.HOME);
});
});
@@ -1350,7 +1339,7 @@ describe('PrivateRoute', () => {
},
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
assertStaysOnRoute(ROUTES.HOME);
});
it('should not fetch users when org data is not available', () => {
@@ -1393,9 +1382,7 @@ describe('PrivateRoute', () => {
},
});
assertDoesNotRedirectTo(ROUTES.WORKSPACE_LOCKED);
assertDoesNotRedirectTo(ROUTES.WORKSPACE_SUSPENDED);
assertDoesNotRedirectTo(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
assertStaysOnRoute(ROUTES.HOME);
});
});
@@ -1436,22 +1423,40 @@ describe('PrivateRoute', () => {
await assertRedirectsTo(ROUTES.UN_AUTHORIZED);
});
it('should allow all roles to access /services route', () => {
const roles = [USER_ROLES.ADMIN, USER_ROLES.EDITOR, USER_ROLES.VIEWER];
roles.forEach((role) => {
jest.clearAllMocks();
renderPrivateRoute({
initialRoute: ROUTES.APPLICATION,
appContext: {
isLoggedIn: true,
user: createMockUser({ role: role as ROLES }),
},
});
assertDoesNotRedirectTo(ROUTES.UN_AUTHORIZED);
it('should allow ADMIN to access /services route', () => {
renderPrivateRoute({
initialRoute: ROUTES.APPLICATION,
appContext: {
isLoggedIn: true,
user: createMockUser({ role: USER_ROLES.ADMIN as ROLES }),
},
});
assertStaysOnRoute(ROUTES.APPLICATION);
});
it('should allow EDITOR to access /services route', () => {
renderPrivateRoute({
initialRoute: ROUTES.APPLICATION,
appContext: {
isLoggedIn: true,
user: createMockUser({ role: USER_ROLES.EDITOR as ROLES }),
},
});
assertStaysOnRoute(ROUTES.APPLICATION);
});
it('should allow VIEWER to access /services route', () => {
renderPrivateRoute({
initialRoute: ROUTES.APPLICATION,
appContext: {
isLoggedIn: true,
user: createMockUser({ role: USER_ROLES.VIEWER as ROLES }),
},
});
assertStaysOnRoute(ROUTES.APPLICATION);
});
it('should redirect VIEWER from /onboarding route (admin only)', async () => {
@@ -1481,7 +1486,7 @@ describe('PrivateRoute', () => {
});
assertRendersChildren();
assertDoesNotRedirectTo(ROUTES.UN_AUTHORIZED);
assertStaysOnRoute(ROUTES.CHANNELS_NEW);
});
it('should allow EDITOR to access /get-started route', () => {
@@ -1493,7 +1498,7 @@ describe('PrivateRoute', () => {
},
});
assertDoesNotRedirectTo(ROUTES.UN_AUTHORIZED);
assertStaysOnRoute(ROUTES.GET_STARTED);
});
});

View File

@@ -32,7 +32,7 @@ func newConfig() factory.Config {
Domain: "signozserviceaccount.com",
},
Analytics: AnalyticsConfig{
Enabled: true,
Enabled: false,
},
}
}

View File

@@ -199,7 +199,7 @@ func (c *jsonConditionBuilder) terminalIndexedCondition(node *telemetrytypes.JSO
}
// buildPrimitiveTerminalCondition builds the condition if the terminal node is a primitive type
// it handles the data type collisions and utilizes indexes for the condition if available
// it handles the data type collisions and utilizes indexes for the condition if available.
func (c *jsonConditionBuilder) buildPrimitiveTerminalCondition(node *telemetrytypes.JSONAccessNode, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
fieldPath := node.FieldPath()
conditions := []string{}
@@ -300,7 +300,7 @@ func (c *jsonConditionBuilder) buildTerminalArrayCondition(node *telemetrytypes.
}
// buildArrayMembershipCondition builds condition of the part where Arrays becomes primitive typed Arrays
// e.g. [300, 404, 500], and value operations will work on the array elements
// e.g. [300, 404, 500], and value operations will work on the array elements.
func (c *jsonConditionBuilder) buildArrayMembershipCondition(node *telemetrytypes.JSONAccessNode, operator qbtypes.FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error) {
arrayPath := node.FieldPath()
// create typed array out of a dynamic array

View File

@@ -20,6 +20,7 @@ var (
ErrCodeAPIKeyAlreadyExists = errors.MustNewCode("api_key_already_exists")
ErrCodeAPIKeytNotFound = errors.MustNewCode("api_key_not_found")
ErrCodeAPIKeyExpired = errors.MustNewCode("api_key_expired")
errInvalidAPIKeyName = errors.New(errors.TypeInvalidInput, ErrCodeAPIKeyInvalidInput, "name must be 180 characters long and contain only lowercase letters (a-z) and hyphens (-)")
)
type FactorAPIKey struct {
@@ -112,7 +113,7 @@ func (key *PostableFactorAPIKey) UnmarshalJSON(data []byte) error {
}
if match := factorAPIKeyNameRegex.MatchString(temp.Name); !match {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAPIKeyInvalidInput, "name must conform to the regex: %s", factorAPIKeyNameRegex.String())
return errInvalidAPIKeyName
}
if temp.ExpiresAt != 0 && time.Now().After(time.Unix(int64(temp.ExpiresAt), 0)) {
@@ -132,7 +133,7 @@ func (key *UpdatableFactorAPIKey) UnmarshalJSON(data []byte) error {
}
if match := factorAPIKeyNameRegex.MatchString(temp.Name); !match {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAPIKeyInvalidInput, "name must conform to the regex: %s", factorAPIKeyNameRegex.String())
return errInvalidAPIKeyName
}
if temp.ExpiresAt != 0 && time.Now().After(time.Unix(int64(temp.ExpiresAt), 0)) {

View File

@@ -23,6 +23,7 @@ var (
ErrCodeServiceAccountNotFound = errors.MustNewCode("service_account_not_found")
ErrCodeServiceAccountRoleAlreadyExists = errors.MustNewCode("service_account_role_already_exists")
ErrCodeServiceAccountOperationUnsupported = errors.MustNewCode("service_account_operation_unsupported")
errInvalidServiceAccountName = errors.New(errors.TypeInvalidInput, ErrCodeServiceAccountInvalidInput, "name must be 150 characters long and contain only lowercase letters (a-z) and hyphens (-)")
)
var (
@@ -214,7 +215,7 @@ func (serviceAccount *PostableServiceAccount) UnmarshalJSON(data []byte) error {
}
if match := serviceAccountNameRegex.MatchString(temp.Name); !match {
return errors.Newf(errors.TypeInvalidInput, ErrCodeServiceAccountInvalidInput, "name must conform to the regex: %s", serviceAccountNameRegex.String())
return errInvalidServiceAccountName
}
*serviceAccount = PostableServiceAccount(temp)

View File

@@ -90,7 +90,7 @@ func (n *JSONAccessNode) FieldPath() string {
return n.Parent.Alias() + "." + key
}
// Returns true if the current node is a non-nested path
// Returns true if the current node is a non-nested path.
func (n *JSONAccessNode) IsNonNestedPath() bool {
return !strings.Contains(n.FieldPath(), ArraySep)
}

View File

@@ -22,7 +22,6 @@ pytest_plugins = [
"fixtures.notification_channel",
"fixtures.alerts",
"fixtures.cloudintegrations",
"fixtures.jsontypeexporter",
]

View File

@@ -1,421 +0,0 @@
"""
Simpler version of jsontypeexporter for test fixtures.
This exports JSON type metadata to the path_types table by parsing JSON bodies
and extracting all paths with their types, similar to how the real jsontypeexporter works.
"""
import datetime
import json
from abc import ABC
from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional, Set, Union
import numpy as np
import pytest
from fixtures import types
if TYPE_CHECKING:
from fixtures.logs import Logs
class JSONPathType(ABC):
"""Represents a JSON path with its type information"""
path: str
type: str
last_seen: np.uint64
def __init__(
self,
path: str,
type: str, # pylint: disable=redefined-builtin
last_seen: Optional[datetime.datetime] = None,
) -> None:
self.path = path
self.type = type
if last_seen is None:
last_seen = datetime.datetime.now()
self.last_seen = np.uint64(int(last_seen.timestamp() * 1e9))
def np_arr(self) -> np.array:
"""Return path type data as numpy array for database insertion"""
return np.array([self.path, self.type, self.last_seen])
# Constants matching jsontypeexporter
ARRAY_SEPARATOR = "[]." # Used in paths like "education[].name"
ARRAY_SUFFIX = "[]" # Used when traversing into array element objects
def _infer_array_type_from_type_strings(types: List[str]) -> Optional[str]:
"""
Infer array type from a list of pre-classified type strings.
Matches jsontypeexporter's inferArrayMask logic (v0.144.2+).
Type strings are: "JSON", "String", "Bool", "Float64", "Int64"
SuperTyping rules (matching Go inferArrayMask):
- JSON alone → Array(JSON)
- JSON + any primitive → Array(Dynamic)
- String alone → Array(Nullable(String)); String + other → Array(Dynamic)
- Float64 wins over Int64 and Bool
- Int64 wins over Bool
- Bool alone → Array(Nullable(Bool))
"""
if len(types) == 0:
return None
unique = set(types)
has_json = "JSON" in unique
# hasPrimitive mirrors Go: (hasJSON && len(unique) > 1) || (!hasJSON && len(unique) > 0)
has_primitive = (has_json and len(unique) > 1) or (not has_json and len(unique) > 0)
if has_json:
if not has_primitive:
return "Array(JSON)"
return "Array(Dynamic)"
# ---- Primitive Type Resolution (Float > Int > Bool) ----
if "String" in unique:
if len(unique) > 1:
return "Array(Dynamic)"
return "Array(Nullable(String))"
if "Float64" in unique:
return "Array(Nullable(Float64))"
if "Int64" in unique:
return "Array(Nullable(Int64))"
if "Bool" in unique:
return "Array(Nullable(Bool))"
return "Array(Dynamic)"
def _infer_array_type(elements: List[Any]) -> Optional[str]:
"""
Infer array type from raw Python list elements.
Classifies each element then delegates to _infer_array_type_from_type_strings.
"""
if len(elements) == 0:
return None
types = []
for elem in elements:
if elem is None:
continue
if isinstance(elem, dict):
types.append("JSON")
elif isinstance(elem, str):
types.append("String")
elif isinstance(elem, bool): # must be before int (bool is subclass of int)
types.append("Bool")
elif isinstance(elem, float):
types.append("Float64")
elif isinstance(elem, int):
types.append("Int64")
return _infer_array_type_from_type_strings(types)
def _python_type_to_clickhouse_type(value: Any) -> str:
"""
Convert Python type to ClickHouse JSON type string.
Maps Python types to ClickHouse JSON data types.
"""
if value is None:
return "String" # Default for null values
if isinstance(value, bool):
return "Bool"
elif isinstance(value, int):
return "Int64"
elif isinstance(value, float):
return "Float64"
elif isinstance(value, str):
return "String"
elif isinstance(value, list):
# Use the sophisticated array type inference
array_type = _infer_array_type(value)
return array_type if array_type else "Array(Dynamic)"
elif isinstance(value, dict):
return "JSON"
else:
return "String" # Default fallback
def _extract_json_paths(
obj: Any,
current_path: str = "",
path_types: Optional[Dict[str, Set[str]]] = None,
level: int = 0,
) -> Dict[str, Set[str]]:
"""
Recursively extract all paths and their types from a JSON object.
Matches jsontypeexporter's analyzePValue logic.
Args:
obj: The JSON object to traverse
current_path: Current path being built (e.g., "user.name")
path_types: Dictionary mapping paths to sets of types found
level: Current nesting level (for depth limiting)
Returns:
Dictionary mapping paths to sets of type strings
"""
if path_types is None:
path_types = {}
if obj is None:
if current_path:
if current_path not in path_types:
path_types[current_path] = set()
path_types[current_path].add("String") # Null defaults to String
return path_types
if isinstance(obj, dict):
# For objects, add the object itself and recurse into keys
if current_path:
if current_path not in path_types:
path_types[current_path] = set()
path_types[current_path].add("JSON")
for key, value in obj.items():
# Build the path for this key
if current_path:
new_path = f"{current_path}.{key}"
else:
new_path = key
# Recurse into the value
_extract_json_paths(value, new_path, path_types, level + 1)
elif isinstance(obj, list):
# Skip empty arrays
if len(obj) == 0:
return path_types
# Collect types from array elements (matching Go: types := make([]pcommon.ValueType, 0, s.Len()))
types = []
for item in obj:
if isinstance(item, dict):
# When traversing into array element objects, use ArraySuffix ([])
# This matches: prefix+ArraySuffix in the Go code
# Example: if current_path is "education", we use "education[]" to traverse into objects
array_prefix = current_path + ARRAY_SUFFIX if current_path else ""
for key, value in item.items():
if array_prefix:
# Use array separator: education[].name
array_path = f"{array_prefix}.{key}"
else:
array_path = key
# Recurse without increasing level (matching Go behavior)
_extract_json_paths(value, array_path, path_types, level)
types.append("JSON")
elif isinstance(item, list):
# Arrays inside arrays are not supported - skip the whole path
# Matching Go: e.logger.Error("arrays inside arrays are not supported!", ...); return nil
return path_types
elif isinstance(item, str):
types.append("String")
elif isinstance(item, bool):
types.append("Bool")
elif isinstance(item, float):
types.append("Float64")
elif isinstance(item, int):
types.append("Int64")
# Infer array type from collected types (matching Go: if mask := inferArrayMask(types); mask != 0)
if len(types) > 0:
array_type = _infer_array_type_from_type_strings(types)
if array_type and current_path:
if current_path not in path_types:
path_types[current_path] = set()
path_types[current_path].add(array_type)
else:
# Primitive value (string, number, bool)
if current_path:
if current_path not in path_types:
path_types[current_path] = set()
obj_type = _python_type_to_clickhouse_type(obj)
path_types[current_path].add(obj_type)
return path_types
def _parse_json_bodies_and_extract_paths(
json_bodies: List[str],
timestamp: Optional[datetime.datetime] = None,
) -> List[JSONPathType]:
"""
Parse JSON bodies and extract all paths with their types.
This mimics the behavior of jsontypeexporter.
Args:
json_bodies: List of JSON body strings to parse
timestamp: Timestamp to use for last_seen (defaults to now)
Returns:
List of JSONPathType objects with all discovered paths and types
"""
if timestamp is None:
timestamp = datetime.datetime.now()
# Aggregate all paths and their types across all JSON bodies
all_path_types: Dict[str, Set[str]] = {}
for json_body in json_bodies:
try:
parsed = json.loads(json_body)
_extract_json_paths(parsed, "", all_path_types, level=0)
except (json.JSONDecodeError, TypeError):
# Skip invalid JSON
continue
# Convert to list of JSONPathType objects
# Each path can have multiple types, so we create one JSONPathType per type
path_type_objects: List[JSONPathType] = []
for path, types_set in all_path_types.items():
for type_str in types_set:
path_type_objects.append(
JSONPathType(path=path, type=type_str, last_seen=timestamp)
)
return path_type_objects
@pytest.fixture(name="export_json_types", scope="function")
def export_json_types(
clickhouse: types.TestContainerClickhouse,
request: pytest.FixtureRequest, # To access migrator fixture
) -> Generator[Callable[[Union[List[JSONPathType], List[str], List[Any]]], None], Any, None]:
"""
Fixture for exporting JSON type metadata to the path_types table.
This is a simpler version of jsontypeexporter for test fixtures.
The function can accept:
1. List of JSONPathType objects (manual specification)
2. List of JSON body strings (auto-extract paths)
3. List of Logs objects (extract from body_json field)
Usage examples:
# Manual specification
export_json_types([
JSONPathType(path="user.name", type="String"),
JSONPathType(path="user.age", type="Int64"),
])
# Auto-extract from JSON strings
export_json_types([
'{"user": {"name": "alice", "age": 25}}',
'{"user": {"name": "bob", "age": 30}}',
])
# Auto-extract from Logs objects
export_json_types(logs_list)
"""
# Ensure migrator has run to create the table
try:
request.getfixturevalue("migrator")
except Exception:
# If migrator fixture is not available, that's okay - table might already exist
pass
def _export_json_types(
data: Union[List[JSONPathType], List[str], List[Any]], # List[Logs] but avoiding circular import
) -> None:
"""
Export JSON type metadata to signoz_metadata.distributed_json_path_types table.
This table stores path and type information for body JSON fields.
"""
path_types: List[JSONPathType] = []
if len(data) == 0:
return
# Determine input type and convert to JSONPathType list
first_item = data[0]
if isinstance(first_item, JSONPathType):
# Already JSONPathType objects
path_types = data # type: ignore
elif isinstance(first_item, str):
# List of JSON strings - parse and extract paths
path_types = _parse_json_bodies_and_extract_paths(data) # type: ignore
else:
# Assume it's a list of Logs objects - extract body_v2
json_bodies: List[str] = []
for log in data: # type: ignore
# Try to get body_v2 attribute
if hasattr(log, "body_v2") and log.body_v2:
json_bodies.append(log.body_v2)
elif hasattr(log, "body") and log.body:
# Fallback to body if body_v2 not available
try:
# Try to parse as JSON
json.loads(log.body)
json_bodies.append(log.body)
except (json.JSONDecodeError, TypeError):
pass
if json_bodies:
path_types = _parse_json_bodies_and_extract_paths(json_bodies)
if len(path_types) == 0:
return
clickhouse.conn.insert(
database="signoz_metadata",
table="distributed_json_path_types",
data=[path_type.np_arr() for path_type in path_types],
column_names=[
"path",
"type",
"last_seen",
],
)
yield _export_json_types
# Cleanup - truncate the local table after tests (following pattern from logs fixture)
clickhouse.conn.query(
f"TRUNCATE TABLE signoz_metadata.json_path_types ON CLUSTER '{clickhouse.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER']}' SYNC"
)
@pytest.fixture(name="export_promoted_paths", scope="function")
def export_promoted_paths(
clickhouse: types.TestContainerClickhouse,
request: pytest.FixtureRequest, # To access migrator fixture
) -> Generator[Callable[[List[str]], None], Any, None]:
"""
Fixture for exporting promoted JSON paths to the promoted paths table.
"""
# Ensure migrator has run to create the table
try:
request.getfixturevalue("migrator")
except Exception:
# If migrator fixture is not available, that's okay - table might already exist
pass
def _export_promoted_paths(paths: List[str]) -> None:
if len(paths) == 0:
return
now_ms = int(datetime.datetime.now().timestamp() * 1000)
rows = [(path, now_ms) for path in paths]
clickhouse.conn.insert(
database="signoz_metadata",
table="distributed_json_promoted_paths",
data=rows,
column_names=[
"path",
"created_at",
],
)
yield _export_promoted_paths
clickhouse.conn.query(
f"TRUNCATE TABLE signoz_metadata.json_promoted_paths ON CLUSTER '{clickhouse.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER']}' SYNC"
)

View File

@@ -122,8 +122,6 @@ class Logs(ABC):
resources: dict[str, Any] = {},
attributes: dict[str, Any] = {},
body: str = "default body",
body_v2: Optional[str] = None,
body_promoted: Optional[str] = None,
severity_text: str = "INFO",
trace_id: str = "",
span_id: str = "",
@@ -169,33 +167,6 @@ class Logs(ABC):
# Set body
self.body = body
# Set body_v2 - if body is JSON, parse and stringify it, otherwise use empty string
# ClickHouse accepts String input for JSON column
if body_v2 is not None:
self.body_v2 = body_v2
else:
# Try to parse body as JSON; if successful use it directly,
# otherwise wrap as {"message": body} matching the normalize operator behavior.
try:
json.loads(body)
self.body_v2 = body
except (json.JSONDecodeError, TypeError):
self.body_v2 = json.dumps({"message": body})
# Set body_promoted - must be valid JSON
# Tests will explicitly pass promoted column's content, but we validate it
if body_promoted is not None:
# Validate that it's valid JSON
try:
json.loads(body_promoted)
self.body_promoted = body_promoted
except (json.JSONDecodeError, TypeError):
# If invalid, default to empty JSON object
self.body_promoted = "{}"
else:
# Default to empty JSON object (valid JSON)
self.body_promoted = "{}"
# Process resources and attributes
self.resources_string = {k: str(v) for k, v in resources.items()}
self.resource_json = (
@@ -355,8 +326,6 @@ class Logs(ABC):
self.severity_text,
self.severity_number,
self.body,
self.body_v2,
self.body_promoted,
self.attributes_string,
self.attributes_number,
self.attributes_bool,
@@ -501,8 +470,6 @@ def insert_logs(
"severity_text",
"severity_number",
"body",
"body_v2",
"body_promoted",
"attributes_string",
"attributes_number",
"attributes_bool",

View File

@@ -1,5 +1,3 @@
from typing import Optional
import docker
import pytest
from testcontainers.core.container import Network
@@ -10,32 +8,27 @@ from fixtures.logger import setup_logger
logger = setup_logger(__name__)
def create_migrator(
@pytest.fixture(name="migrator", scope="package")
def migrator(
network: Network,
clickhouse: types.TestContainerClickhouse,
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
cache_key: str = "migrator",
env_overrides: Optional[dict] = None,
) -> types.Operation:
"""
Factory function for running schema migrations.
Accepts optional env_overrides to customize the migrator environment.
Package-scoped fixture for running schema migrations.
"""
def create() -> None:
version = request.config.getoption("--schema-migrator-version")
client = docker.from_env()
environment = dict(env_overrides) if env_overrides else {}
container = client.containers.run(
image=f"signoz/signoz-schema-migrator:{version}",
command=f"sync --replication=true --cluster-name=cluster --up= --dsn={clickhouse.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN"]}",
detach=True,
auto_remove=False,
network=network.id,
environment=environment,
)
result = container.wait()
@@ -54,7 +47,6 @@ def create_migrator(
detach=True,
auto_remove=False,
network=network.id,
environment=environment,
)
result = container.wait()
@@ -67,7 +59,7 @@ def create_migrator(
container.remove()
return types.Operation(name=cache_key)
return types.Operation(name="migrator")
def delete(_: types.Operation) -> None:
pass
@@ -78,27 +70,9 @@ def create_migrator(
return dev.wrap(
request,
pytestconfig,
cache_key,
"migrator",
lambda: types.Operation(name=""),
create,
delete,
restore,
)
@pytest.fixture(name="migrator", scope="package")
def migrator(
network: Network,
clickhouse: types.TestContainerClickhouse,
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.Operation:
"""
Package-scoped fixture for running schema migrations.
"""
return create_migrator(
network=network,
clickhouse=clickhouse,
request=request,
pytestconfig=pytestconfig,
)

View File

@@ -1,70 +0,0 @@
import pytest
from testcontainers.core.container import Network
from fixtures import types
from fixtures.migrator import create_migrator
from fixtures.signoz import create_signoz
UNSUPPORTED_CLICKHOUSE_VERSIONS = {"25.5.6"}
def pytest_collection_modifyitems(
config: pytest.Config, items: list[pytest.Item]
) -> None:
version = config.getoption("--clickhouse-version")
if version in UNSUPPORTED_CLICKHOUSE_VERSIONS:
skip = pytest.mark.skip(
reason=f"JSON body QB tests require ClickHouse > {version}"
)
for item in items:
item.add_marker(skip)
@pytest.fixture(name="migrator", scope="package")
def migrator_json(
network: Network,
clickhouse: types.TestContainerClickhouse,
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.Operation:
"""
Package-scoped migrator with ENABLE_LOGS_MIGRATIONS_V2=1.
"""
return create_migrator(
network=network,
clickhouse=clickhouse,
request=request,
pytestconfig=pytestconfig,
cache_key="migrator-json-body",
env_overrides={
"ENABLE_LOGS_MIGRATIONS_V2": "1",
},
)
@pytest.fixture(name="signoz", scope="package")
def signoz_json_body(
network: Network,
zeus: types.TestContainerDocker,
gateway: types.TestContainerDocker,
sqlstore: types.TestContainerSQL,
clickhouse: types.TestContainerClickhouse,
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.SigNoz:
"""
Package-scoped fixture for SigNoz with BODY_JSON_QUERY_ENABLED=true.
"""
return create_signoz(
network=network,
zeus=zeus,
gateway=gateway,
sqlstore=sqlstore,
clickhouse=clickhouse,
request=request,
pytestconfig=pytestconfig,
cache_key="signoz-json-body",
env_overrides={
"BODY_JSON_QUERY_ENABLED": "true",
},
)