mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-29 13:20:28 +01:00
Compare commits
6 Commits
refactor/c
...
web-settin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d51d03efd5 | ||
|
|
c6b2fe47d5 | ||
|
|
1d6fa6e507 | ||
|
|
910645516d | ||
|
|
edc1278769 | ||
|
|
da1b09c479 |
@@ -68,6 +68,12 @@ web:
|
||||
appcues:
|
||||
# Whether to enable Appcues in web.
|
||||
enabled: true
|
||||
sentry:
|
||||
# Whether to enable Sentry in web.
|
||||
enabled: true
|
||||
pylon:
|
||||
# Whether to enable Pylon in web.
|
||||
enabled: true
|
||||
|
||||
##################### Cache #####################
|
||||
cache:
|
||||
|
||||
@@ -3237,8 +3237,20 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/ErrorsResponseerroradditional'
|
||||
type: array
|
||||
invalidReferences:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
message:
|
||||
type: string
|
||||
retry:
|
||||
$ref: '#/components/schemas/ErrorsResponseretryjson'
|
||||
suggestions:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
@@ -3250,6 +3262,11 @@ components:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
ErrorsResponseretryjson:
|
||||
properties:
|
||||
delay:
|
||||
$ref: '#/components/schemas/TimeDuration'
|
||||
type: object
|
||||
FactoryResponse:
|
||||
properties:
|
||||
healthy:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"required": [
|
||||
"posthog",
|
||||
"appcues"
|
||||
"appcues",
|
||||
"sentry",
|
||||
"pylon"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
@@ -28,6 +30,30 @@
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Pylon": {
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Sentry": {
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@@ -36,6 +62,12 @@
|
||||
},
|
||||
"posthog": {
|
||||
"$ref": "#/definitions/Posthog"
|
||||
},
|
||||
"pylon": {
|
||||
"$ref": "#/definitions/Pylon"
|
||||
},
|
||||
"sentry": {
|
||||
"$ref": "#/definitions/Sentry"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -112,7 +112,10 @@
|
||||
|
||||
<script>
|
||||
var PYLON_APP_ID = '<%- PYLON_APP_ID %>';
|
||||
if (PYLON_APP_ID) {
|
||||
var pylonSettings =
|
||||
((window.signozBootData || {}).settings || {}).pylon || {};
|
||||
var pylonEnabled = pylonSettings.enabled !== false;
|
||||
if (PYLON_APP_ID && pylonEnabled) {
|
||||
(function () {
|
||||
var e = window;
|
||||
var t = document;
|
||||
|
||||
@@ -35,7 +35,6 @@ import { PreferenceContextProvider } from 'providers/preferences/context/Prefere
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { extractDomain } from 'utils/app';
|
||||
import { bootSettings } from 'utils/bootData';
|
||||
|
||||
import { Home } from './pageComponents';
|
||||
import PrivateRoute from './Private';
|
||||
@@ -292,7 +291,8 @@ function App(): JSX.Element {
|
||||
isLoggedInState &&
|
||||
isChatSupportEnabled &&
|
||||
!showAddCreditCardModal &&
|
||||
(isCloudUser || isEnterpriseSelfHostedUser)
|
||||
(isCloudUser || isEnterpriseSelfHostedUser) &&
|
||||
(window.signozBootData?.settings?.pylon.enabled ?? true)
|
||||
) {
|
||||
const email = user.email || '';
|
||||
const secret = process.env.PYLON_IDENTITY_SECRET || '';
|
||||
@@ -334,14 +334,20 @@ function App(): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
if (isCloudUser || isEnterpriseSelfHostedUser) {
|
||||
if (bootSettings.posthog.enabled && process.env.POSTHOG_KEY) {
|
||||
if (
|
||||
(window.signozBootData?.settings?.posthog.enabled ?? true) &&
|
||||
process.env.POSTHOG_KEY
|
||||
) {
|
||||
posthog.init(process.env.POSTHOG_KEY, {
|
||||
api_host: 'https://us.i.posthog.com',
|
||||
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
|
||||
});
|
||||
}
|
||||
|
||||
if (!isSentryInitialized) {
|
||||
if (
|
||||
!isSentryInitialized &&
|
||||
(window.signozBootData?.settings?.sentry.enabled ?? true)
|
||||
) {
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
tunnel: process.env.TUNNEL_URL,
|
||||
|
||||
@@ -2051,6 +2051,10 @@ export interface ErrorsResponseerroradditionalDTO {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ErrorsResponseretryjsonDTO {
|
||||
delay?: TimeDurationDTO;
|
||||
}
|
||||
|
||||
export interface ErrorsJSONDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -2060,10 +2064,23 @@ export interface ErrorsJSONDTO {
|
||||
* @type array
|
||||
*/
|
||||
errors?: ErrorsResponseerroradditionalDTO[];
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
invalidReferences?: string[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
message: string;
|
||||
retry?: ErrorsResponseretryjsonDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
suggestions?: string[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
12
frontend/src/components/RouteTab/RouteTab.styles.scss
Normal file
12
frontend/src/components/RouteTab/RouteTab.styles.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.route-tab-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.route-tab-extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -70,7 +70,7 @@ describe('RouteTab component', () => {
|
||||
</Router>,
|
||||
);
|
||||
expect(history.location.pathname).toBe('/');
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Tab2' }));
|
||||
fireEvent.mouseDown(screen.getByRole('tab', { name: 'Tab2' }));
|
||||
expect(history.location.pathname).toBe('/tab2');
|
||||
});
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('RouteTab component', () => {
|
||||
/>
|
||||
</Router>,
|
||||
);
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Tab2' }));
|
||||
fireEvent.mouseDown(screen.getByRole('tab', { name: 'Tab2' }));
|
||||
expect(onChangeHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import './RouteTab.styles.scss';
|
||||
|
||||
import {
|
||||
generatePath,
|
||||
matchPath,
|
||||
useLocation,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import {
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsRoot,
|
||||
TabsTrigger,
|
||||
} from '@signozhq/ui/tabs';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
|
||||
import { RouteTabProps } from './types';
|
||||
@@ -16,11 +23,13 @@ interface Params {
|
||||
function RouteTab({
|
||||
routes,
|
||||
activeKey,
|
||||
defaultActiveKey,
|
||||
onChangeHandler,
|
||||
history,
|
||||
showRightSection,
|
||||
...rest
|
||||
}: RouteTabProps & TabsProps): JSX.Element {
|
||||
showRightSection = true,
|
||||
tabBarExtraContent,
|
||||
hideTabBar = false,
|
||||
}: RouteTabProps): JSX.Element {
|
||||
const params = useParams<Params>();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -46,38 +55,38 @@ function RouteTab({
|
||||
}
|
||||
};
|
||||
|
||||
const items = routes.map(({ Component, name, route, key }) => ({
|
||||
label: name,
|
||||
key,
|
||||
tabKey: route,
|
||||
children: <Component />,
|
||||
}));
|
||||
const resolvedActiveKey = currentRoute?.key || activeKey;
|
||||
const extraContent =
|
||||
tabBarExtraContent ??
|
||||
(showRightSection && (
|
||||
<HeaderRightSection enableAnnouncements={false} enableShare enableFeedback />
|
||||
));
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
onChange={onChange}
|
||||
destroyInactiveTabPane
|
||||
activeKey={currentRoute?.key || activeKey}
|
||||
defaultActiveKey={currentRoute?.key || activeKey}
|
||||
animated
|
||||
items={items}
|
||||
tabBarExtraContent={
|
||||
showRightSection && (
|
||||
<HeaderRightSection
|
||||
enableAnnouncements={false}
|
||||
enableShare
|
||||
enableFeedback
|
||||
/>
|
||||
)
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
<TabsRoot
|
||||
value={resolvedActiveKey}
|
||||
defaultValue={defaultActiveKey ?? resolvedActiveKey}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
{!hideTabBar && (
|
||||
<div className="route-tab-header">
|
||||
<TabsList>
|
||||
{routes.map(({ name, key }) => (
|
||||
<TabsTrigger key={key} value={key}>
|
||||
{name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{extraContent && <div className="route-tab-extra">{extraContent}</div>}
|
||||
</div>
|
||||
)}
|
||||
{routes.map(({ key, Component }) => (
|
||||
<TabsContent key={key} value={key}>
|
||||
<Component />
|
||||
</TabsContent>
|
||||
))}
|
||||
</TabsRoot>
|
||||
);
|
||||
}
|
||||
|
||||
RouteTab.defaultProps = {
|
||||
onChangeHandler: undefined,
|
||||
showRightSection: true,
|
||||
};
|
||||
|
||||
export default RouteTab;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TabsProps } from 'antd';
|
||||
import { History } from 'history';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export type TabRoutes = {
|
||||
name: React.ReactNode;
|
||||
@@ -10,8 +10,11 @@ export type TabRoutes = {
|
||||
|
||||
export interface RouteTabProps {
|
||||
routes: TabRoutes[];
|
||||
activeKey: TabsProps['activeKey'];
|
||||
activeKey: string | undefined;
|
||||
defaultActiveKey?: string;
|
||||
onChangeHandler?: (key: string) => void;
|
||||
history: History<unknown>;
|
||||
showRightSection: boolean;
|
||||
showRightSection?: boolean;
|
||||
tabBarExtraContent?: ReactNode;
|
||||
hideTabBar?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
.settings-tabs {
|
||||
.ant-tabs-nav-list {
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
transition: opacity 0.1s !important;
|
||||
|
||||
.ant-tabs-tab + .ant-tabs-tab {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.ant-tabs-tab:not(:last-child) {
|
||||
border-right: 1px solid var(--l1-border) !important;
|
||||
}
|
||||
|
||||
.overview-btn {
|
||||
width: 114px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.variables-btn {
|
||||
width: 114px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.public-dashboard-btn {
|
||||
width: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.disabled-btn {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-tabs-tab-active {
|
||||
.overview-btn {
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
.variables-btn {
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
.public-dashboard-btn {
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
background: var(--l1-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-nav::before {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Tabs, Tooltip } from 'antd';
|
||||
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { Braces, Globe, Table } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -9,8 +9,6 @@ import DashboardVariableSettings from './DashboardVariableSettings';
|
||||
import GeneralDashboardSettings from './General';
|
||||
import PublicDashboardSetting from './PublicDashboard';
|
||||
|
||||
import './DashboardSettingsContent.styles.scss';
|
||||
|
||||
function DashboardSettings({
|
||||
variablesSettingsTabHandle,
|
||||
}: {
|
||||
@@ -21,49 +19,26 @@ function DashboardSettings({
|
||||
|
||||
const enablePublicDashboard = isCloudUser || isEnterpriseSelfHostedUser;
|
||||
|
||||
const publicDashboardItem = {
|
||||
label: (
|
||||
<Tooltip
|
||||
title={
|
||||
user?.role !== USER_ROLES.ADMIN
|
||||
? 'Only admins can publish / manage public dashboards'
|
||||
: ''
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Globe size={14} />}
|
||||
className={`public-dashboard-btn ${
|
||||
user?.role !== USER_ROLES.ADMIN ? 'disabled-btn' : ''
|
||||
}`}
|
||||
>
|
||||
Publish
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
const publicDashboardItem: TabItemProps = {
|
||||
key: 'public-dashboard',
|
||||
label: 'Publish',
|
||||
prefixIcon: <Globe size={14} />,
|
||||
children: <PublicDashboardSetting />,
|
||||
disabled: user?.role !== USER_ROLES.ADMIN,
|
||||
disabledReason: 'Only admins can publish / manage public dashboards',
|
||||
};
|
||||
|
||||
const items = [
|
||||
const items: TabItemProps[] = [
|
||||
{
|
||||
label: (
|
||||
<Button type="text" icon={<Table size={14} />} className="overview-btn">
|
||||
Overview
|
||||
</Button>
|
||||
),
|
||||
key: 'general',
|
||||
label: 'Overview',
|
||||
prefixIcon: <Table size={14} />,
|
||||
children: <GeneralDashboardSettings />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Button type="text" icon={<Braces size={14} />} className="variables-btn">
|
||||
Variables
|
||||
</Button>
|
||||
),
|
||||
key: 'variables',
|
||||
label: 'Variables',
|
||||
prefixIcon: <Braces size={14} />,
|
||||
children: (
|
||||
<DashboardVariableSettings
|
||||
variablesSettingsTabHandle={variablesSettingsTabHandle}
|
||||
@@ -73,7 +48,7 @@ function DashboardSettings({
|
||||
...(enablePublicDashboard ? [publicDashboardItem] : []),
|
||||
];
|
||||
|
||||
return <Tabs items={items} animated className="settings-tabs" />;
|
||||
return <Tabs items={items} />;
|
||||
}
|
||||
|
||||
export default DashboardSettings;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Button, Tabs, TabsProps } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import { CableCar, Group } from '@signozhq/icons';
|
||||
import { IntegrationDetailedProps } from 'types/api/integrations/types';
|
||||
@@ -22,18 +21,11 @@ function IntegrationDetailContent(
|
||||
): JSX.Element {
|
||||
const { activeDetailTab, integrationData, integrationId, setActiveDetailTab } =
|
||||
props;
|
||||
const items: TabsProps['items'] = [
|
||||
const items: TabItemProps[] = [
|
||||
{
|
||||
key: 'overview',
|
||||
label: (
|
||||
<Button
|
||||
type="text"
|
||||
className="integration-tab-btns"
|
||||
icon={<CableCar size={14} />}
|
||||
>
|
||||
<Typography.Text className="typography">Overview</Typography.Text>
|
||||
</Button>
|
||||
),
|
||||
label: 'Overview',
|
||||
prefixIcon: <CableCar size={14} />,
|
||||
children: (
|
||||
<Overview
|
||||
categories={integrationData.categories}
|
||||
@@ -44,15 +36,8 @@ function IntegrationDetailContent(
|
||||
},
|
||||
{
|
||||
key: 'configuration',
|
||||
label: (
|
||||
<Button
|
||||
type="text"
|
||||
className="integration-tab-btns"
|
||||
icon={<ConfigureIcon />}
|
||||
>
|
||||
<Typography.Text className="typography">Configure</Typography.Text>
|
||||
</Button>
|
||||
),
|
||||
label: 'Configure',
|
||||
prefixIcon: <ConfigureIcon />,
|
||||
children: (
|
||||
<Configure
|
||||
configuration={integrationData.configuration}
|
||||
@@ -62,15 +47,8 @@ function IntegrationDetailContent(
|
||||
},
|
||||
{
|
||||
key: 'dataCollected',
|
||||
label: (
|
||||
<Button
|
||||
type="text"
|
||||
className="integration-tab-btns"
|
||||
icon={<Group size={14} />}
|
||||
>
|
||||
<Typography.Text className="typography">Data Collected</Typography.Text>
|
||||
</Button>
|
||||
),
|
||||
label: 'Data Collected',
|
||||
prefixIcon: <Group size={14} />,
|
||||
children: (
|
||||
<DataCollected
|
||||
logsData={integrationData.data_collected.logs}
|
||||
@@ -81,11 +59,7 @@ function IntegrationDetailContent(
|
||||
];
|
||||
return (
|
||||
<div className="integration-detail-container">
|
||||
<Tabs
|
||||
activeKey={activeDetailTab}
|
||||
items={items}
|
||||
onChange={setActiveDetailTab}
|
||||
/>
|
||||
<Tabs value={activeDetailTab} items={items} onChange={setActiveDetailTab} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,45 +168,6 @@
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.integration-tab-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 8px 18px 8px !important;
|
||||
|
||||
.typography {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
.integration-tab-btns:hover {
|
||||
&.ant-btn-text {
|
||||
background-color: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-nav-list {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-tabs-tab + .ant-tabs-tab {
|
||||
margin: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.uninstall-integration-bar {
|
||||
|
||||
@@ -1,26 +1,3 @@
|
||||
.service-route-tab {
|
||||
margin-bottom: 64px;
|
||||
.ant-tabs-nav {
|
||||
&::before {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
.ant-tabs-nav-wrap {
|
||||
.ant-tabs-nav-list {
|
||||
.ant-tabs-ink-bar {
|
||||
background-color: var(--primary-background) !important;
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
font-size: 13px;
|
||||
font-family: 'Inter';
|
||||
color: var(--l1-foreground);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
gap: 10px;
|
||||
}
|
||||
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import DBCall from 'container/MetricsApplication/Tabs/DBCall';
|
||||
import External from 'container/MetricsApplication/Tabs/External';
|
||||
@@ -24,7 +24,7 @@ function MetricsApplication(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
const items: TabItemProps[] = [
|
||||
{
|
||||
label: TAB_KEY_VS_LABEL[MetricsApplicationTab.OVER_METRICS],
|
||||
key: MetricsApplicationTab.OVER_METRICS,
|
||||
@@ -53,9 +53,8 @@ function MetricsApplication(): JSX.Element {
|
||||
<ApDexApplication />
|
||||
<Tabs
|
||||
items={items}
|
||||
activeKey={activeKey}
|
||||
value={activeKey}
|
||||
className="service-route-tab"
|
||||
destroyInactiveTabPane
|
||||
onChange={onTabChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.pipeline-tabs {
|
||||
.ant-tabs-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@ import { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import type { TabsProps } from 'antd';
|
||||
import { Tabs } from 'antd';
|
||||
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
|
||||
import getPipeline from 'api/pipeline/get';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ChangeHistory from 'container/PipelinePage/Layouts/ChangeHistory';
|
||||
@@ -13,8 +12,6 @@ import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFall
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Pipeline } from 'types/api/pipeline/def';
|
||||
|
||||
import './Pipelines.styles.scss';
|
||||
|
||||
const pipelineRefetchInterval = (
|
||||
pipelineResponse: SuccessResponse<Pipeline> | undefined,
|
||||
): number | false => {
|
||||
@@ -46,7 +43,7 @@ function Pipelines(): JSX.Element {
|
||||
refetchInterval: pipelineRefetchInterval,
|
||||
});
|
||||
|
||||
const tabItems: TabsProps['items'] = useMemo(
|
||||
const tabItems: TabItemProps[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'pipelines',
|
||||
@@ -83,11 +80,7 @@ function Pipelines(): JSX.Element {
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<Tabs
|
||||
className="pipeline-tabs"
|
||||
defaultActiveKey="pipelines"
|
||||
items={tabItems}
|
||||
/>
|
||||
<Tabs defaultValue="pipelines" items={tabItems} />
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ function SettingsPage(): JSX.Element {
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
tabBarStyle={{ display: 'none' }}
|
||||
hideTabBar
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,88 +71,6 @@
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
padding-top: 16px;
|
||||
|
||||
.flamegraph-waterfall-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 31px;
|
||||
color: var(--l2-foreground);
|
||||
padding: 5px 20px;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.span-list-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 31px;
|
||||
padding: 5px 20px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
.ant-btn-icon {
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-visualisation-tabs {
|
||||
.ant-tabs-tab {
|
||||
border-radius: 2px 0px 0px 0px;
|
||||
background: var(--l2-background);
|
||||
border-radius: 2px 2px 0px 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.ant-tabs-tab-active {
|
||||
background-color: var(--l1-background);
|
||||
|
||||
.ant-btn {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab + .ant-tabs-tab {
|
||||
margin: 0px;
|
||||
border-left: 0px;
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
height: 1px !important;
|
||||
background: var(--l1-background) !important;
|
||||
}
|
||||
|
||||
.ant-tabs-nav-list {
|
||||
transform: translate(15px, 0px) !important;
|
||||
}
|
||||
|
||||
.ant-tabs-nav::before {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin: 0px;
|
||||
padding: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from '@signozhq/resizable';
|
||||
import { Button, Tabs } from 'antd';
|
||||
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
|
||||
import FlamegraphImg from 'assets/TraceDetail/Flamegraph';
|
||||
import cx from 'classnames';
|
||||
import TraceFlamegraph from 'container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph';
|
||||
@@ -86,18 +86,11 @@ function TraceDetailsV2(): JSX.Element {
|
||||
}
|
||||
}, [noData]);
|
||||
|
||||
const items = [
|
||||
const items: TabItemProps[] = [
|
||||
{
|
||||
label: (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FlamegraphImg />}
|
||||
className="flamegraph-waterfall-toggle"
|
||||
>
|
||||
Flamegraph
|
||||
</Button>
|
||||
),
|
||||
key: 'flamegraph',
|
||||
label: 'Flamegraph',
|
||||
prefixIcon: <FlamegraphImg />,
|
||||
children: (
|
||||
<>
|
||||
<TraceFlamegraph
|
||||
@@ -145,11 +138,7 @@ function TraceDetailsV2(): JSX.Element {
|
||||
totalSpans={traceData?.payload?.totalSpansCount || 0}
|
||||
notFound={noData}
|
||||
/>
|
||||
{!noData ? (
|
||||
<Tabs items={items} animated className="trace-visualisation-tabs" />
|
||||
) : (
|
||||
<NoData />
|
||||
)}
|
||||
{!noData ? <Tabs items={items} /> : <NoData />}
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle withHandle className="resizable-handle" />
|
||||
|
||||
@@ -22,21 +22,6 @@ $dark-theme: 'darkMode';
|
||||
|
||||
&__tabs {
|
||||
margin-top: 148px;
|
||||
|
||||
.ant-tabs {
|
||||
&-nav {
|
||||
&::before {
|
||||
border-color: var(--l1-border);
|
||||
|
||||
.#{$light-theme} & {
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
&-nav-wrap {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__modal {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from 'react-query';
|
||||
import type { TabsProps } from 'antd';
|
||||
import type { TabItemProps } from '@signozhq/ui/tabs';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
Row,
|
||||
Skeleton,
|
||||
Space,
|
||||
Tabs,
|
||||
} from 'antd';
|
||||
import { Tabs } from '@signozhq/ui/tabs';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import updateCreditCardApi from 'api/v1/checkout/create';
|
||||
@@ -154,7 +154,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
/>
|
||||
));
|
||||
|
||||
const tabItems: TabsProps['items'] = [
|
||||
const tabItems: TabItemProps[] = [
|
||||
{
|
||||
key: 'whyChooseSignoz',
|
||||
label: t('whyChooseSignoz'),
|
||||
@@ -398,8 +398,8 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
<div className="workspace-locked__tabs">
|
||||
<Tabs
|
||||
items={tabItems}
|
||||
defaultActiveKey="youAreInGoodCompany"
|
||||
onTabClick={handleTabClick}
|
||||
defaultValue="youAreInGoodCompany"
|
||||
onChange={handleTabClick}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -825,5 +825,6 @@ body.ai-assistant-panel-open {
|
||||
// overrides
|
||||
:root {
|
||||
--input-focus-outline-width: 0;
|
||||
--tab-list-primary-gap: 12px;
|
||||
--radius-2: 4px;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
export interface WebSettings {
|
||||
appcues: Appcues;
|
||||
posthog: Posthog;
|
||||
pylon: Pylon;
|
||||
sentry: Sentry;
|
||||
}
|
||||
export interface Appcues {
|
||||
enabled: boolean;
|
||||
@@ -10,3 +12,9 @@ export interface Appcues {
|
||||
export interface Posthog {
|
||||
enabled: boolean;
|
||||
}
|
||||
export interface Pylon {
|
||||
enabled: boolean;
|
||||
}
|
||||
export interface Sentry {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
export {};
|
||||
|
||||
type BootData = typeof import('../bootData');
|
||||
|
||||
function loadModule(settings?: object | null): BootData {
|
||||
(window as any).signozBootData =
|
||||
settings !== undefined ? { settings } : undefined;
|
||||
let mod!: BootData;
|
||||
jest.isolateModules(() => {
|
||||
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
|
||||
mod = require('../bootData');
|
||||
});
|
||||
return mod;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
delete (window as any).signozBootData;
|
||||
});
|
||||
|
||||
describe('when window.signozBootData is absent', () => {
|
||||
it('defaults posthog and appcues to enabled', () => {
|
||||
const { bootSettings } = loadModule();
|
||||
expect(bootSettings.posthog.enabled).toBe(true);
|
||||
expect(bootSettings.appcues.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when window.signozBootData.settings is null (injection failed)', () => {
|
||||
it('defaults posthog and appcues to enabled', () => {
|
||||
const { bootSettings } = loadModule(null);
|
||||
expect(bootSettings.posthog.enabled).toBe(true);
|
||||
expect(bootSettings.appcues.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when window.signozBootData.settings is populated', () => {
|
||||
it('reads posthog enabled: true', () => {
|
||||
const { bootSettings } = loadModule({ posthog: { enabled: true } });
|
||||
expect(bootSettings.posthog.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('reads posthog enabled: false', () => {
|
||||
const { bootSettings } = loadModule({ posthog: { enabled: false } });
|
||||
expect(bootSettings.posthog.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('reads appcues enabled: true', () => {
|
||||
const { bootSettings } = loadModule({ appcues: { enabled: true } });
|
||||
expect(bootSettings.appcues.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('reads appcues enabled: false', () => {
|
||||
const { bootSettings } = loadModule({ appcues: { enabled: false } });
|
||||
expect(bootSettings.appcues.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('missing sub-namespace defaults to enabled', () => {
|
||||
const { bootSettings } = loadModule({ posthog: { enabled: false } });
|
||||
expect(bootSettings.appcues.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when window.signozBootData exists but settings is undefined', () => {
|
||||
it('defaults posthog and appcues to enabled', () => {
|
||||
(window as any).signozBootData = {};
|
||||
let mod!: BootData;
|
||||
jest.isolateModules(() => {
|
||||
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
|
||||
mod = require('../bootData');
|
||||
});
|
||||
expect(mod.bootSettings.posthog.enabled).toBe(true);
|
||||
expect(mod.bootSettings.appcues.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { WebSettings } from 'types/generated/webSettings';
|
||||
|
||||
const raw = window.signozBootData?.settings as
|
||||
| Partial<WebSettings>
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export const bootSettings: Readonly<WebSettings> = {
|
||||
posthog: { enabled: raw?.posthog?.enabled ?? true },
|
||||
appcues: { enabled: raw?.appcues?.enabled ?? true },
|
||||
};
|
||||
@@ -31,6 +31,8 @@ function devBootDataPlugin(env: Record<string, string>): Plugin {
|
||||
const settings = {
|
||||
posthog: { enabled: env.VITE_POSTHOG_ENABLED !== 'false' },
|
||||
appcues: { enabled: env.VITE_APPCUES_ENABLED !== 'false' },
|
||||
sentry: { enabled: env.VITE_SENTRY_ENABLED !== 'false' },
|
||||
pylon: { enabled: env.VITE_PYLON_ENABLED !== 'false' },
|
||||
};
|
||||
return html.replaceAll('[[.Settings]]', JSON.stringify(settings));
|
||||
},
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"errors" //nolint:depguard
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
// base is the fundamental struct that implements the error interface.
|
||||
// The order of the struct is 'TCMEUAS'.
|
||||
type base struct {
|
||||
// t denotes the custom type of the error.
|
||||
t typ
|
||||
@@ -25,6 +25,14 @@ type base struct {
|
||||
a []string
|
||||
// s contains the stacktrace captured at error creation time.
|
||||
s fmt.Stringer
|
||||
// r is the retry strategy for the error, if applicable.
|
||||
r *retry
|
||||
// suggestions is a list of user-facing suggestions related to the error, if present.
|
||||
// For example, narrow the time range window or typo suggestion
|
||||
suggestions []string
|
||||
// invalidReferences is a list of references that were invalid and contributed to the error, if present.
|
||||
// For example, a typo from user avg(sum), we return invalidRefences: ['sum']
|
||||
invalidReferences []string
|
||||
}
|
||||
|
||||
// Stacktrace returns the stacktrace captured at error creation time, formatted as a string.
|
||||
@@ -39,13 +47,16 @@ func (b *base) Stacktrace() string {
|
||||
// and returns a new base error.
|
||||
func (b *base) WithStacktrace(s string) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: b.a,
|
||||
s: rawStacktrace(s),
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: b.a,
|
||||
s: rawStacktrace(s),
|
||||
r: b.r,
|
||||
suggestions: b.suggestions,
|
||||
invalidReferences: b.invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,13 +124,16 @@ func WithAdditionalf(cause error, format string, args ...any) *base {
|
||||
s = original.s
|
||||
}
|
||||
b := &base{
|
||||
t: t,
|
||||
c: c,
|
||||
m: m,
|
||||
e: e,
|
||||
u: u,
|
||||
a: a,
|
||||
s: s,
|
||||
t: t,
|
||||
c: c,
|
||||
m: m,
|
||||
e: e,
|
||||
u: u,
|
||||
a: a,
|
||||
s: s,
|
||||
r: retryOf(cause),
|
||||
suggestions: suggestionsOf(cause),
|
||||
invalidReferences: invalidReferencesOf(cause),
|
||||
}
|
||||
|
||||
return b.WithAdditional(append(a, fmt.Sprintf(format, args...))...)
|
||||
@@ -128,29 +142,88 @@ func WithAdditionalf(cause error, format string, args ...any) *base {
|
||||
// WithUrl adds a url to the base error and returns a new base error.
|
||||
func (b *base) WithUrl(u string) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: u,
|
||||
a: b.a,
|
||||
s: b.s,
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: u,
|
||||
a: b.a,
|
||||
s: b.s,
|
||||
r: b.r,
|
||||
suggestions: b.suggestions,
|
||||
invalidReferences: b.invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdditional adds additional messages to the base error and returns a new base error.
|
||||
func (b *base) WithAdditional(a ...string) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: a,
|
||||
s: b.s,
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: a,
|
||||
s: b.s,
|
||||
r: b.r,
|
||||
suggestions: b.suggestions,
|
||||
invalidReferences: b.invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
// withRetry adds retry metadata to the base error and returns a new base error.
|
||||
func (b *base) withRetry(r retry) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: b.a,
|
||||
s: b.s,
|
||||
r: &r,
|
||||
suggestions: b.suggestions,
|
||||
invalidReferences: b.invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
// WithSuggestions replaces the list of suggestions on the base error.
|
||||
func (b *base) WithSuggestions(suggestions ...string) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: b.a,
|
||||
s: b.s,
|
||||
r: b.r,
|
||||
suggestions: suggestions,
|
||||
invalidReferences: b.invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
// WithInvalidReferences replaces the list of invalid references on the base error.
|
||||
func (b *base) WithInvalidReferences(invalidReferences ...string) *base {
|
||||
return &base{
|
||||
t: b.t,
|
||||
c: b.c,
|
||||
m: b.m,
|
||||
e: b.e,
|
||||
u: b.u,
|
||||
a: b.a,
|
||||
s: b.s,
|
||||
r: b.r,
|
||||
suggestions: b.suggestions,
|
||||
invalidReferences: invalidReferences,
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetryAfter sets the retry delay on the base error and returns a new base error.
|
||||
func (b *base) WithRetryAfter(delay time.Duration) *base {
|
||||
return b.withRetry(newRetryAfter(delay))
|
||||
}
|
||||
|
||||
// Unwrapb is a combination of built-in errors.As and type casting.
|
||||
// It finds the first error in cause that matches base,
|
||||
// and if one is found, returns the individual fields of base.
|
||||
@@ -226,16 +299,6 @@ func NewInvalidInputf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeInvalidInput, code, format, args...)
|
||||
}
|
||||
|
||||
// WrapUnexpectedf is a wrapper around Wrapf with TypeUnexpected.
|
||||
func WrapUnexpectedf(cause error, code Code, format string, args ...any) *base {
|
||||
return Wrapf(cause, TypeInvalidInput, code, format, args...)
|
||||
}
|
||||
|
||||
// NewUnexpectedf is a wrapper around Newf with TypeUnexpected.
|
||||
func NewUnexpectedf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeInvalidInput, code, format, args...)
|
||||
}
|
||||
|
||||
// NewMethodNotAllowedf is a wrapper around Newf with TypeMethodNotAllowed.
|
||||
func NewMethodNotAllowedf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeMethodNotAllowed, code, format, args...)
|
||||
@@ -251,6 +314,26 @@ func NewTimeoutf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeTimeout, code, format, args...)
|
||||
}
|
||||
|
||||
// WrapUnauthenticatedf is a wrapper around Wrapf with TypeUnauthenticated.
|
||||
func WrapUnauthenticatedf(cause error, code Code, format string, args ...any) *base {
|
||||
return Wrapf(cause, TypeUnauthenticated, code, format, args...)
|
||||
}
|
||||
|
||||
// NewUnauthenticatedf is a wrapper around Newf with TypeUnauthenticated.
|
||||
func NewUnauthenticatedf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeUnauthenticated, code, format, args...)
|
||||
}
|
||||
|
||||
// WrapForbiddenf is a wrapper around Wrapf with TypeForbidden.
|
||||
func WrapForbiddenf(cause error, code Code, format string, args ...any) *base {
|
||||
return Wrapf(cause, TypeForbidden, code, format, args...)
|
||||
}
|
||||
|
||||
// NewForbiddenf is a wrapper around Newf with TypeForbidden.
|
||||
func NewForbiddenf(code Code, format string, args ...any) *base {
|
||||
return Newf(TypeForbidden, code, format, args...)
|
||||
}
|
||||
|
||||
// Attr returns an slog.Attr with a standardized "exception" key for the given error.
|
||||
func Attr(err error) slog.Attr {
|
||||
return slog.Any("exception", err)
|
||||
@@ -262,3 +345,37 @@ func TypeAttr(err error) attribute.KeyValue {
|
||||
t, _, _, _, _, _ := Unwrapb(err)
|
||||
return attribute.String("error.type", t.String())
|
||||
}
|
||||
|
||||
// RetryDelayOf returns the explicit retry delay set via WithRetryAfter,
|
||||
// or zero if the error carries no retry delay.
|
||||
func RetryDelayOf(err error) time.Duration {
|
||||
base, ok := err.(*base)
|
||||
if !ok || base.r == nil {
|
||||
return 0
|
||||
}
|
||||
return base.r.delay
|
||||
}
|
||||
|
||||
func retryOf(err error) *retry {
|
||||
base, ok := err.(*base)
|
||||
if ok {
|
||||
return base.r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func suggestionsOf(err error) []string {
|
||||
base, ok := err.(*base)
|
||||
if ok {
|
||||
return base.suggestions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func invalidReferencesOf(err error) []string {
|
||||
base, ok := err.(*base)
|
||||
if ok {
|
||||
return base.invalidReferences
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package errors
|
||||
import (
|
||||
"errors" //nolint:depguard
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
@@ -59,6 +61,95 @@ func TestAttr(t *testing.T) {
|
||||
assert.Equal(t, err, attr.Value.Any())
|
||||
}
|
||||
|
||||
func TestWithSuggestions(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithSuggestions("try this")
|
||||
assert.Equal(t, []string{"try this"}, suggestionsOf(err))
|
||||
|
||||
// WithSuggestions replaces the existing list.
|
||||
err = err.WithSuggestions("try this instead")
|
||||
assert.Equal(t, []string{"try this instead"}, suggestionsOf(err))
|
||||
|
||||
// Variadic form replaces with multiple entries.
|
||||
err = err.WithSuggestions("first", "second")
|
||||
assert.Equal(t, []string{"first", "second"}, suggestionsOf(err))
|
||||
}
|
||||
|
||||
func TestWithRetryAfter(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryAfter(5 * time.Microsecond)
|
||||
r := retryOf(err)
|
||||
|
||||
assert.Equal(t, 5, int(r.delay.Microseconds()))
|
||||
}
|
||||
|
||||
func TestWithInvalidReferences(t *testing.T) {
|
||||
// WithInvalidReferences populates the list.
|
||||
err := New(TypeInvalidInput, MustNewCode("bad_ref"), "bad ref").
|
||||
WithInvalidReferences("queries[0]", "queries[1]")
|
||||
assert.Equal(t, []string{"queries[0]", "queries[1]"}, invalidReferencesOf(err))
|
||||
|
||||
// WithInvalidReferences replaces the entire list on each call.
|
||||
err = err.WithInvalidReferences("queries[2]")
|
||||
assert.Equal(t, []string{"queries[2]"}, invalidReferencesOf(err),
|
||||
"WithInvalidReferences must replace the entire list")
|
||||
}
|
||||
|
||||
func TestAsJSONBaseError(t *testing.T) {
|
||||
err := New(TypeInvalidInput, MustNewCode("bad_input"), "field foo is bad").
|
||||
WithUrl("https://docs/bad_input").
|
||||
WithAdditional("hint1", "hint2").
|
||||
WithSuggestions("try this").
|
||||
WithInvalidReferences("queries[0]")
|
||||
|
||||
j := AsJSON(err)
|
||||
|
||||
assert.Equal(t, "invalid-input", j.Type)
|
||||
assert.Equal(t, "bad_input", j.Code)
|
||||
assert.Equal(t, "field foo is bad", j.Message)
|
||||
assert.Equal(t, "https://docs/bad_input", j.Url)
|
||||
assert.Equal(t, []responseerroradditional{{Message: "hint1"}, {Message: "hint2"}}, j.Errors)
|
||||
|
||||
// InvalidInput auto-applies the after_fix policy via NewInvalidInputf — but
|
||||
// New (bare constructor) does not. The retry block should reflect that.
|
||||
assert.Nil(t, j.Retry, "bare New(...) should not populate a retry block")
|
||||
|
||||
assert.Equal(t, []string{"try this"}, j.Suggestions)
|
||||
assert.Equal(t, []string{"queries[0]"}, j.InvalidReferences)
|
||||
}
|
||||
|
||||
func TestAsJSONRetryBlock(t *testing.T) {
|
||||
t.Run("RetryAfterIncludesDuration", func(t *testing.T) {
|
||||
err := NewTimeoutf(MustNewCode("slow"), "slow").WithRetryAfter(5 * time.Second)
|
||||
j := AsJSON(err)
|
||||
require.NotNil(t, j.Retry)
|
||||
assert.Equal(t, 5*time.Second, j.Retry.Delay)
|
||||
})
|
||||
|
||||
t.Run("NonAfterPolicyOmitsDurationField", func(t *testing.T) {
|
||||
// NewInvalidInputf auto-applies retryAfterFix via the constructor helper.
|
||||
err := NewInvalidInputf(MustNewCode("bad"), "bad")
|
||||
j := AsJSON(err)
|
||||
require.Nil(t, j.Retry, "retry must be empty")
|
||||
})
|
||||
|
||||
t.Run("BareErrorOmitsRetryBlock", func(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("boom"), "boom")
|
||||
j := AsJSON(err)
|
||||
assert.Nil(t, j.Retry, "bare New(...) without WithRetry* must omit retry")
|
||||
})
|
||||
|
||||
t.Run("NonBaseErrorOmitsRetryBlock", func(t *testing.T) {
|
||||
// Stdlib errors carry no retry metadata; AsJSON omits the retry block.
|
||||
j := AsJSON(errors.New("plain stdlib error"))
|
||||
assert.Nil(t, j.Retry, "non-base errors must omit the retry block")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsJSONOptionalFieldsOmittedWhenEmpty(t *testing.T) {
|
||||
j := AsJSON(New(TypeInternal, MustNewCode("boom"), "boom"))
|
||||
assert.Nil(t, j.Suggestions, "no suggestions set => Suggestions must be nil so json omitempty drops it")
|
||||
assert.Nil(t, j.InvalidReferences, "no invalid references set => InvalidReferences must be nil so json omitempty drops it")
|
||||
}
|
||||
|
||||
func TestWithStacktrace(t *testing.T) {
|
||||
err := New(TypeInternal, MustNewCode("test_code"), "panic").WithStacktrace("custom stack trace")
|
||||
|
||||
|
||||
@@ -3,13 +3,22 @@ package errors
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type JSON struct {
|
||||
Code string `json:"code" required:"true"`
|
||||
Message string `json:"message" required:"true"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Errors []responseerroradditional `json:"errors,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Code string `json:"code" required:"true"`
|
||||
Message string `json:"message" required:"true"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Errors []responseerroradditional `json:"errors,omitempty"`
|
||||
Retry *responseretryjson `json:"retry,omitempty"`
|
||||
Suggestions []string `json:"suggestions,omitempty"`
|
||||
InvalidReferences []string `json:"invalidReferences,omitempty"`
|
||||
}
|
||||
|
||||
type responseretryjson struct {
|
||||
Delay time.Duration `json:"delay"`
|
||||
}
|
||||
|
||||
type responseerroradditional struct {
|
||||
@@ -18,18 +27,27 @@ type responseerroradditional struct {
|
||||
|
||||
func AsJSON(cause error) *JSON {
|
||||
// See if this is an instance of the base error or not
|
||||
_, c, m, _, u, a := Unwrapb(cause)
|
||||
t, c, m, _, u, a := Unwrapb(cause)
|
||||
|
||||
rea := make([]responseerroradditional, len(a))
|
||||
for k, v := range a {
|
||||
rea[k] = responseerroradditional{v}
|
||||
}
|
||||
|
||||
var retry *responseretryjson
|
||||
if r := retryOf(cause); r != nil {
|
||||
retry = &responseretryjson{Delay: r.delay}
|
||||
}
|
||||
|
||||
return &JSON{
|
||||
Code: c.String(),
|
||||
Message: m,
|
||||
Url: u,
|
||||
Errors: rea,
|
||||
Type: t.String(),
|
||||
Code: c.String(),
|
||||
Message: m,
|
||||
Url: u,
|
||||
Errors: rea,
|
||||
Retry: retry,
|
||||
Suggestions: suggestionsOf(cause),
|
||||
InvalidReferences: invalidReferencesOf(cause),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
pkg/errors/retry.go
Normal file
16
pkg/errors/retry.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type retry struct {
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
// newRetryAfter builds a retry value carrying the given delay.
|
||||
func newRetryAfter(d time.Duration) retry {
|
||||
return retry{
|
||||
delay: d,
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,7 @@ var (
|
||||
TypeForbidden = typ{"forbidden"}
|
||||
TypeCanceled = typ{"canceled"}
|
||||
TypeTimeout = typ{"timeout"}
|
||||
TypeUnexpected = typ{"unexpected"} // Generic mismatch of expectations
|
||||
TypeFatal = typ{"fatal"} // Unrecoverable failure (e.g. panic)
|
||||
TypeFatal = typ{"fatal"} // Unrecoverable failure (e.g. panic)
|
||||
TypeLicenseUnavailable = typ{"license-unavailable"}
|
||||
TypeTooManyRequests = typ{"too-many-requests"}
|
||||
)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
@@ -121,6 +123,13 @@ func Error(rw http.ResponseWriter, cause error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Retry-After carries the explicit delay declared via
|
||||
// errors.WithRetryAfter. Set it before WriteHeader so headers go on the wire.
|
||||
d := errors.RetryDelayOf(cause)
|
||||
if d.Seconds() > 0 {
|
||||
rw.Header().Set("Retry-After", strconv.Itoa(int(math.Ceil(d.Seconds()))))
|
||||
}
|
||||
|
||||
rw.WriteHeader(httpCode)
|
||||
_, _ = rw.Write(body)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -97,13 +99,13 @@ func TestError(t *testing.T) {
|
||||
name: "AlreadyExists",
|
||||
statusCode: http.StatusConflict,
|
||||
err: errors.New(errors.TypeAlreadyExists, errors.MustNewCode("already_exists"), "already exists").WithUrl("https://already_exists"),
|
||||
expected: []byte(`{"status":"error","error":{"code":"already_exists","message":"already exists","url":"https://already_exists"}}`),
|
||||
expected: []byte(`{"status":"error","error":{"type":"already-exists","code":"already_exists","message":"already exists","url":"https://already_exists"}}`),
|
||||
},
|
||||
"/unauthenticated": {
|
||||
name: "Unauthenticated",
|
||||
statusCode: http.StatusUnauthorized,
|
||||
err: errors.New(errors.TypeUnauthenticated, errors.MustNewCode("not_allowed"), "not allowed").WithUrl("https://unauthenticated").WithAdditional("a1", "a2"),
|
||||
expected: []byte(`{"status":"error","error":{"code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1"},{"message":"a2"}]}}`),
|
||||
expected: []byte(`{"status":"error","error":{"type":"unauthenticated","code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1"},{"message":"a2"}]}}`),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -145,3 +147,67 @@ func TestError(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestErrorRetryAfterHeader verifies that the HTTP Retry-After header is set
|
||||
// when (and only when) the error declares an explicit WithRetryAfter delay.
|
||||
// Other retry policies (backoff, after_fix, after_auth, never) and bare errors
|
||||
// without any policy must NOT emit the header — clients ignore non-numeric or
|
||||
// missing values, but emitting one wrongly would mislead retry libraries.
|
||||
func TestErrorRetryAfterHeader(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
name string
|
||||
err error
|
||||
wantRetryAfter string // expected header value; "" means header must be absent
|
||||
wantBodyContains string // substring that must appear in the JSON body
|
||||
wantBodyNotContains string // substring that must NOT appear in the JSON body
|
||||
}{
|
||||
"/with_retry_after_5s": {
|
||||
name: "ExplicitDelay5Seconds",
|
||||
err: errors.New(errors.TypeTooManyRequests, errors.MustNewCode("rate_limited"), "slow down").WithRetryAfter(5 * time.Second),
|
||||
wantRetryAfter: "5",
|
||||
wantBodyContains: `"retry":{"delay":5000000000}`,
|
||||
},
|
||||
"/with_retry_after_subsecond": {
|
||||
name: "SubSecondRoundsUp",
|
||||
err: errors.New(errors.TypeTooManyRequests, errors.MustNewCode("rate_limited"), "slow down").WithRetryAfter(500 * time.Millisecond),
|
||||
wantRetryAfter: "1", // ceiling-rounded
|
||||
wantBodyContains: `"delay":500000000`,
|
||||
},
|
||||
"/bare_no_policy": {
|
||||
name: "BareErrorNoHeaderNoRetryBlock",
|
||||
err: errors.New(errors.TypeInternal, errors.MustNewCode("boom"), "boom"),
|
||||
wantRetryAfter: "",
|
||||
wantBodyContains: `"code":"boom"`,
|
||||
wantBodyNotContains: `"retry"`,
|
||||
},
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if tc, ok := testCases[req.URL.Path]; ok {
|
||||
Error(rw, tc.err)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
for path, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := http.Get(srv.URL + path)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, res.Body.Close()) }()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantRetryAfter, res.Header.Get("Retry-After"),
|
||||
"Retry-After header for %s", tc.name)
|
||||
if tc.wantBodyContains != "" {
|
||||
assert.Contains(t, string(body), tc.wantBodyContains,
|
||||
"body should contain %q for %s", tc.wantBodyContains, tc.name)
|
||||
}
|
||||
if tc.wantBodyNotContains != "" {
|
||||
assert.NotContains(t, string(body), tc.wantBodyNotContains,
|
||||
"body should NOT contain %q for %s", tc.wantBodyNotContains, tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if whereClause == nil || whereClause.WhereClause == nil {
|
||||
if whereClause.IsEmpty() {
|
||||
return sqlbuilder.NewWhereClause(), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -964,7 +964,7 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if whereClause == nil || whereClause.WhereClause == nil {
|
||||
if whereClause.IsEmpty() {
|
||||
return sqlbuilder.NewWhereClause(), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -202,15 +202,15 @@ func (handler *handler) exportRawDataJSONL(rowChan <-chan *qbtypes.RawRow, errCh
|
||||
}
|
||||
jsonBytes, err := json.Marshal(row.Data)
|
||||
if err != nil {
|
||||
return false, errors.NewUnexpectedf(errors.CodeInternal, "error marshaling JSON: %s", err)
|
||||
return false, errors.NewInternalf(errors.CodeInternal, "error marshaling JSON: %s", err)
|
||||
}
|
||||
totalBytes += uint64(len(jsonBytes)) + 1
|
||||
|
||||
if _, err := writer.Write(jsonBytes); err != nil {
|
||||
return false, errors.NewUnexpectedf(errors.CodeInternal, "error writing JSON: %s", err)
|
||||
return false, errors.NewInternalf(errors.CodeInternal, "error writing JSON: %s", err)
|
||||
}
|
||||
if _, err := writer.Write([]byte("\n")); err != nil {
|
||||
return false, errors.NewUnexpectedf(errors.CodeInternal, "error writing JSON newline: %s", err)
|
||||
return false, errors.NewInternalf(errors.CodeInternal, "error writing JSON newline: %s", err)
|
||||
}
|
||||
|
||||
if totalBytes > MaxExportBytesLimit {
|
||||
|
||||
@@ -510,9 +510,7 @@ func (s *store) buildFilterClause(ctx context.Context, filter qbtypes.Filter, st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prepared == nil || prepared.WhereClause == nil {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
return prepared.WhereClause, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ func (module *getter) ListDeprecatedUsersByOrgID(ctx context.Context, orgID valu
|
||||
roleNames := userIDToRoleNames[user.ID]
|
||||
|
||||
if len(roleNames) == 0 {
|
||||
return nil, errors.Newf(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found for user: %s", user.ID.String())
|
||||
return nil, errors.Newf(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found for user: %s", user.ID.String())
|
||||
}
|
||||
|
||||
role := authtypes.SigNozManagedRoleToExistingLegacyRole[roleNames[0]]
|
||||
@@ -113,11 +113,11 @@ func (module *getter) GetDeprecatedUserByOrgIDAndID(ctx context.Context, orgID v
|
||||
}
|
||||
|
||||
if len(userRoles) == 0 {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
||||
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
||||
}
|
||||
|
||||
if userRoles[0].Role == nil {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
}
|
||||
|
||||
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
|
||||
@@ -141,11 +141,11 @@ func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.Deprecate
|
||||
}
|
||||
|
||||
if len(userRoles) == 0 {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
||||
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
|
||||
}
|
||||
|
||||
if userRoles[0].Role == nil {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
}
|
||||
|
||||
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
|
||||
@@ -211,7 +211,7 @@ func (module *getter) GetRolesByUserID(ctx context.Context, userID valuer.UUID)
|
||||
|
||||
for _, ur := range userRoles {
|
||||
if ur.Role == nil {
|
||||
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ func getOperators(ops []pipelinetypes.PipelineOperator) ([]pipelinetypes.Pipelin
|
||||
|
||||
func processSeverityParser(operator *pipelinetypes.PipelineOperator) error {
|
||||
if operator.Type != "severity_parser" {
|
||||
return errors.NewUnexpectedf(CodeInvalidOperatorType, "operator type received %s", operator.Type)
|
||||
return errors.NewInternalf(CodeInvalidOperatorType, "operator type received %s", operator.Type)
|
||||
}
|
||||
|
||||
parseFromNotNilCheck, err := fieldNotNilCheck(operator.ParseFrom)
|
||||
@@ -236,7 +236,7 @@ func processSeverityParser(operator *pipelinetypes.PipelineOperator) error {
|
||||
// processJSONParser converts simple JSON parser operator into multiple operators for JSONMapping of default variables
|
||||
func processJSONParser(parent *pipelinetypes.PipelineOperator) ([]pipelinetypes.PipelineOperator, error) {
|
||||
if parent.Type != "json_parser" {
|
||||
return nil, errors.NewUnexpectedf(CodeInvalidOperatorType, "operator type received %s", parent.Type)
|
||||
return nil, errors.NewInternalf(CodeInvalidOperatorType, "operator type received %s", parent.Type)
|
||||
}
|
||||
|
||||
parseFromNotNilCheck, err := fieldNotNilCheck(parent.ParseFrom)
|
||||
|
||||
@@ -206,7 +206,6 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
dataType = telemetrytypes.FieldDataTypeFloat64
|
||||
}
|
||||
|
||||
//
|
||||
bodyJSONEnabled := v.flagger.BooleanOrEmpty(v.ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
|
||||
// Handle *If functions with predicate + values
|
||||
@@ -231,8 +230,8 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// not possible for whereClause to be nil here but still adding a check.
|
||||
if whereClause == nil {
|
||||
// not possible for whereClause to be empty here but still adding a check.
|
||||
if whereClause.IsEmpty() {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid predicate argument for %q: %q", name, origPred)
|
||||
}
|
||||
|
||||
|
||||
@@ -96,8 +96,12 @@ type PreparedWhereClause struct {
|
||||
WarningsDocURL string
|
||||
}
|
||||
|
||||
func (p PreparedWhereClause) IsEmpty() bool {
|
||||
return p.WhereClause == nil
|
||||
}
|
||||
|
||||
// PrepareWhereClause generates a ClickHouse compatible WHERE clause from the filter query.
|
||||
func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWhereClause, error) {
|
||||
func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (PreparedWhereClause, error) {
|
||||
|
||||
// Setup the ANTLR parsing pipeline
|
||||
input := antlr.NewInputStream(query)
|
||||
@@ -148,7 +152,7 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWher
|
||||
}
|
||||
}
|
||||
|
||||
return nil, combinedErrors.WithAdditional(additionals...).WithUrl(searchTroubleshootingGuideURL)
|
||||
return PreparedWhereClause{}, combinedErrors.WithAdditional(additionals...).WithUrl(searchTroubleshootingGuideURL)
|
||||
}
|
||||
|
||||
// Visit the parse tree with our ClickHouse visitor
|
||||
@@ -166,18 +170,17 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWher
|
||||
if url == "" {
|
||||
url = searchTroubleshootingGuideURL
|
||||
}
|
||||
return nil, combinedErrors.WithAdditional(visitor.errors...).WithUrl(url)
|
||||
return PreparedWhereClause{}, combinedErrors.WithAdditional(visitor.errors...).WithUrl(url)
|
||||
}
|
||||
|
||||
// Return nil so callers can skip the
|
||||
// entire CTE/subquery rather than emitting WHERE clause that select all the rows
|
||||
// Return empty where clause so callers can skip the WHERE clause
|
||||
if cond == "" || cond == SkipConditionLiteral {
|
||||
return nil, nil //nolint:nilnil
|
||||
return PreparedWhereClause{WhereClause: nil, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL}, nil
|
||||
}
|
||||
|
||||
whereClause := sqlbuilder.NewWhereClause().AddWhereExpr(visitor.builder.Args, cond)
|
||||
|
||||
return &PreparedWhereClause{WhereClause: whereClause, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL}, nil
|
||||
return PreparedWhereClause{WhereClause: whereClause, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL}, nil
|
||||
}
|
||||
|
||||
// Visit dispatches to the specific visit method based on node type.
|
||||
|
||||
@@ -874,7 +874,7 @@ func TestVisitComparison_AND(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -883,7 +883,7 @@ func TestVisitComparison_AND(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -968,7 +968,7 @@ func TestVisitComparison_NOT(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -977,7 +977,7 @@ func TestVisitComparison_NOT(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1070,7 +1070,7 @@ func TestVisitComparison_OR(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1079,7 +1079,7 @@ func TestVisitComparison_OR(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1151,7 +1151,7 @@ func TestVisitComparison_Precedence(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1160,7 +1160,7 @@ func TestVisitComparison_Precedence(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1254,7 +1254,7 @@ func TestVisitComparison_Parens(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1263,7 +1263,7 @@ func TestVisitComparison_Parens(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1409,7 +1409,7 @@ func TestVisitComparison_FullText(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1418,7 +1418,7 @@ func TestVisitComparison_FullText(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1518,7 +1518,7 @@ func TestVisitComparison_AllVariable(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1527,7 +1527,7 @@ func TestVisitComparison_AllVariable(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1597,7 +1597,7 @@ func TestVisitComparison_FunctionCalls(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1606,7 +1606,7 @@ func TestVisitComparison_FunctionCalls(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1666,7 +1666,7 @@ func TestVisitComparison_UnknownKeys(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1675,7 +1675,7 @@ func TestVisitComparison_UnknownKeys(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1758,7 +1758,7 @@ func TestVisitComparison_SkippableLiteralValues(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1767,7 +1767,7 @@ func TestVisitComparison_SkippableLiteralValues(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if result != nil {
|
||||
if !result.IsEmpty() {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
|
||||
@@ -210,6 +210,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewMigrateInstalledIntegrationDashboardsFactory(sqlstore),
|
||||
sqlmigration.NewAddDashboardNameFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewFixChangelogOperationTypeFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewCloudIntegrationRemoveCascadeDeleteFactory(sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
130
pkg/sqlmigration/091_ci_remove_cascade_delete.go
Normal file
130
pkg/sqlmigration/091_ci_remove_cascade_delete.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type cloudIntegrationRemoveCascadeDelete struct {
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
type ciServiceRow struct {
|
||||
bun.BaseModel `bun:"table:cloud_integration_service"`
|
||||
|
||||
ID string `bun:"id"`
|
||||
CreatedAt time.Time `bun:"created_at"`
|
||||
UpdatedAt time.Time `bun:"updated_at"`
|
||||
Type string `bun:"type"`
|
||||
Config string `bun:"config"`
|
||||
CloudIntegrationID string `bun:"cloud_integration_id"`
|
||||
}
|
||||
|
||||
func NewCloudIntegrationRemoveCascadeDeleteFactory(sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("ci_remove_cascade_delete"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &cloudIntegrationRemoveCascadeDelete{sqlschema: sqlschema}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (migration *cloudIntegrationRemoveCascadeDelete) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *cloudIntegrationRemoveCascadeDelete) Up(ctx context.Context, db *bun.DB) error {
|
||||
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
// get all existing rows
|
||||
var rows []*ciServiceRow
|
||||
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get existing table
|
||||
table, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("cloud_integration_service"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// drop the existing table
|
||||
for _, sql := range migration.sqlschema.Operator().DropTable(table) {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create new table without cascade delete FK
|
||||
newTable := &sqlschema.Table{
|
||||
Name: sqlschema.TableName("cloud_integration_service"),
|
||||
Columns: []*sqlschema.Column{
|
||||
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
|
||||
{Name: "type", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
{Name: "config", DataType: sqlschema.DataTypeText, Nullable: true},
|
||||
{Name: "cloud_integration_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||
},
|
||||
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
|
||||
ColumnNames: []sqlschema.ColumnName{"id"},
|
||||
},
|
||||
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||
{
|
||||
ReferencingColumnName: sqlschema.ColumnName("cloud_integration_id"),
|
||||
ReferencedTableName: sqlschema.TableName("cloud_integration"),
|
||||
ReferencedColumnName: sqlschema.ColumnName("id"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// create table
|
||||
for _, sql := range migration.sqlschema.Operator().CreateTable(newTable) {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// add back existing rows
|
||||
if len(rows) > 0 {
|
||||
if _, err := tx.NewInsert().Model(&rows).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create existing unique index on (cloud_integration_id, type)
|
||||
indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{
|
||||
TableName: "cloud_integration_service",
|
||||
ColumnNames: []sqlschema.ColumnName{"cloud_integration_id", "type"},
|
||||
})
|
||||
for _, sql := range indexSQLs {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return migration.sqlschema.ToggleFKEnforcement(ctx, db, true)
|
||||
}
|
||||
|
||||
func (migration *cloudIntegrationRemoveCascadeDelete) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -282,12 +282,10 @@ func (b *auditQueryStatementBuilder) buildListQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -422,12 +420,10 @@ func (b *auditQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
}
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -526,12 +522,10 @@ func (b *auditQueryStatementBuilder) buildScalarQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -544,8 +538,8 @@ func (b *auditQueryStatementBuilder) addFilterCondition(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (*querybuilder.PreparedWhereClause, error) {
|
||||
var preparedWhereClause *querybuilder.PreparedWhereClause
|
||||
) (querybuilder.PreparedWhereClause, error) {
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
|
||||
if query.Filter != nil && query.Filter.Expression != "" {
|
||||
@@ -564,11 +558,11 @@ func (b *auditQueryStatementBuilder) addFilterCondition(
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return preparedWhereClause, err
|
||||
}
|
||||
}
|
||||
|
||||
if preparedWhereClause != nil {
|
||||
if !preparedWhereClause.IsEmpty() {
|
||||
sb.AddWhereClause(preparedWhereClause.WhereClause)
|
||||
}
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ func TestFilterExprLogsBodyJSON(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
if clause == nil {
|
||||
if clause.IsEmpty() {
|
||||
t.Errorf("Expected clause for query: %s\n", tc.query)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2403,7 +2403,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
if clause == nil {
|
||||
if clause.IsEmpty() {
|
||||
t.Errorf("Expected clause for query: %s\n", tc.query)
|
||||
return
|
||||
}
|
||||
@@ -2524,7 +2524,7 @@ func TestFilterExprLogsConflictNegation(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
if clause == nil {
|
||||
if clause.IsEmpty() {
|
||||
t.Errorf("Expected clause for query: %s\n", tc.query)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -348,12 +348,10 @@ func (b *logQueryStatementBuilder) buildListQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -506,12 +504,10 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
}
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -627,12 +623,10 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -646,9 +640,9 @@ func (b *logQueryStatementBuilder) addFilterCondition(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (*querybuilder.PreparedWhereClause, error) {
|
||||
) (querybuilder.PreparedWhereClause, error) {
|
||||
|
||||
var preparedWhereClause *querybuilder.PreparedWhereClause
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
bodyJSONEnabled := b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
@@ -671,11 +665,11 @@ func (b *logQueryStatementBuilder) addFilterCondition(
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return preparedWhereClause, err
|
||||
}
|
||||
}
|
||||
|
||||
if preparedWhereClause != nil {
|
||||
if !preparedWhereClause.IsEmpty() {
|
||||
sb.AddWhereClause(preparedWhereClause.WhereClause)
|
||||
}
|
||||
|
||||
|
||||
@@ -237,8 +237,10 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
name string
|
||||
requestType qbtypes.RequestType
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
|
||||
variables map[string]qbtypes.VariableItem
|
||||
expected qbtypes.Statement
|
||||
expectedErr error
|
||||
expectWarn bool
|
||||
}{
|
||||
{
|
||||
name: "default list",
|
||||
@@ -312,6 +314,22 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "filter skips entirely but emits LIKE-without-wildcards warning",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "message LIKE 'plain' OR message IN $env",
|
||||
},
|
||||
Limit: 10,
|
||||
},
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Type: qbtypes.DynamicVariableType, Value: "__all__"},
|
||||
},
|
||||
expectedErr: nil,
|
||||
expectWarn: true,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -340,16 +358,20 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
||||
q, err := statementBuilder.Build(ctx, 1747947419000, 1747983448000, c.requestType, c.query, nil)
|
||||
q, err := statementBuilder.Build(ctx, 1747947419000, 1747983448000, c.requestType, c.query, c.variables)
|
||||
|
||||
if c.expectedErr != nil {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), c.expectedErr.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.expected.Query, q.Query)
|
||||
require.Equal(t, c.expected.Args, q.Args)
|
||||
require.Equal(t, c.expected.Warnings, q.Warnings)
|
||||
if c.expectWarn {
|
||||
require.NotEmpty(t, q.Warnings)
|
||||
} else {
|
||||
require.Equal(t, c.expected.Query, q.Query)
|
||||
require.Equal(t, c.expected.Args, q.Args)
|
||||
require.Equal(t, c.expected.Warnings, q.Warnings)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1424,7 +1424,7 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSel
|
||||
if err != nil {
|
||||
t.logger.WarnContext(ctx, "error parsing existing query for related values", errors.Attr(err))
|
||||
}
|
||||
if whereClause != nil {
|
||||
if !whereClause.IsEmpty() {
|
||||
sb.AddWhereClause(whereClause.WhereClause)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggDeltaFastPath(
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (string, []any, error) {
|
||||
var filterWhere *querybuilder.PreparedWhereClause
|
||||
var filterWhere querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
stepSec := int64(query.StepInterval.Seconds())
|
||||
|
||||
@@ -161,7 +161,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggDeltaFastPath(
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
if filterWhere != nil {
|
||||
if !filterWhere.IsEmpty() {
|
||||
sb.AddWhereClause(filterWhere.WhereClause)
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggDelta(
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (string, []any, error) {
|
||||
var filterWhere *querybuilder.PreparedWhereClause
|
||||
var filterWhere querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
|
||||
stepSec := int64(query.StepInterval.Seconds())
|
||||
@@ -250,7 +250,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggDelta(
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
if filterWhere != nil {
|
||||
if !filterWhere.IsEmpty() {
|
||||
sb.AddWhereClause(filterWhere.WhereClause)
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (string, []any, error) {
|
||||
var filterWhere *querybuilder.PreparedWhereClause
|
||||
var filterWhere querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
stepSec := int64(query.StepInterval.Seconds())
|
||||
|
||||
@@ -320,7 +320,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
if filterWhere != nil {
|
||||
if !filterWhere.IsEmpty() {
|
||||
baseSb.AddWhereClause(filterWhere.WhereClause)
|
||||
}
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ func (b *MetricQueryStatementBuilder) buildTimeSeriesCTE(
|
||||
) (string, []any, error) {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
var preparedWhereClause *querybuilder.PreparedWhereClause
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
|
||||
if query.Filter != nil && query.Filter.Expression != "" {
|
||||
@@ -311,7 +311,7 @@ func (b *MetricQueryStatementBuilder) buildTimeSeriesCTE(
|
||||
sb.EQ("__normalized", false),
|
||||
)
|
||||
|
||||
if preparedWhereClause != nil {
|
||||
if !preparedWhereClause.IsEmpty() {
|
||||
sb.AddWhereClause(preparedWhereClause.WhereClause)
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ func (b *resourceFilterStatementBuilder[T]) addConditions(
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if filterWhereClause == nil {
|
||||
if filterWhereClause.IsEmpty() {
|
||||
// this means all conditions evaluated to no-op (non-resource fields, unknown keys, skipped full-text/functions)
|
||||
// the CTE would select all fingerprints, so skip it entirely
|
||||
return true, nil
|
||||
|
||||
@@ -353,12 +353,10 @@ func (b *traceQueryStatementBuilder) buildListQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -471,12 +469,10 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -622,12 +618,10 @@ func (b *traceQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
}
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -740,12 +734,10 @@ func (b *traceQueryStatementBuilder) buildScalarQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -759,9 +751,9 @@ func (b *traceQueryStatementBuilder) addFilterCondition(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (*querybuilder.PreparedWhereClause, error) {
|
||||
) (querybuilder.PreparedWhereClause, error) {
|
||||
|
||||
var preparedWhereClause *querybuilder.PreparedWhereClause
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
|
||||
if query.Filter != nil && query.Filter.Expression != "" {
|
||||
@@ -779,11 +771,11 @@ func (b *traceQueryStatementBuilder) addFilterCondition(
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return preparedWhereClause, err
|
||||
}
|
||||
}
|
||||
|
||||
if preparedWhereClause != nil {
|
||||
if !preparedWhereClause.IsEmpty() {
|
||||
sb.AddWhereClause(preparedWhereClause.WhereClause)
|
||||
}
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ func (b *traceOperatorCTEBuilder) buildQueryCTE(ctx context.Context, queryName s
|
||||
b.stmtBuilder.logger.ErrorContext(ctx, "Failed to prepare where clause", errors.Attr(err), slog.String("filter", query.Filter.Expression))
|
||||
return "", err
|
||||
}
|
||||
if filterWhereClause != nil {
|
||||
if !filterWhereClause.IsEmpty() {
|
||||
b.stmtBuilder.logger.DebugContext(ctx, "Adding where clause", slog.Any("where_clause", filterWhereClause.WhereClause))
|
||||
sb.AddWhereClause(filterWhereClause.WhereClause)
|
||||
} else {
|
||||
|
||||
@@ -29,7 +29,7 @@ func NewContextWithClaims(ctx context.Context, claims Claims) context.Context {
|
||||
func ClaimsFromContext(ctx context.Context) (Claims, error) {
|
||||
claims, ok := ctx.Value(claimsKey{}).(Claims)
|
||||
if !ok {
|
||||
return Claims{}, errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
|
||||
return Claims{}, errors.NewUnauthenticatedf(errors.CodeUnauthenticated, "unauthenticated")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
@@ -42,7 +42,7 @@ func NewContextWithAccessToken(ctx context.Context, accessToken string) context.
|
||||
func AccessTokenFromContext(ctx context.Context) (string, error) {
|
||||
accessToken, ok := ctx.Value(accessTokenKey{}).(string)
|
||||
if !ok {
|
||||
return "", errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
|
||||
return "", errors.NewUnauthenticatedf(errors.CodeUnauthenticated, "unauthenticated")
|
||||
}
|
||||
|
||||
return accessToken, nil
|
||||
@@ -55,7 +55,7 @@ func NewContextWithAPIKey(ctx context.Context, apiKey string) context.Context {
|
||||
func APIKeyFromContext(ctx context.Context) (string, error) {
|
||||
apiKey, ok := ctx.Value(apiKeyKey{}).(string)
|
||||
if !ok {
|
||||
return "", errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
|
||||
return "", errors.NewUnauthenticatedf(errors.CodeUnauthenticated, "unauthenticated")
|
||||
}
|
||||
|
||||
return apiKey, nil
|
||||
@@ -77,7 +77,7 @@ func (c *Claims) IsSelfAccess(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "only the user/admin can access their own resource")
|
||||
return errors.NewForbiddenf(errors.CodeForbidden, "only the user/admin can access their own resource")
|
||||
}
|
||||
|
||||
func (c *Claims) IdentityID() string {
|
||||
|
||||
@@ -23,6 +23,8 @@ type Config struct {
|
||||
type SettingsConfig struct {
|
||||
Posthog PosthogConfig `mapstructure:"posthog"`
|
||||
Appcues AppcuesConfig `mapstructure:"appcues"`
|
||||
Sentry SentryConfig `mapstructure:"sentry"`
|
||||
Pylon PylonConfig `mapstructure:"sentry"`
|
||||
}
|
||||
|
||||
type PosthogConfig struct {
|
||||
@@ -33,6 +35,14 @@ type AppcuesConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
type SentryConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
type PylonConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(factory.MustNewName("web"), newConfig)
|
||||
}
|
||||
@@ -49,6 +59,12 @@ func newConfig() factory.Config {
|
||||
Appcues: AppcuesConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
Sentry: SentryConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
Pylon: PylonConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package web
|
||||
type Settings struct {
|
||||
Posthog Posthog `json:"posthog" required:"true"`
|
||||
Appcues Appcues `json:"appcues" required:"true"`
|
||||
Sentry Sentry `json:"sentry" required:"true"`
|
||||
Pylon Pylon `json:"pylon" required:"true"`
|
||||
}
|
||||
|
||||
type Posthog struct {
|
||||
@@ -13,6 +15,14 @@ type Appcues struct {
|
||||
Enabled bool `json:"enabled" required:"true"`
|
||||
}
|
||||
|
||||
type Sentry struct {
|
||||
Enabled bool `json:"enabled" required:"true"`
|
||||
}
|
||||
|
||||
type Pylon struct {
|
||||
Enabled bool `json:"enabled" required:"true"`
|
||||
}
|
||||
|
||||
func NewSettings(config Config) Settings {
|
||||
return Settings{
|
||||
Posthog: Posthog{
|
||||
@@ -21,5 +31,11 @@ func NewSettings(config Config) Settings {
|
||||
Appcues: Appcues{
|
||||
Enabled: config.Settings.Appcues.Enabled,
|
||||
},
|
||||
Sentry: Sentry{
|
||||
Enabled: config.Settings.Sentry.Enabled,
|
||||
},
|
||||
Pylon: Pylon{
|
||||
Enabled: config.Settings.Pylon.Enabled,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ from fixtures.querier import (
|
||||
build_group_by_field,
|
||||
build_logs_aggregation,
|
||||
build_order_by,
|
||||
build_raw_query,
|
||||
build_scalar_query,
|
||||
find_named_result,
|
||||
index_series_by_label,
|
||||
@@ -2625,3 +2626,43 @@ def test_logs_aggregation_filter_by_trace_id(
|
||||
orphan_count, orphan_warnings = _count(narrow_start_ms, now_ms, orphan_trace_id)
|
||||
assert orphan_count == 1, f"Expected count=1 for orphan trace_id aggregation, got {orphan_count} — query may have been incorrectly short-circuited"
|
||||
assert not any(outside_range_msg in m for m in orphan_warnings), f"Did not expect outside-range warning for orphan trace_id, got {orphan_warnings}"
|
||||
|
||||
|
||||
def test_logs_list_ambigous_warnings(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
) -> None:
|
||||
insert_logs(
|
||||
[
|
||||
Logs(
|
||||
timestamp=datetime.now(tz=UTC) - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "java",
|
||||
},
|
||||
attributes={
|
||||
"service.name": "java",
|
||||
},
|
||||
body="This is a log message, coming from a java application",
|
||||
severity_text="DEBUG",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD),
|
||||
start_ms=int((datetime.now(tz=UTC) - timedelta(minutes=1)).timestamp() * 1000),
|
||||
end_ms=int(datetime.now(tz=UTC).timestamp() * 1000),
|
||||
request_type="raw",
|
||||
queries=[build_raw_query(name="A", signal="logs", filter_expression='service.name = "java"')],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
warning = response.json()["data"].get("warning", None)
|
||||
assert warning is not None
|
||||
assert warning["message"] == "Encountered warnings"
|
||||
assert len(warning.get("warnings")) > 0
|
||||
assert any(["ambiguous" in w["message"] for w in warning.get("warnings")])
|
||||
|
||||
Reference in New Issue
Block a user