mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-01 14:50:29 +01:00
Compare commits
1 Commits
issue_5131
...
chore/play
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6477a5e2ee |
13
.github/workflows/e2eci.yaml
vendored
13
.github/workflows/e2eci.yaml
vendored
@@ -68,9 +68,22 @@ jobs:
|
||||
- name: pnpm-install
|
||||
run: |
|
||||
cd tests/e2e && pnpm install --frozen-lockfile
|
||||
- name: playwright-cache
|
||||
id: playwright-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('tests/e2e/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
- name: playwright-browsers
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd tests/e2e && pnpm playwright install --with-deps ${{ matrix.project }}
|
||||
- name: playwright-system-deps
|
||||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
cd tests/e2e && pnpm playwright install-deps ${{ matrix.project }}
|
||||
- name: bring-up-stack
|
||||
run: |
|
||||
cd tests && \
|
||||
|
||||
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -52,7 +52,6 @@ jobs:
|
||||
- rootuser
|
||||
- serviceaccount
|
||||
- querier_json_body
|
||||
- querier_skip_resource_fingerprint
|
||||
- ttl
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
|
||||
@@ -42,5 +42,4 @@ export enum LOCALSTORAGE {
|
||||
LICENSE_KEY_CALLOUT_DISMISSED = 'LICENSE_KEY_CALLOUT_DISMISSED',
|
||||
DASHBOARD_PREFERENCES = 'DASHBOARD_PREFERENCES',
|
||||
ACTIVE_SIGNOZ_INSTANCE_URL = 'ACTIVE_SIGNOZ_INSTANCE_URL',
|
||||
DASHBOARDS_LIST_VISIBLE_COLUMNS = 'DASHBOARDS_LIST_VISIBLE_COLUMNS',
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
export function useIsDashboardV2(): boolean {
|
||||
const { featureFlags } = useAppContext();
|
||||
return Boolean(
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_DASHBOARD_V2)
|
||||
?.active,
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
function DashboardPageV2(): JSX.Element {
|
||||
return <>DashboardPageV2</>;
|
||||
}
|
||||
|
||||
export default DashboardPageV2;
|
||||
@@ -1,3 +1,8 @@
|
||||
import DashboardPageV2 from './DashboardPageV2';
|
||||
|
||||
function DashboardPageV2(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard Page V2</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default DashboardPageV2;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
gap: 8px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--muted-foreground);
|
||||
font-family: Inter;
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { AnnouncementBanner } from '@signozhq/ui/announcement-banner';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { LayoutGrid } from '@signozhq/icons';
|
||||
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import DashboardsList from './components/DashboardsList';
|
||||
|
||||
import styles from './DashboardsListPageV2.module.scss';
|
||||
|
||||
function DashboardsListPageV2(): JSX.Element {
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
{showBanner && (
|
||||
<AnnouncementBanner
|
||||
type="warning"
|
||||
onClose={(): void => setShowBanner(false)}
|
||||
>
|
||||
You're on the V2 dashboards page. If you landed here unintentionally,
|
||||
please reach out to Ashwin.
|
||||
</AnnouncementBanner>
|
||||
)}
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerLeft}>
|
||||
<LayoutGrid size={14} className={styles.icon} />
|
||||
<Typography.Text className={styles.text}>Dashboards</Typography.Text>
|
||||
</div>
|
||||
<HeaderRightSection
|
||||
enableAnnouncements={false}
|
||||
enableShare
|
||||
enableFeedback
|
||||
/>
|
||||
</div>
|
||||
<DashboardsList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsListPageV2;
|
||||
@@ -1,28 +0,0 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Make signoz ghost-Button rows fill the popover and left-align their label.
|
||||
.menuItem {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
:global(.dashboardActionsPopover) {
|
||||
:global(.ant-popover-inner) {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { Popover } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import {
|
||||
Expand,
|
||||
EllipsisVertical,
|
||||
Link2,
|
||||
SquareArrowOutUpRight,
|
||||
} from '@signozhq/icons';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import DeleteActionItem from './DeleteActionItem';
|
||||
import styles from './ActionsPopover.module.scss';
|
||||
|
||||
interface Props {
|
||||
link: string;
|
||||
dashboardId: string;
|
||||
dashboardName: string;
|
||||
createdBy: string;
|
||||
isLocked: boolean;
|
||||
onView: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
function ActionsPopover({
|
||||
link,
|
||||
dashboardId,
|
||||
dashboardName,
|
||||
createdBy,
|
||||
isLocked,
|
||||
onView,
|
||||
}: Props): JSX.Element {
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
<div className={styles.content}>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Expand size={14} />}
|
||||
onClick={onView}
|
||||
testId="dashboard-action-view"
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<SquareArrowOutUpRight size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openInNewTab(link);
|
||||
}}
|
||||
testId="dashboard-action-open-new-tab"
|
||||
>
|
||||
Open in New Tab
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Link2 size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setCopy(getAbsoluteUrl(link));
|
||||
}}
|
||||
testId="dashboard-action-copy-link"
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
<DeleteActionItem
|
||||
dashboardId={dashboardId}
|
||||
dashboardName={dashboardName}
|
||||
createdBy={createdBy}
|
||||
isLocked={isLocked}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
placement="bottomRight"
|
||||
arrow={false}
|
||||
rootClassName="dashboardActionsPopover"
|
||||
trigger="click"
|
||||
>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
testId="dashboard-action-icon"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<EllipsisVertical size={14} />
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionsPopover;
|
||||
@@ -1,122 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { Modal, Tooltip } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { CircleAlert, Trash2 } from '@signozhq/icons';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import deleteDashboard from 'api/v1/dashboards/id/delete';
|
||||
import { invalidateListDashboardsV2 } from 'api/generated/services/dashboard';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import styles from './ActionsPopover.module.scss';
|
||||
|
||||
interface Props {
|
||||
dashboardId: string;
|
||||
dashboardName: string;
|
||||
createdBy: string;
|
||||
isLocked: boolean;
|
||||
}
|
||||
|
||||
function DeleteActionItem({
|
||||
dashboardId,
|
||||
dashboardName,
|
||||
createdBy,
|
||||
isLocked,
|
||||
}: Props): JSX.Element {
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
const { user } = useAppContext();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const queryClient = useQueryClient();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
const isAuthor = user?.email === createdBy;
|
||||
const isDisabled = isLocked || (user.role === USER_ROLES.VIEWER && !isAuthor);
|
||||
|
||||
const { mutate: runDelete } = useMutation({
|
||||
mutationFn: () => deleteDashboard({ id: dashboardId }),
|
||||
onSuccess: async () => {
|
||||
toast.success(
|
||||
t('dashboard:delete_dashboard_success', { name: dashboardName }),
|
||||
);
|
||||
await invalidateListDashboardsV2(queryClient);
|
||||
},
|
||||
onError: (error: APIError) => {
|
||||
showErrorModal(error);
|
||||
},
|
||||
});
|
||||
|
||||
const openConfirm = useCallback((): void => {
|
||||
const { destroy } = modal.confirm({
|
||||
title: (
|
||||
<Typography.Title level={5}>
|
||||
Are you sure you want to delete the
|
||||
<span style={{ color: 'var(--danger-background)', fontWeight: 500 }}>
|
||||
{' '}
|
||||
{dashboardName}{' '}
|
||||
</span>
|
||||
dashboard?
|
||||
</Typography.Title>
|
||||
),
|
||||
icon: (
|
||||
<CircleAlert
|
||||
style={{ color: 'var(--danger-background)', marginInlineEnd: '12px' }}
|
||||
size="3xl"
|
||||
/>
|
||||
),
|
||||
okText: 'Delete',
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
onClick: (e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
runDelete(undefined, { onSettled: () => destroy() });
|
||||
},
|
||||
},
|
||||
centered: true,
|
||||
});
|
||||
}, [modal, dashboardName, runDelete]);
|
||||
|
||||
const tooltip = ((): string => {
|
||||
if (!isLocked) {
|
||||
return '';
|
||||
}
|
||||
if (user.role === USER_ROLES.ADMIN || isAuthor) {
|
||||
return t('dashboard:locked_dashboard_delete_tooltip_admin_author');
|
||||
}
|
||||
return t('dashboard:locked_dashboard_delete_tooltip_editor');
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
<Tooltip placement="left" title={tooltip}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="destructive"
|
||||
className={styles.menuItem}
|
||||
prefix={<Trash2 size={14} />}
|
||||
disabled={isDisabled}
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isDisabled) {
|
||||
openConfirm();
|
||||
}
|
||||
}}
|
||||
testId="dashboard-action-delete"
|
||||
>
|
||||
Delete Dashboard
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{contextHolder}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteActionItem;
|
||||
@@ -1,164 +0,0 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: flex;
|
||||
padding: 12px 14.634px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 7.317px;
|
||||
border-radius: 4px;
|
||||
border: 0.915px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.previewHeader {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.previewIcon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.previewTitle {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12.805px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 18.293px;
|
||||
letter-spacing: -0.064px;
|
||||
}
|
||||
|
||||
.previewDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.previewRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.formattedTime {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.formattedTimeText {
|
||||
font-family: Inter;
|
||||
font-size: 12.805px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 16.463px;
|
||||
letter-spacing: -0.064px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.userTag {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 8px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: normal;
|
||||
letter-spacing: -0.05px;
|
||||
border-radius: 12.805px;
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
|
||||
.userLabel {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12.805px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 16.463px;
|
||||
letter-spacing: -0.064px;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0px 0px 0px 14.634px;
|
||||
}
|
||||
|
||||
.actionLeft {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.connectionLine {
|
||||
border-top: 1px dashed var(--l1-border);
|
||||
min-width: 20px;
|
||||
flex-grow: 1;
|
||||
margin: 0px 8px;
|
||||
}
|
||||
|
||||
.actionRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.saveChanges {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 8px 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
:global(.configureMetadataModalRoot) {
|
||||
:global(.ant-modal-content) {
|
||||
width: 500px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--card);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
:global(.ant-modal-header) {
|
||||
background: var(--card);
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
:global(.ant-modal-body) {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
:global(.ant-modal-footer) {
|
||||
margin-top: 0px;
|
||||
padding: 4px 16px 16px 16px;
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { CalendarClock, Check, Clock4 } from '@signozhq/icons';
|
||||
import { get } from 'lodash-es';
|
||||
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import { lastUpdatedLabel, type DashboardListItem } from '../../utils';
|
||||
import {
|
||||
DynamicColumns,
|
||||
useDashboardsListVisibleColumnsStore,
|
||||
type DashboardDynamicColumns,
|
||||
} from './useDynamicColumns';
|
||||
|
||||
import styles from './ConfigureMetadataModal.module.scss';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
previewDashboard: DashboardListItem | undefined;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function ConfigureMetadataModal({
|
||||
open,
|
||||
previewDashboard,
|
||||
onClose,
|
||||
}: Props): JSX.Element {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const storedColumns = useDashboardsListVisibleColumnsStore(
|
||||
(s) => s.visibleColumns,
|
||||
);
|
||||
const setStoredColumns = useDashboardsListVisibleColumnsStore(
|
||||
(s) => s.setVisibleColumns,
|
||||
);
|
||||
const [draftColumns, setDraftColumns] =
|
||||
useState<DashboardDynamicColumns>(storedColumns);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setDraftColumns(storedColumns);
|
||||
}
|
||||
}, [open, storedColumns]);
|
||||
|
||||
const handleSave = (): void => {
|
||||
setStoredColumns(draftColumns);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const previewImage = previewDashboard?.image || Base64Icons[0];
|
||||
const previewName = previewDashboard?.spec?.display?.name;
|
||||
const previewCreatedBy = previewDashboard?.createdBy;
|
||||
const previewUpdatedBy = previewDashboard?.updatedBy;
|
||||
const previewUpdatedAt = previewDashboard?.updatedAt;
|
||||
|
||||
const formattedCreatedAt = previewDashboard
|
||||
? formatTimezoneAdjustedTimestamp(
|
||||
get(previewDashboard, 'createdAt', '') as string,
|
||||
DATE_TIME_FORMATS.DASH_DATETIME_UTC,
|
||||
)
|
||||
: '';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
title="Configure Metadata"
|
||||
footer={
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Check size={14} />}
|
||||
className={styles.saveChanges}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
}
|
||||
rootClassName="configureMetadataModalRoot"
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.preview}>
|
||||
<section className={styles.previewHeader}>
|
||||
<img
|
||||
src={previewImage}
|
||||
alt="dashboard-image"
|
||||
className={styles.previewIcon}
|
||||
/>
|
||||
<Typography.Text className={styles.previewTitle}>
|
||||
{previewName}
|
||||
</Typography.Text>
|
||||
</section>
|
||||
<section className={styles.previewDetails}>
|
||||
<section className={styles.previewRow}>
|
||||
{draftColumns.createdAt && (
|
||||
<span className={styles.formattedTime}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text className={styles.formattedTimeText}>
|
||||
{formattedCreatedAt}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
)}
|
||||
{draftColumns.createdBy && (
|
||||
<div className={styles.user}>
|
||||
<Typography.Text className={styles.userTag}>
|
||||
{previewCreatedBy?.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.userLabel}>
|
||||
{previewCreatedBy}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<section className={styles.previewRow}>
|
||||
{draftColumns.updatedAt && (
|
||||
<span className={styles.formattedTime}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text className={styles.formattedTimeText}>
|
||||
{lastUpdatedLabel(previewUpdatedAt)}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
)}
|
||||
{draftColumns.updatedBy && (
|
||||
<div className={styles.user}>
|
||||
<Typography.Text className={styles.userTag}>
|
||||
{previewUpdatedBy?.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.userLabel}>
|
||||
{previewUpdatedBy}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className={styles.action}>
|
||||
<div className={styles.actionLeft}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>Created at</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.connectionLine} />
|
||||
<div className={styles.actionRight}>
|
||||
<Switch
|
||||
value
|
||||
disabled
|
||||
onChange={(check): void =>
|
||||
setDraftColumns((prev) => ({
|
||||
...prev,
|
||||
[DynamicColumns.CREATED_AT]: check,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.action}>
|
||||
<div className={styles.actionLeft}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>Created by</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.connectionLine} />
|
||||
<div className={styles.actionRight}>
|
||||
<Switch
|
||||
value
|
||||
disabled
|
||||
onChange={(check): void =>
|
||||
setDraftColumns((prev) => ({
|
||||
...prev,
|
||||
[DynamicColumns.CREATED_BY]: check,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.action}>
|
||||
<div className={styles.actionLeft}>
|
||||
<Clock4 size={14} />
|
||||
<Typography.Text>Updated at</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.connectionLine} />
|
||||
<div className={styles.actionRight}>
|
||||
<Switch
|
||||
value={draftColumns.updatedAt}
|
||||
onChange={(check): void =>
|
||||
setDraftColumns((prev) => ({
|
||||
...prev,
|
||||
[DynamicColumns.UPDATED_AT]: check,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.action}>
|
||||
<div className={styles.actionLeft}>
|
||||
<Clock4 size={14} />
|
||||
<Typography.Text>Updated by</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.connectionLine} />
|
||||
<div className={styles.actionRight}>
|
||||
<Switch
|
||||
value={draftColumns.updatedBy}
|
||||
onChange={(check): void =>
|
||||
setDraftColumns((prev) => ({
|
||||
...prev,
|
||||
[DynamicColumns.UPDATED_BY]: check,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfigureMetadataModal;
|
||||
@@ -1,52 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
export interface DashboardDynamicColumns {
|
||||
createdAt: boolean;
|
||||
createdBy: boolean;
|
||||
updatedAt: boolean;
|
||||
updatedBy: boolean;
|
||||
}
|
||||
|
||||
export enum DynamicColumns {
|
||||
CREATED_AT = 'createdAt',
|
||||
CREATED_BY = 'createdBy',
|
||||
UPDATED_AT = 'updatedAt',
|
||||
UPDATED_BY = 'updatedBy',
|
||||
}
|
||||
|
||||
const DEFAULT_COLUMNS: DashboardDynamicColumns = {
|
||||
createdAt: true,
|
||||
createdBy: true,
|
||||
updatedAt: false,
|
||||
updatedBy: false,
|
||||
};
|
||||
|
||||
interface DashboardsListVisibleColumnsState {
|
||||
visibleColumns: DashboardDynamicColumns;
|
||||
setVisibleColumns: (next: DashboardDynamicColumns) => void;
|
||||
}
|
||||
|
||||
export const useDashboardsListVisibleColumnsStore =
|
||||
create<DashboardsListVisibleColumnsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
visibleColumns: DEFAULT_COLUMNS,
|
||||
setVisibleColumns: (next): void => {
|
||||
set({ visibleColumns: next });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: LOCALSTORAGE.DASHBOARDS_LIST_VISIBLE_COLUMNS,
|
||||
merge: (persisted, current) => ({
|
||||
...current,
|
||||
visibleColumns: {
|
||||
...DEFAULT_COLUMNS,
|
||||
...((persisted as Partial<DashboardsListVisibleColumnsState>)
|
||||
?.visibleColumns ?? {}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -1,34 +0,0 @@
|
||||
.menuItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.templatesItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.primaryButton {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.textButton {
|
||||
display: flex;
|
||||
width: 153px;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 6px 12px;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border-radius: 2px;
|
||||
background: var(--primary-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
:global(.createDashboardMenuOverlay) {
|
||||
width: 200px;
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
// eslint-disable-next-line signoz/no-antd-components -- TODO: migrate Dropdown to @signozhq/ui/dropdown-menu
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
ExternalLink,
|
||||
Github,
|
||||
LayoutGrid,
|
||||
Plus,
|
||||
Radius,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import styles from './CreateDashboardDropdown.module.scss';
|
||||
|
||||
interface Props {
|
||||
canCreate: boolean;
|
||||
onCreate: () => void;
|
||||
onImportJSON: () => void;
|
||||
variant?: 'primary' | 'text';
|
||||
}
|
||||
|
||||
const TEMPLATES_HREF =
|
||||
'https://signoz.io/docs/dashboards/dashboard-templates/overview/';
|
||||
|
||||
function CreateDashboardDropdown({
|
||||
canCreate,
|
||||
onCreate,
|
||||
onImportJSON,
|
||||
variant = 'primary',
|
||||
}: Props): JSX.Element {
|
||||
const items: MenuProps['items'] = useMemo(() => {
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'import-json',
|
||||
label: (
|
||||
<div
|
||||
className={styles.menuItem}
|
||||
data-testid="import-json-menu-cta"
|
||||
onClick={onImportJSON}
|
||||
>
|
||||
<Radius size={14} /> Import JSON
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'view-templates',
|
||||
label: (
|
||||
<a
|
||||
href={TEMPLATES_HREF}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid="view-templates-menu-cta"
|
||||
>
|
||||
<div className={styles.templatesItem}>
|
||||
<div className={styles.menuItem}>
|
||||
<Github size={14} /> View templates
|
||||
</div>
|
||||
<ExternalLink size={14} />
|
||||
</div>
|
||||
</a>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (canCreate) {
|
||||
menuItems.unshift({
|
||||
key: 'create-dashboard',
|
||||
label: (
|
||||
<div
|
||||
className={styles.menuItem}
|
||||
data-testid="create-dashboard-menu-cta"
|
||||
onClick={onCreate}
|
||||
>
|
||||
<LayoutGrid size={14} /> Create dashboard
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
}, [canCreate, onCreate, onImportJSON]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlayClassName="createDashboardMenuOverlay"
|
||||
menu={{ items }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
>
|
||||
{variant === 'primary' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
className={cx('periscope-btn primary', styles.primaryButton)}
|
||||
icon={<Plus size={14} />}
|
||||
data-testid="new-dashboard-cta"
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List: New dashboard clicked', {});
|
||||
}}
|
||||
>
|
||||
New dashboard
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.textButton}
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List: New dashboard clicked', {});
|
||||
}}
|
||||
>
|
||||
New Dashboard
|
||||
</Button>
|
||||
)}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDashboardDropdown;
|
||||
@@ -1,152 +0,0 @@
|
||||
.row {
|
||||
padding: 12px 16px 16px 16px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-top: none;
|
||||
background: var(--l2-background);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.titleWithAction {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.titleBlock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
line-height: 20px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.titleLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
line-height: 20px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tagsWithActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 28px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
|
||||
color: var(--bg-sienna-400);
|
||||
text-align: center;
|
||||
font-family: Inter;
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 24px;
|
||||
}
|
||||
|
||||
.createdAt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.createdBy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50px;
|
||||
background: var(--l1-border);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatarText {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 8px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: normal;
|
||||
letter-spacing: -0.05px;
|
||||
}
|
||||
|
||||
.byLabel {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.updatedBy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
:global(.titleTooltipOverlay) {
|
||||
:global(.ant-tooltip-content) :global(.ant-tooltip-inner) {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { CalendarClock } from '@signozhq/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import type { DashboardListItem } from '../../utils';
|
||||
import { lastUpdatedLabel, tagsToStrings } from '../../utils';
|
||||
import ActionsPopover from '../ActionsPopover/ActionsPopover';
|
||||
|
||||
import styles from './DashboardRow.module.scss';
|
||||
|
||||
interface Props {
|
||||
dashboard: DashboardListItem;
|
||||
index: number;
|
||||
canAct: boolean;
|
||||
showUpdatedAt: boolean;
|
||||
showUpdatedBy: boolean;
|
||||
}
|
||||
|
||||
function DashboardRow({
|
||||
dashboard,
|
||||
index,
|
||||
canAct,
|
||||
showUpdatedAt,
|
||||
showUpdatedBy,
|
||||
}: Props): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const id = dashboard.id;
|
||||
const name = dashboard.spec?.display?.name ?? '';
|
||||
const image = dashboard.image || Base64Icons[0];
|
||||
const createdBy = dashboard.createdBy ?? '';
|
||||
const updatedBy = dashboard.updatedBy ?? '';
|
||||
const createdAt = dashboard.createdAt ?? '';
|
||||
const updatedAt = dashboard.updatedAt ?? '';
|
||||
const isLocked = !!dashboard.locked;
|
||||
const tags = tagsToStrings(dashboard.tags);
|
||||
|
||||
const link = generatePath(ROUTES.DASHBOARD, { dashboardId: id });
|
||||
const formattedCreatedAt = formatTimezoneAdjustedTimestamp(
|
||||
createdAt,
|
||||
DATE_TIME_FORMATS.DASH_DATETIME_UTC,
|
||||
);
|
||||
|
||||
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
event.stopPropagation();
|
||||
safeNavigate(link, { newTab: isModifierKeyPressed(event) });
|
||||
logEvent('Dashboard List: Clicked on dashboard', {
|
||||
dashboardId: id,
|
||||
dashboardName: name,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.row} onClick={onClickHandler}>
|
||||
<div className={styles.titleWithAction}>
|
||||
<div className={styles.titleBlock}>
|
||||
<Tooltip
|
||||
title={name.length > 50 ? name : ''}
|
||||
placement="left"
|
||||
overlayClassName="titleTooltipOverlay"
|
||||
>
|
||||
<div className={styles.titleLink} onClick={onClickHandler}>
|
||||
<img src={image} alt="dashboard-image" className={styles.icon} />
|
||||
<Typography.Text
|
||||
data-testid={`dashboard-title-${index}`}
|
||||
className={styles.title}
|
||||
>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className={styles.tagsWithActions}>
|
||||
{tags.length > 0 && (
|
||||
<div className={styles.tags}>
|
||||
{tags.slice(0, 3).map((tag) => (
|
||||
<Badge className={styles.tag} key={tag}>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{tags.length > 3 && (
|
||||
<Badge className={styles.tag} key={tags[3]}>
|
||||
+ <span> {tags.length - 3} </span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canAct && (
|
||||
<ActionsPopover
|
||||
link={link}
|
||||
dashboardId={id}
|
||||
dashboardName={name}
|
||||
createdBy={createdBy}
|
||||
isLocked={isLocked}
|
||||
onView={onClickHandler}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.createdAt}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>{formattedCreatedAt}</Typography.Text>
|
||||
</div>
|
||||
|
||||
{createdBy && (
|
||||
<div className={styles.createdBy}>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{createdBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{createdBy}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showUpdatedAt && (
|
||||
<div className={styles.createdAt}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>{lastUpdatedLabel(updatedAt)}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updatedBy && showUpdatedBy && (
|
||||
<div className={styles.updatedBy}>
|
||||
<Typography.Text className={styles.byLabel}>
|
||||
Last Updated By -
|
||||
</Typography.Text>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{updatedBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{updatedBy}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardRow;
|
||||
@@ -1,96 +0,0 @@
|
||||
.container {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.viewContent {
|
||||
width: calc(100% - 30px);
|
||||
max-width: 836px;
|
||||
|
||||
:global(.ant-table-wrapper) :global(.ant-table-cell) {
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
background: var(--l1-background) !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-wrapper)
|
||||
:global(.ant-table-tbody)
|
||||
:global(.ant-table-row)
|
||||
:global(.ant-table-cell)
|
||||
> div {
|
||||
// Row content is the only child of the td; it carries the borders.
|
||||
}
|
||||
|
||||
:global(.ant-table-wrapper)
|
||||
:global(.ant-table-tbody)
|
||||
:global(.ant-table-row:last-child)
|
||||
:global(.ant-table-cell)
|
||||
> div {
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item) > a {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item-active) {
|
||||
background-color: var(--primary-background);
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item-active) > a {
|
||||
color: var(--foreground) !important;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-lg);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 28px;
|
||||
letter-spacing: -0.09px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.integrationsContainer {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.integrationsContent {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AxiosError } from 'axios';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
createDashboardV2,
|
||||
useListDashboardsV2,
|
||||
} from 'api/generated/services/dashboard';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { RequestDashboardBtn } from 'container/ListOfDashboard/RequestDashboardBtn';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import {
|
||||
usePage,
|
||||
useSearch,
|
||||
useSortColumn,
|
||||
useSortOrder,
|
||||
type SortColumn,
|
||||
type SortOrder,
|
||||
} from '../../hooks/useDashboardsListQueryParams';
|
||||
import type { DashboardListItem } from '../../utils';
|
||||
import ConfigureMetadataModal from '../ConfigureMetadataModal/ConfigureMetadataModal';
|
||||
import { useDashboardsListVisibleColumnsStore } from '../ConfigureMetadataModal/useDynamicColumns';
|
||||
import CreateDashboardDropdown from '../CreateDashboardDropdown/CreateDashboardDropdown';
|
||||
import ImportJSONModal from '../ImportJSONModal/ImportJSONModal';
|
||||
import ListHeader from '../ListHeader/ListHeader';
|
||||
import EmptyState from '../states/EmptyState/EmptyState';
|
||||
import ErrorState from '../states/ErrorState/ErrorState';
|
||||
import LoadingState from '../states/LoadingState/LoadingState';
|
||||
import NoResultsState from '../states/NoResultsState/NoResultsState';
|
||||
import SearchBar from '../SearchBar/SearchBar';
|
||||
import DashboardsListContent from './DashboardsListContent';
|
||||
|
||||
import styles from './DashboardsList.module.scss';
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
function DashboardsList(): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { t } = useTranslation('dashboard');
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { isCloudUser } = useGetTenantLicense();
|
||||
|
||||
const { user } = useAppContext();
|
||||
const [action, canCreateNewDashboard] = useComponentPermission(
|
||||
['action', 'create_new_dashboards'],
|
||||
user.role,
|
||||
);
|
||||
|
||||
const [searchString, setSearchString] = useSearch();
|
||||
const [sortColumn, setSortColumn] = useSortColumn();
|
||||
const [sortOrder, setSortOrder] = useSortOrder();
|
||||
const [page, setPage] = usePage();
|
||||
|
||||
const [searchInput, setSearchInput] = useState(searchString);
|
||||
|
||||
// Keep the local input in sync with external searchString changes
|
||||
// (browser back/forward, deep link). User typing only mutates
|
||||
// searchInput, so this won't fight with in-flight edits.
|
||||
useEffect(() => {
|
||||
setSearchInput(searchString);
|
||||
}, [searchString]);
|
||||
|
||||
const handleSubmitSearch = useCallback((): void => {
|
||||
const next = searchInput.trim();
|
||||
if (next === searchString) {
|
||||
return;
|
||||
}
|
||||
void setSearchString(next);
|
||||
void setPage(1);
|
||||
}, [searchInput, searchString, setSearchString, setPage]);
|
||||
|
||||
const listParams = useMemo(
|
||||
() => ({
|
||||
query: searchString.trim() || undefined,
|
||||
sort: sortColumn,
|
||||
order: sortOrder,
|
||||
limit: PAGE_SIZE,
|
||||
offset: (page - 1) * PAGE_SIZE,
|
||||
}),
|
||||
[searchString, sortColumn, sortOrder, page],
|
||||
);
|
||||
|
||||
const {
|
||||
data: response,
|
||||
isLoading,
|
||||
isFetching,
|
||||
error,
|
||||
refetch,
|
||||
} = useListDashboardsV2(listParams, { query: { keepPreviousData: true } });
|
||||
|
||||
const apiError = useMemo(
|
||||
() => (error ? toAPIError(error) : undefined),
|
||||
[error],
|
||||
);
|
||||
const errorHttpStatus = apiError?.getHttpStatusCode();
|
||||
const errorMessage = apiError?.getErrorMessage();
|
||||
|
||||
const dashboards = useMemo<DashboardListItem[]>(
|
||||
() => response?.data?.dashboards ?? [],
|
||||
[response],
|
||||
);
|
||||
const total = response?.data?.total ?? 0;
|
||||
|
||||
const [isImportOpen, setIsImportOpen] = useState(false);
|
||||
const [isConfigureOpen, setIsConfigureOpen] = useState(false);
|
||||
const visibleColumns = useDashboardsListVisibleColumnsStore(
|
||||
(s) => s.visibleColumns,
|
||||
);
|
||||
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const handleCreateNew = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
logEvent('Dashboard List: Create dashboard clicked', {});
|
||||
setCreating(true);
|
||||
const created = await createDashboardV2({
|
||||
schemaVersion: 'v6',
|
||||
// Backend requires `name` (immutable, server-side identifier);
|
||||
// asking it to generate one keeps the UI's "new dashboard" flow.
|
||||
generateName: true,
|
||||
tags: null,
|
||||
spec: {
|
||||
display: { name: t('new_dashboard_title', { ns: 'dashboard' }) },
|
||||
},
|
||||
});
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, { dashboardId: created.data.id }),
|
||||
);
|
||||
} catch (e) {
|
||||
showErrorModal(e as APIError);
|
||||
toast.error((e as AxiosError).toString() || 'Failed to create dashboard');
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
}, [safeNavigate, showErrorModal, t]);
|
||||
|
||||
const handleImportToggle = useCallback((): void => {
|
||||
logEvent('Dashboard List V2: Import JSON clicked', {});
|
||||
setIsImportOpen((s) => !s);
|
||||
}, []);
|
||||
|
||||
const onSortChange = useCallback(
|
||||
(column: SortColumn): void => {
|
||||
void setSortColumn(column);
|
||||
void setPage(1);
|
||||
},
|
||||
[setSortColumn, setPage],
|
||||
);
|
||||
|
||||
const onOrderChange = useCallback(
|
||||
(order: SortOrder): void => {
|
||||
void setSortOrder(order);
|
||||
void setPage(1);
|
||||
},
|
||||
[setSortOrder, setPage],
|
||||
);
|
||||
|
||||
const visitLoggedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!visitLoggedRef.current && !isLoading && response !== undefined) {
|
||||
logEvent('Dashboard List V2: Page visited', { number: dashboards.length });
|
||||
visitLoggedRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.viewContent}>
|
||||
<div className={styles.titleContainer}>
|
||||
<Typography.Title className={styles.title}>Dashboards</Typography.Title>
|
||||
<Typography.Text className={styles.subtitle}>
|
||||
Create and manage dashboards for your workspace.
|
||||
</Typography.Text>
|
||||
{isCloudUser && (
|
||||
<div className={styles.integrationsContainer}>
|
||||
<div className={styles.integrationsContent}>
|
||||
<RequestDashboardBtn />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<LoadingState />
|
||||
) : !error && dashboards.length === 0 && !searchString && page === 1 ? (
|
||||
<EmptyState
|
||||
createDropdown={
|
||||
canCreateNewDashboard ? (
|
||||
<CreateDashboardDropdown
|
||||
canCreate={!!canCreateNewDashboard}
|
||||
onCreate={handleCreateNew}
|
||||
onImportJSON={handleImportToggle}
|
||||
variant="text"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.toolbar}>
|
||||
<SearchBar
|
||||
value={searchInput}
|
||||
onChange={setSearchInput}
|
||||
onSubmit={handleSubmitSearch}
|
||||
/>
|
||||
{canCreateNewDashboard && (
|
||||
<CreateDashboardDropdown
|
||||
canCreate={!!canCreateNewDashboard}
|
||||
onCreate={handleCreateNew}
|
||||
onImportJSON={handleImportToggle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<ErrorState
|
||||
isCloudUser={!!isCloudUser}
|
||||
onRetry={(): void => {
|
||||
refetch();
|
||||
}}
|
||||
httpStatus={errorHttpStatus}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
) : dashboards.length === 0 ? (
|
||||
<NoResultsState searchString={searchInput} />
|
||||
) : (
|
||||
<>
|
||||
<ListHeader
|
||||
sortColumn={sortColumn}
|
||||
onSortChange={onSortChange}
|
||||
sortOrder={sortOrder}
|
||||
onOrderChange={onOrderChange}
|
||||
onConfigureMetadata={(): void => setIsConfigureOpen(true)}
|
||||
/>
|
||||
<DashboardsListContent
|
||||
dashboards={dashboards}
|
||||
page={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
total={total}
|
||||
onPageChange={setPage}
|
||||
canAct={!!action}
|
||||
showUpdatedAt={visibleColumns.updatedAt}
|
||||
showUpdatedBy={visibleColumns.updatedBy}
|
||||
loading={creating || isFetching}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<ImportJSONModal
|
||||
open={isImportOpen}
|
||||
onClose={(): void => setIsImportOpen(false)}
|
||||
/>
|
||||
|
||||
<ConfigureMetadataModal
|
||||
open={isConfigureOpen}
|
||||
previewDashboard={dashboards[0]}
|
||||
onClose={(): void => setIsConfigureOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsList;
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Table } from 'antd';
|
||||
import type { TableProps } from 'antd/lib';
|
||||
|
||||
import type { DashboardListItem } from '../../utils';
|
||||
import DashboardRow from '../DashboardRow/DashboardRow';
|
||||
|
||||
interface Props {
|
||||
dashboards: DashboardListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
onPageChange: (page: number) => void;
|
||||
canAct: boolean;
|
||||
showUpdatedAt: boolean;
|
||||
showUpdatedBy: boolean;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
function DashboardsListContent({
|
||||
dashboards,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
onPageChange,
|
||||
canAct,
|
||||
showUpdatedAt,
|
||||
showUpdatedBy,
|
||||
loading,
|
||||
}: Props): JSX.Element {
|
||||
const columns: TableProps<DashboardListItem>['columns'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Dashboards',
|
||||
key: 'dashboard',
|
||||
render: (_, dashboard, index): JSX.Element => (
|
||||
<DashboardRow
|
||||
dashboard={dashboard}
|
||||
index={index}
|
||||
canAct={canAct}
|
||||
showUpdatedAt={showUpdatedAt}
|
||||
showUpdatedBy={showUpdatedBy}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[canAct, showUpdatedAt, showUpdatedBy],
|
||||
);
|
||||
|
||||
const paginationConfig = total > pageSize && {
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
onChange: onPageChange,
|
||||
current: page,
|
||||
total,
|
||||
hideOnSinglePage: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dashboards.map((d) => ({ ...d, key: d.id }))}
|
||||
showSorterTooltip
|
||||
loading={loading}
|
||||
showHeader={false}
|
||||
pagination={paginationConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsListContent;
|
||||
@@ -1,3 +0,0 @@
|
||||
import DashboardsList from './DashboardsList';
|
||||
|
||||
export default DashboardsList;
|
||||
@@ -1,73 +0,0 @@
|
||||
.contentContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.contentHeader {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.jsonError {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
color: var(--warning-background);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.importJsonModalWrapper) {
|
||||
:global(.ant-modal-content) {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.margin) {
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
:global(.view-lines) {
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
:global(.ant-modal-footer) {
|
||||
margin-top: 0;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { red } from '@ant-design/colors';
|
||||
import MEditor, { Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Flex, Modal, Upload, UploadProps } from 'antd';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
CircleAlert,
|
||||
ExternalLink,
|
||||
Github,
|
||||
MonitorDot,
|
||||
MoveRight,
|
||||
Sparkles,
|
||||
} from '@signozhq/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { createDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import sampleDashboard from './sampleDashboard.json';
|
||||
|
||||
import styles from './ImportJSONModal.module.scss';
|
||||
import { normalizeToPostable } from './ImportJSONModalUtils';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function ImportJSONModal({ open, onClose }: Props): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { t } = useTranslation(['dashboard', 'common']);
|
||||
const [isUploadError, setIsUploadError] = useState(false);
|
||||
const [isCreateError, setIsCreateError] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [editorValue, setEditorValue] = useState('');
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const handleUpload: UploadProps['onChange'] = (info) => {
|
||||
const lastFile = info.fileList[info.fileList.length - 1];
|
||||
if (!lastFile?.originFileObj) {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event): void => {
|
||||
try {
|
||||
const target = event.target?.result;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const parsed = JSON.parse(target.toString());
|
||||
setEditorValue(JSON.stringify(parsed, null, 2));
|
||||
setIsUploadError(false);
|
||||
} catch {
|
||||
setIsUploadError(true);
|
||||
}
|
||||
};
|
||||
reader.readAsText(lastFile.originFileObj);
|
||||
};
|
||||
|
||||
const handleImport = async (): Promise<void> => {
|
||||
try {
|
||||
setIsCreating(true);
|
||||
logEvent('Dashboard List V2: Import and next clicked', {});
|
||||
const parsed = JSON.parse(editorValue) as Record<string, unknown>;
|
||||
const payload = normalizeToPostable(parsed);
|
||||
const response = await createDashboardV2(payload);
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
|
||||
);
|
||||
logEvent('Dashboard List V2: New dashboard imported successfully', {
|
||||
dashboardId: response.data?.id,
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setIsCreateError(true);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : t('error_loading_json'),
|
||||
);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
setIsUploadError(false);
|
||||
setIsCreateError(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const setEditorTheme = (monaco: Monaco): void => {
|
||||
monaco.editor.defineTheme('my-theme', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
|
||||
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
|
||||
],
|
||||
colors: { 'editor.background': Color.BG_INK_300 },
|
||||
});
|
||||
};
|
||||
|
||||
const renderError = (msg: string): JSX.Element => (
|
||||
<div className={styles.jsonError}>
|
||||
<CircleAlert size="md" color={red[7]} />
|
||||
<Typography className={styles.errorText}>{msg}</Typography>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
wrapClassName="importJsonModalWrapper"
|
||||
open={open}
|
||||
centered
|
||||
closable
|
||||
keyboard
|
||||
maskClosable
|
||||
onCancel={handleClose}
|
||||
destroyOnClose
|
||||
width="60vw"
|
||||
footer={
|
||||
<div className={styles.footer}>
|
||||
{isCreateError && renderError(t('error_loading_json'))}
|
||||
{isUploadError && renderError(t('error_upload_json'))}
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Flex gap="small">
|
||||
<Upload
|
||||
accept=".json"
|
||||
showUploadList={false}
|
||||
multiple={false}
|
||||
onChange={handleUpload}
|
||||
beforeUpload={(): boolean => false}
|
||||
action="none"
|
||||
>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
icon={<MonitorDot size={14} />}
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List V2: Upload JSON file clicked', {});
|
||||
}}
|
||||
>
|
||||
{t('upload_json_file')}
|
||||
</Button>
|
||||
</Upload>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
icon={<Sparkles size={14} />}
|
||||
onClick={(): void => {
|
||||
setEditorValue(JSON.stringify(sampleDashboard, null, 2));
|
||||
setIsUploadError(false);
|
||||
logEvent('Dashboard List V2: Load sample clicked', {});
|
||||
}}
|
||||
>
|
||||
Load sample
|
||||
</Button>
|
||||
<a
|
||||
href="https://signoz.io/docs/dashboards/dashboard-templates/overview/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
icon={<Github size={14} />}
|
||||
>
|
||||
{t('view_template')}
|
||||
<ExternalLink size={14} />
|
||||
</Button>
|
||||
</a>
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
loading={isCreating}
|
||||
className="periscope-btn primary"
|
||||
type="primary"
|
||||
>
|
||||
{t('import_and_next')} <MoveRight size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.contentContainer}>
|
||||
<div className={styles.contentHeader}>
|
||||
<Typography.Text>{t('import_json')}</Typography.Text>
|
||||
</div>
|
||||
<MEditor
|
||||
language="json"
|
||||
height="40vh"
|
||||
onChange={(newValue): void => setEditorValue(newValue || '')}
|
||||
value={editorValue}
|
||||
options={{
|
||||
scrollbar: { alwaysConsumeMouseWheel: false },
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
fontFamily: 'Space Mono',
|
||||
}}
|
||||
theme={isDarkMode ? 'my-theme' : 'light'}
|
||||
onMount={(_, monaco): void => {
|
||||
document.fonts.ready.then(() => {
|
||||
monaco.editor.remeasureFonts();
|
||||
});
|
||||
}}
|
||||
beforeMount={setEditorTheme}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportJSONModal;
|
||||
@@ -1,50 +0,0 @@
|
||||
import {
|
||||
DashboardtypesDashboardSpecDTO,
|
||||
DashboardtypesPostableDashboardV2DTO,
|
||||
TagtypesPostableTagDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
// Accept either a complete PostableDashboardV2 (flat shape with `spec` and
|
||||
// top-level `name` / `image` / `tags` / `schemaVersion`) or a bare spec — wrap
|
||||
// the latter with defaults so users can paste either shape that exists in the
|
||||
// wild (e.g. testdata/perses.json is a bare spec). The legacy nested
|
||||
// `{ metadata: { ... }, spec }` shape is also accepted and flattened.
|
||||
//
|
||||
// The backend requires `name` (immutable identifier); if the payload doesn't
|
||||
// carry one, fall back to `generateName: true` so the server assigns one.
|
||||
export function normalizeToPostable(
|
||||
parsed: Record<string, unknown>,
|
||||
): DashboardtypesPostableDashboardV2DTO {
|
||||
const hasSpec = 'spec' in parsed;
|
||||
const legacyMeta = parsed.metadata as
|
||||
| {
|
||||
schemaVersion?: string;
|
||||
name?: string;
|
||||
image?: string;
|
||||
tags?: TagtypesPostableTagDTO[] | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
const resolvedName = (parsed.name as string | undefined) ?? legacyMeta?.name;
|
||||
|
||||
if (hasSpec) {
|
||||
return {
|
||||
schemaVersion:
|
||||
(parsed.schemaVersion as string) || legacyMeta?.schemaVersion || 'v6',
|
||||
...(resolvedName ? { name: resolvedName } : { generateName: true }),
|
||||
image: (parsed.image as string) ?? legacyMeta?.image,
|
||||
tags:
|
||||
(parsed.tags as TagtypesPostableTagDTO[] | null) ??
|
||||
legacyMeta?.tags ??
|
||||
null,
|
||||
spec: parsed.spec as DashboardtypesDashboardSpecDTO,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
schemaVersion: 'v6',
|
||||
generateName: true,
|
||||
tags: null,
|
||||
spec: parsed as unknown as DashboardtypesDashboardSpecDTO,
|
||||
};
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
{
|
||||
"display": {
|
||||
"name": "NV dashboard with sections",
|
||||
"description": ""
|
||||
},
|
||||
"datasources": {
|
||||
"SigNozDatasource": {
|
||||
"default": true,
|
||||
"plugin": {
|
||||
"kind": "signoz/Datasource",
|
||||
"spec": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panels": {
|
||||
"b424e23b": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": ""
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/NumberPanel",
|
||||
"spec": {
|
||||
"formatting": {
|
||||
"unit": "s",
|
||||
"decimalPrecision": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "container.cpu.time",
|
||||
"reduceTo": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "rate"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"251df4d5": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": ""
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"fillSpans": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "recommendations",
|
||||
"decimalPrecision": "2"
|
||||
},
|
||||
"chartAppearance": {
|
||||
"lineInterpolation": "spline",
|
||||
"showPoints": false,
|
||||
"lineStyle": "solid",
|
||||
"fillMode": "none",
|
||||
"spanGaps": {"fillOnlyBelow": true}
|
||||
},
|
||||
"legend": {
|
||||
"position": "bottom"
|
||||
}
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "app_recommendations_counter",
|
||||
"reduceTo": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "rate"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"layouts": [
|
||||
{
|
||||
"kind": "Grid",
|
||||
"spec": {
|
||||
"display": {
|
||||
"title": "Bravo"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/b424e23b"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Grid",
|
||||
"spec": {
|
||||
"display": {
|
||||
"title": "Alpha"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/251df4d5"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
height: 44px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 6px 6px 0px 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.rightActions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
// Shared trigger button for the sort + configure-group icons in the right
|
||||
// actions cluster. Provides a square hover/active background so users know
|
||||
// which icon they're targeting.
|
||||
.iconTrigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&[aria-expanded='true'] {
|
||||
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.sortContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.sortHeading {
|
||||
color: var(--l3-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 12px 18px 6px 14px;
|
||||
}
|
||||
|
||||
.sortDivider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--l1-border);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.sortButton {
|
||||
text-align: start;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
padding: 12px 18px 12px 14px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.configureContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.configureItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: color-mix(in srgb, var(--l1-foreground) 10%, transparent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: color-mix(in srgb, var(--l1-foreground) 18%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.configureIcon {
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:global(.sortDashboardsPopover) {
|
||||
:global(.ant-popover-inner) {
|
||||
display: flex;
|
||||
padding: 0px;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.configureGroupPopover) {
|
||||
:global(.ant-popover-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
padding: 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { Button, Popover, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
ArrowDownWideNarrow,
|
||||
Check,
|
||||
Ellipsis,
|
||||
HdmiPort,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import type {
|
||||
SortColumn,
|
||||
SortOrder,
|
||||
} from '../../hooks/useDashboardsListQueryParams';
|
||||
|
||||
import styles from './ListHeader.module.scss';
|
||||
|
||||
interface Props {
|
||||
sortColumn: SortColumn;
|
||||
onSortChange: (column: SortColumn) => void;
|
||||
sortOrder: SortOrder;
|
||||
onOrderChange: (order: SortOrder) => void;
|
||||
onConfigureMetadata: () => void;
|
||||
}
|
||||
|
||||
function ListHeader({
|
||||
sortColumn,
|
||||
onSortChange,
|
||||
sortOrder,
|
||||
onOrderChange,
|
||||
onConfigureMetadata,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Typography.Text className={styles.label}>All Dashboards</Typography.Text>
|
||||
<section className={styles.rightActions}>
|
||||
<Tooltip title="Sort">
|
||||
<Popover
|
||||
trigger="click"
|
||||
content={
|
||||
<div className={styles.sortContent}>
|
||||
<Typography.Text className={styles.sortHeading}>
|
||||
Sort By
|
||||
</Typography.Text>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.sortButton}
|
||||
onClick={(): void => onSortChange('name')}
|
||||
data-testid="sort-by-name"
|
||||
>
|
||||
Name
|
||||
{sortColumn === 'name' && <Check size={14} />}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.sortButton}
|
||||
onClick={(): void => onSortChange('created_at')}
|
||||
data-testid="sort-by-last-created"
|
||||
>
|
||||
Last created
|
||||
{sortColumn === 'created_at' && <Check size={14} />}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.sortButton}
|
||||
onClick={(): void => onSortChange('updated_at')}
|
||||
data-testid="sort-by-last-updated"
|
||||
>
|
||||
Last updated
|
||||
{sortColumn === 'updated_at' && <Check size={14} />}
|
||||
</Button>
|
||||
<div className={styles.sortDivider} />
|
||||
<Typography.Text className={styles.sortHeading}>Order</Typography.Text>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.sortButton}
|
||||
onClick={(): void => onOrderChange('asc')}
|
||||
data-testid="sort-order-asc"
|
||||
>
|
||||
Ascending
|
||||
{sortOrder === 'asc' && <Check size={14} />}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.sortButton}
|
||||
onClick={(): void => onOrderChange('desc')}
|
||||
data-testid="sort-order-desc"
|
||||
>
|
||||
Descending
|
||||
{sortOrder === 'desc' && <Check size={14} />}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
rootClassName="sortDashboardsPopover"
|
||||
placement="bottomRight"
|
||||
arrow={false}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.iconTrigger}
|
||||
data-testid="sort-by"
|
||||
aria-label="Sort"
|
||||
>
|
||||
<ArrowDownWideNarrow size={14} />
|
||||
</button>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
trigger="click"
|
||||
content={
|
||||
<div className={styles.configureContent}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.configureItem}
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onConfigureMetadata();
|
||||
}}
|
||||
data-testid="configure-metadata-trigger"
|
||||
>
|
||||
<span className={styles.configureIcon}>
|
||||
<HdmiPort size={14} />
|
||||
</span>
|
||||
<span>Configure metadata</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
rootClassName="configureGroupPopover"
|
||||
placement="bottomRight"
|
||||
arrow={false}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.iconTrigger}
|
||||
aria-label="More options"
|
||||
>
|
||||
<Ellipsis size={14} />
|
||||
</button>
|
||||
</Popover>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListHeader;
|
||||
@@ -1,24 +0,0 @@
|
||||
.submit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ChangeEvent, KeyboardEvent, MouseEvent } from 'react';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { CornerDownLeft, Search } from '@signozhq/icons';
|
||||
|
||||
import styles from './SearchBar.module.scss';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
function SearchBar({ value, onChange, onSubmit }: Props): JSX.Element {
|
||||
return (
|
||||
<Input
|
||||
placeholder="Search with DSL (e.g. name CONTAINS 'foo')"
|
||||
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
||||
suffix={
|
||||
<button
|
||||
type="button"
|
||||
className={styles.submit}
|
||||
aria-label="Run search"
|
||||
data-testid="dashboards-list-search-submit"
|
||||
onMouseDown={(e: MouseEvent<HTMLButtonElement>): void => {
|
||||
// Prevent the input's blur from firing first and double-submitting.
|
||||
e.preventDefault();
|
||||
}}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
<CornerDownLeft size={12} color={Color.BG_VANILLA_400} />
|
||||
</button>
|
||||
}
|
||||
value={value}
|
||||
testId="dashboards-list-search"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||
onChange(e.target.value)
|
||||
}
|
||||
onBlur={onSubmit}
|
||||
onKeyDown={(e: KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key === 'Enter') {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchBar;
|
||||
@@ -1,40 +0,0 @@
|
||||
.wrapper {
|
||||
composes: cardWrapper from '../states.module.scss';
|
||||
padding: 105px 141px;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.noDashboard {
|
||||
composes: bodyText from '../states.module.scss';
|
||||
color: var(--l1-foreground);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.info {
|
||||
composes: bodyText from '../states.module.scss';
|
||||
color: var(--l2-foreground);
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.learnMore {
|
||||
composes: learnMoreLink from '../states.module.scss';
|
||||
}
|
||||
|
||||
.learnMoreArrow {
|
||||
composes: learnMoreArrow from '../states.module.scss';
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ArrowUpRight } from '@signozhq/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import dashboardsUrl from '@/assets/Icons/dashboards.svg';
|
||||
|
||||
import styles from './EmptyState.module.scss';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
interface Props {
|
||||
createDropdown?: ReactNode;
|
||||
}
|
||||
|
||||
const LEARN_MORE_HREF =
|
||||
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state';
|
||||
|
||||
function EmptyState({ createDropdown }: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<img src={dashboardsUrl} alt="dashboards" className={styles.image} />
|
||||
<section className={styles.copy}>
|
||||
<Typography.Text className={styles.noDashboard}>
|
||||
No dashboards yet.{' '}
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.info}>
|
||||
Create a dashboard to start visualizing your data
|
||||
</Typography.Text>
|
||||
</section>
|
||||
|
||||
{createDropdown ? (
|
||||
<section className={styles.actions}>
|
||||
{createDropdown}
|
||||
<Button
|
||||
variant="link"
|
||||
color="primary"
|
||||
className={styles.learnMore}
|
||||
testId="learn-more"
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List: Learn more clicked', {});
|
||||
openInNewTab(LEARN_MORE_HREF);
|
||||
}}
|
||||
>
|
||||
Learn more
|
||||
</Button>
|
||||
<ArrowUpRight size={16} className={styles.learnMoreArrow} />
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmptyState;
|
||||
@@ -1,36 +0,0 @@
|
||||
.wrapper {
|
||||
composes: cardWrapper from '../states.module.scss';
|
||||
padding: 105px 141px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
composes: bodyText from '../states.module.scss';
|
||||
color: var(--l1-foreground);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.errorDetail {
|
||||
composes: bodyText from '../states.module.scss';
|
||||
color: var(--l2-foreground);
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.learnMore {
|
||||
composes: learnMoreLink from '../states.module.scss';
|
||||
}
|
||||
|
||||
.learnMoreArrow {
|
||||
composes: learnMoreArrow from '../states.module.scss';
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ArrowUpRight, RotateCw } from '@signozhq/icons';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
|
||||
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
|
||||
|
||||
import { formatQueryErrorMessage } from '../../../utils';
|
||||
import styles from './ErrorState.module.scss';
|
||||
|
||||
interface Props {
|
||||
isCloudUser: boolean;
|
||||
onRetry: () => void;
|
||||
httpStatus?: number;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const GENERIC_MESSAGE =
|
||||
'Something went wrong :/ Please retry or contact support.';
|
||||
const INVALID_QUERY_FALLBACK = 'Please review the syntax and try again.';
|
||||
|
||||
function ErrorState({
|
||||
isCloudUser,
|
||||
onRetry,
|
||||
httpStatus,
|
||||
errorMessage,
|
||||
}: Props): JSX.Element {
|
||||
// 4xx responses are client errors — the same request will keep failing.
|
||||
// Surface the BE-provided detail (e.g. DSL parse errors) and skip Retry.
|
||||
const isClientError =
|
||||
httpStatus !== undefined && httpStatus >= 400 && httpStatus < 500;
|
||||
|
||||
const cleanedDetail = formatQueryErrorMessage(errorMessage);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<img src={awwSnapUrl} alt="something went wrong" className={styles.img} />
|
||||
|
||||
{isClientError ? (
|
||||
<>
|
||||
<Typography.Text className={styles.errorText}>
|
||||
Invalid query
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.errorDetail}>
|
||||
{cleanedDetail || INVALID_QUERY_FALLBACK}
|
||||
</Typography.Text>
|
||||
</>
|
||||
) : (
|
||||
<Typography.Text className={styles.errorText}>
|
||||
{GENERIC_MESSAGE}
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
<section className={styles.actionButtons}>
|
||||
{!isClientError && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
prefix={<RotateCw size={16} />}
|
||||
onClick={onRetry}
|
||||
testId="dashboards-list-retry"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="link"
|
||||
color="primary"
|
||||
className={styles.learnMore}
|
||||
onClick={(): void => handleContactSupport(isCloudUser)}
|
||||
testId="dashboards-list-contact-support"
|
||||
>
|
||||
Contact Support
|
||||
</Button>
|
||||
<ArrowUpRight size={16} className={styles.learnMoreArrow} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorState;
|
||||
@@ -1,11 +0,0 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
height: 125px;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Skeleton } from 'antd';
|
||||
|
||||
import styles from './LoadingState.module.scss';
|
||||
|
||||
function LoadingState(): JSX.Element {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingState;
|
||||
@@ -1,5 +0,0 @@
|
||||
.wrapper {
|
||||
composes: cardWrapper from '../states.module.scss';
|
||||
padding: 105px 190px;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
|
||||
|
||||
import styles from './NoResultsState.module.scss';
|
||||
|
||||
interface Props {
|
||||
searchString: string;
|
||||
}
|
||||
|
||||
function NoResultsState({ searchString }: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<img src={emptyStateUrl} alt="img" height={32} width={32} />
|
||||
<Typography.Text>
|
||||
No dashboards found for {searchString}. Create a new dashboard?
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoResultsState;
|
||||
@@ -1,34 +0,0 @@
|
||||
// Shared building blocks for the dashboards-list view states.
|
||||
// Composed via CSS-modules `composes:` from each state's own SCSS.
|
||||
|
||||
.cardWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 320px;
|
||||
margin-top: 16px;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--l1-border);
|
||||
}
|
||||
|
||||
.bodyText {
|
||||
font-family: Inter;
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.learnMoreLink {
|
||||
composes: bodyText;
|
||||
color: var(--bg-robin-400);
|
||||
font-weight: var(--font-weight-medium);
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.learnMoreArrow {
|
||||
margin-left: -20px;
|
||||
color: var(--bg-robin-400);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import {
|
||||
parseAsInteger,
|
||||
parseAsString,
|
||||
parseAsStringLiteral,
|
||||
useQueryState,
|
||||
type Options,
|
||||
type UseQueryStateReturn,
|
||||
} from 'nuqs';
|
||||
|
||||
export const SORT_COLUMNS = ['updated_at', 'created_at', 'name'] as const;
|
||||
export type SortColumn = (typeof SORT_COLUMNS)[number];
|
||||
|
||||
export const SORT_ORDERS = ['asc', 'desc'] as const;
|
||||
export type SortOrder = (typeof SORT_ORDERS)[number];
|
||||
|
||||
const opts: Options = { history: 'push' };
|
||||
|
||||
export const useSortColumn = (): UseQueryStateReturn<SortColumn, SortColumn> =>
|
||||
useQueryState(
|
||||
'sort',
|
||||
parseAsStringLiteral(SORT_COLUMNS)
|
||||
.withDefault('updated_at')
|
||||
.withOptions(opts),
|
||||
);
|
||||
|
||||
export const useSortOrder = (): UseQueryStateReturn<SortOrder, SortOrder> =>
|
||||
useQueryState(
|
||||
'order',
|
||||
parseAsStringLiteral(SORT_ORDERS).withDefault('desc').withOptions(opts),
|
||||
);
|
||||
|
||||
export const usePage = (): UseQueryStateReturn<number, number> =>
|
||||
useQueryState('page', parseAsInteger.withDefault(1).withOptions(opts));
|
||||
|
||||
export const useSearch = (): UseQueryStateReturn<string, string> =>
|
||||
useQueryState('search', parseAsString.withDefault('').withOptions(opts));
|
||||
@@ -1,3 +1,9 @@
|
||||
import DashboardsListPageV2 from './DashboardsListPageV2';
|
||||
function DashboardsListPageV2(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboards List Page V2</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsListPageV2;
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import type { DashboardtypesGettableDashboardWithPinDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export type DashboardListItem = DashboardtypesGettableDashboardWithPinDTO;
|
||||
|
||||
export const tagsToStrings = (
|
||||
tags: { key: string; value: string }[] | null | undefined,
|
||||
): string[] =>
|
||||
(tags ?? []).map((tag) =>
|
||||
tag.key === tag.value ? tag.key : `${tag.key}:${tag.value}`,
|
||||
);
|
||||
|
||||
export const lastUpdatedLabel = (time: string | undefined): string => {
|
||||
if (!time || isEmpty(time)) {
|
||||
return 'No updates yet!';
|
||||
}
|
||||
const diff = dayjs();
|
||||
const ref = dayjs(time);
|
||||
const months = diff.diff(ref, 'months');
|
||||
if (months > 0) {
|
||||
return `Last Updated ${months} months ago`;
|
||||
}
|
||||
const days = diff.diff(ref, 'days');
|
||||
if (days > 0) {
|
||||
return `Last Updated ${days} days ago`;
|
||||
}
|
||||
const hours = diff.diff(ref, 'hours');
|
||||
if (hours > 0) {
|
||||
return `Last Updated ${hours} hrs ago`;
|
||||
}
|
||||
const minutes = diff.diff(ref, 'minutes');
|
||||
if (minutes > 0) {
|
||||
return `Last Updated ${minutes} mins ago`;
|
||||
}
|
||||
const seconds = diff.diff(ref, 'seconds');
|
||||
return `Last Updated ${seconds} sec ago`;
|
||||
};
|
||||
|
||||
// Normalize BE query-parse error messages for display:
|
||||
// - Drop the "invalid filter query:" prefix (the UI already says "Invalid query").
|
||||
// - Backticks → double quotes for the format hint that follows the em-dash.
|
||||
// - Trim surrounding whitespace.
|
||||
export const formatQueryErrorMessage = (raw: string | undefined): string => {
|
||||
if (!raw) {
|
||||
return '';
|
||||
}
|
||||
return raw
|
||||
.replace(/^invalid filter query:\s*/i, '')
|
||||
.replace(/`([^`]+)`/g, '"$1"')
|
||||
.trim();
|
||||
};
|
||||
@@ -48,9 +48,7 @@
|
||||
"node_modules",
|
||||
"src/parser/*.ts",
|
||||
"src/parser/TraceOperatorParser/*.ts",
|
||||
"orval.config.ts",
|
||||
"src/pages/DashboardsListPageV2/**/*",
|
||||
"src/pages/DashboardPageV2/**/*"
|
||||
"orval.config.ts"
|
||||
],
|
||||
"include": [
|
||||
"./src",
|
||||
|
||||
@@ -679,7 +679,21 @@ func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*qbtypes.Cac
|
||||
|
||||
// deduplicateWarnings removes duplicate warnings.
|
||||
func (bc *bucketCache) deduplicateWarnings(warnings []string) []string {
|
||||
return dedupeWarnings(warnings)
|
||||
if len(warnings) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]bool, len(warnings))
|
||||
unique := make([]string, 0, len(warnings)) // Pre-allocate capacity
|
||||
|
||||
for _, warning := range warnings {
|
||||
if !seen[warning] {
|
||||
seen[warning] = true
|
||||
unique = append(unique, warning)
|
||||
}
|
||||
}
|
||||
|
||||
return unique
|
||||
}
|
||||
|
||||
// trimResultToFluxBoundary trims the result to exclude data points beyond the flux boundary.
|
||||
|
||||
@@ -7,12 +7,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
type SkipResourceFingerprint struct {
|
||||
Enabled bool `yaml:"enabled" mapstructure:"enabled"`
|
||||
// If count of fingerprint is above threshold, skip the fingerprint subquery and filter on main table instead.
|
||||
Threshold uint64 `yaml:"threshold" mapstructure:"threshold"`
|
||||
}
|
||||
|
||||
// Config represents the configuration for the querier.
|
||||
type Config struct {
|
||||
// CacheTTL is the TTL for cached query results
|
||||
@@ -21,8 +15,6 @@ type Config struct {
|
||||
FluxInterval time.Duration `yaml:"flux_interval" mapstructure:"flux_interval"`
|
||||
// MaxConcurrentQueries is the maximum number of concurrent queries for missing ranges
|
||||
MaxConcurrentQueries int `yaml:"max_concurrent_queries" mapstructure:"max_concurrent_queries"`
|
||||
// SkipResourceFingerprint configures when the resource fingerprint subquery is skipped in favor of main-table filtering.
|
||||
SkipResourceFingerprint SkipResourceFingerprint `yaml:"skip_resource_fingerprint" mapstructure:"skip_resource_fingerprint"`
|
||||
}
|
||||
|
||||
// NewConfigFactory creates a new config factory for querier.
|
||||
@@ -36,10 +28,6 @@ func newConfig() factory.Config {
|
||||
CacheTTL: 168 * time.Hour,
|
||||
FluxInterval: 5 * time.Minute,
|
||||
MaxConcurrentQueries: 4,
|
||||
SkipResourceFingerprint: SkipResourceFingerprint{
|
||||
Enabled: false,
|
||||
Threshold: 100000,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +42,6 @@ func (c Config) Validate() error {
|
||||
if c.MaxConcurrentQueries <= 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "max_concurrent_queries must be positive, got %v", c.MaxConcurrentQueries)
|
||||
}
|
||||
if c.SkipResourceFingerprint.Enabled && c.SkipResourceFingerprint.Threshold == 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "skip_resource_fingerprint.threshold must be > 0 when enabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -642,12 +642,6 @@ func (q *querier) run(
|
||||
},
|
||||
}
|
||||
|
||||
// Warnings can arrive duplicated: the bucket cache returns the cached
|
||||
// portion's warnings alongside an identical warning emitted by every
|
||||
// freshly-executed missing range (see mergeResults), and distinct queries
|
||||
// can surface the same warning. Collapse exact duplicates before building
|
||||
// the response.
|
||||
warnings = dedupeWarnings(warnings)
|
||||
if len(warnings) != 0 {
|
||||
warns := make([]qbtypes.QueryWarnDataAdditional, len(warnings))
|
||||
for i, warning := range warnings {
|
||||
@@ -1131,8 +1125,6 @@ func (q *querier) adjustStepInterval(queries []qbtypes.QueryEnvelope, start, end
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
||||
if qe.GetSource() == telemetrytypes.SourceMeter {
|
||||
clampStep(qe, meterRecommended, meterMin, &warnings)
|
||||
// we don't want to return warnings for meter metrics.
|
||||
warnings = nil
|
||||
} else {
|
||||
clampStep(qe, metricRecommended, metricMin, &warnings)
|
||||
}
|
||||
@@ -1148,21 +1140,3 @@ func (q *querier) adjustStepInterval(queries []qbtypes.QueryEnvelope, start, end
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
// dedupeWarnings removes exact-duplicate warning messages while preserving the
|
||||
// order of first occurrence. Returns nil for an empty input. Warning counts are
|
||||
// tiny (a handful per request), so a linear scan beats the allocation and
|
||||
// hashing overhead of a map.
|
||||
func dedupeWarnings(warnings []string) []string {
|
||||
if len(warnings) == 0 {
|
||||
return nil
|
||||
}
|
||||
unique := make([]string, 0, len(warnings))
|
||||
// N^2 is faster than map-based deduping for small warning counts, and it preserves order of first occurrence without extra bookkeeping.
|
||||
for _, warning := range warnings {
|
||||
if !slices.Contains(unique, warning) {
|
||||
unique = append(unique, warning)
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
@@ -88,8 +88,6 @@ func newProvider(
|
||||
traceAggExprRewriter,
|
||||
telemetryStore,
|
||||
flagger,
|
||||
cfg.SkipResourceFingerprint.Enabled,
|
||||
cfg.SkipResourceFingerprint.Threshold,
|
||||
)
|
||||
|
||||
// Create trace operator statement builder
|
||||
@@ -123,9 +121,6 @@ func newProvider(
|
||||
telemetrylogs.DefaultFullTextColumn,
|
||||
telemetrylogs.GetBodyJSONKey,
|
||||
flagger,
|
||||
telemetryStore,
|
||||
cfg.SkipResourceFingerprint.Enabled,
|
||||
cfg.SkipResourceFingerprint.Threshold,
|
||||
)
|
||||
|
||||
// Create audit statement builder
|
||||
|
||||
@@ -89,9 +89,6 @@ func prepareQuerierForLogs(t *testing.T, telemetryStore telemetrystore.Telemetry
|
||||
telemetrylogs.DefaultFullTextColumn,
|
||||
telemetrylogs.GetBodyJSONKey,
|
||||
fl,
|
||||
nil,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
return querier.New(
|
||||
@@ -137,8 +134,6 @@ func prepareQuerierForTraces(t *testing.T, telemetryStore telemetrystore.Telemet
|
||||
traceAggExprRewriter,
|
||||
telemetryStore,
|
||||
fl,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
return querier.New(
|
||||
|
||||
@@ -127,7 +127,7 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Limit: 100,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'signoz.audit.resource.id') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'signoz.audit.resource.id') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", "019b-5678-efgh-9012", "%signoz.audit.resource.id%", "%signoz.audit.resource.id\":\"019b-5678-efgh-9012%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
@@ -144,7 +144,7 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Limit: 100,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "delete", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1205,9 +1205,6 @@ func buildJSONTestStatementBuilder(t *testing.T, addIndexes bool) (*logQueryStat
|
||||
DefaultFullTextColumn,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
nil,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
return statementBuilder, mockMetadataStore
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetryresourcefilter"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
@@ -20,14 +19,13 @@ import (
|
||||
)
|
||||
|
||||
type logQueryStatementBuilder struct {
|
||||
logger *slog.Logger
|
||||
metadataStore telemetrytypes.MetadataStore
|
||||
fm qbtypes.FieldMapper
|
||||
cb qbtypes.ConditionBuilder
|
||||
resourceFilterResolver *telemetryresourcefilter.ResourceFingerprintResolver[qbtypes.LogAggregation]
|
||||
aggExprRewriter qbtypes.AggExprRewriter
|
||||
fl flagger.Flagger
|
||||
skipResourceFingerprintEnabled bool
|
||||
logger *slog.Logger
|
||||
metadataStore telemetrytypes.MetadataStore
|
||||
fm qbtypes.FieldMapper
|
||||
cb qbtypes.ConditionBuilder
|
||||
resourceFilterStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
|
||||
aggExprRewriter qbtypes.AggExprRewriter
|
||||
fl flagger.Flagger
|
||||
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
|
||||
@@ -44,13 +42,10 @@ func NewLogQueryStatementBuilder(
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey,
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
|
||||
fl flagger.Flagger,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
skipResourceFingerprintEnable bool,
|
||||
skipResourceFingerprintThreshold uint64,
|
||||
) *logQueryStatementBuilder {
|
||||
logsSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrylogs")
|
||||
|
||||
resourceFilterResolver := telemetryresourcefilter.NewResolver[qbtypes.LogAggregation](
|
||||
resourceFilterStmtBuilder := telemetryresourcefilter.New[qbtypes.LogAggregation](
|
||||
settings,
|
||||
DBName,
|
||||
LogsResourceV2TableName,
|
||||
@@ -60,21 +55,18 @@ func NewLogQueryStatementBuilder(
|
||||
fullTextColumn,
|
||||
jsonKeyToKey,
|
||||
fl,
|
||||
telemetryStore,
|
||||
skipResourceFingerprintThreshold,
|
||||
)
|
||||
|
||||
return &logQueryStatementBuilder{
|
||||
logger: logsSettings.Logger(),
|
||||
metadataStore: metadataStore,
|
||||
fm: fieldMapper,
|
||||
cb: conditionBuilder,
|
||||
resourceFilterResolver: resourceFilterResolver,
|
||||
aggExprRewriter: aggExprRewriter,
|
||||
fl: fl,
|
||||
skipResourceFingerprintEnabled: skipResourceFingerprintEnable,
|
||||
fullTextColumn: fullTextColumn,
|
||||
jsonKeyToKey: jsonKeyToKey,
|
||||
logger: logsSettings.Logger(),
|
||||
metadataStore: metadataStore,
|
||||
fm: fieldMapper,
|
||||
cb: conditionBuilder,
|
||||
resourceFilterStmtBuilder: resourceFilterStmtBuilder,
|
||||
aggExprRewriter: aggExprRewriter,
|
||||
fl: fl,
|
||||
fullTextColumn: fullTextColumn,
|
||||
jsonKeyToKey: jsonKeyToKey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,11 +271,9 @@ func (b *logQueryStatementBuilder) buildListQuery(
|
||||
bodyJSONEnabled = b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
)
|
||||
|
||||
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
|
||||
if err != nil {
|
||||
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if frag != "" {
|
||||
} else if frag != "" {
|
||||
cteFragments = append(cteFragments, frag)
|
||||
cteArgs = append(cteArgs, args)
|
||||
}
|
||||
@@ -325,7 +315,7 @@ func (b *logQueryStatementBuilder) buildListQuery(
|
||||
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
|
||||
// Add filter conditions
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -383,11 +373,9 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
bodyJSONEnabled = b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
)
|
||||
|
||||
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
|
||||
if err != nil {
|
||||
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if frag != "" {
|
||||
} else if frag != "" {
|
||||
cteFragments = append(cteFragments, frag)
|
||||
cteArgs = append(cteArgs, args)
|
||||
}
|
||||
@@ -431,7 +419,7 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
// Add FROM clause
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
|
||||
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -543,11 +531,9 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
|
||||
bodyJSONEnabled = b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
)
|
||||
|
||||
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
|
||||
if err != nil {
|
||||
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if frag != "" && !skipResourceCTE {
|
||||
} else if frag != "" && !skipResourceCTE {
|
||||
cteFragments = append(cteFragments, frag)
|
||||
cteArgs = append(cteArgs, args)
|
||||
}
|
||||
@@ -590,7 +576,7 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
|
||||
|
||||
// Add filter conditions
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -654,7 +640,6 @@ func (b *logQueryStatementBuilder) addFilterCondition(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
skipResourceFilter bool,
|
||||
) (querybuilder.PreparedWhereClause, error) {
|
||||
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
@@ -671,7 +656,7 @@ func (b *logQueryStatementBuilder) addFilterCondition(
|
||||
ConditionBuilder: b.cb,
|
||||
FieldKeys: keys,
|
||||
BodyJSONEnabled: bodyJSONEnabled,
|
||||
SkipResourceFilter: skipResourceFilter,
|
||||
SkipResourceFilter: true,
|
||||
FullTextColumn: b.fullTextColumn,
|
||||
JsonKeyToKey: b.jsonKeyToKey,
|
||||
Variables: variables,
|
||||
@@ -722,30 +707,33 @@ func (b *logQueryStatementBuilder) maybeAttachResourceFilter(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
|
||||
start, end uint64,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (cteSQL string, cteArgs []any, skipResourceFilter bool, err error) {
|
||||
) (cteSQL string, cteArgs []any, err error) {
|
||||
|
||||
if b.skipResourceFingerprintEnabled {
|
||||
decision, err := b.resourceFilterResolver.Resolve(ctx, query, start, end, variables)
|
||||
if err != nil {
|
||||
return "", nil, true, err
|
||||
}
|
||||
switch decision {
|
||||
case qbtypes.ResourceFilterResolveKindNoOp:
|
||||
return "", nil, true, nil
|
||||
case qbtypes.ResourceFilterResolveKindFallback:
|
||||
return "", nil, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
stmt, err := b.resourceFilterResolver.StatementBuilder().Build(
|
||||
ctx, start, end, qbtypes.RequestTypeRaw, query, variables,
|
||||
)
|
||||
stmt, err := b.buildResourceFilterCTE(ctx, query, start, end, variables)
|
||||
if err != nil {
|
||||
return "", nil, true, err
|
||||
return "", nil, err
|
||||
}
|
||||
if stmt == nil {
|
||||
return "", nil, true, nil
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
sb.Where("resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
|
||||
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, true, nil
|
||||
|
||||
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, nil
|
||||
}
|
||||
|
||||
func (b *logQueryStatementBuilder) buildResourceFilterCTE(
|
||||
ctx context.Context,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
|
||||
start, end uint64,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (*qbtypes.Statement, error) {
|
||||
return b.resourceFilterStmtBuilder.Build(
|
||||
ctx,
|
||||
start,
|
||||
end,
|
||||
qbtypes.RequestTypeRaw,
|
||||
query,
|
||||
variables,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,36 +2,19 @@ package telemetrylogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cmock "github.com/SigNoz/clickhouse-go-mock"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type regexQueryMatcher struct{}
|
||||
|
||||
func (m *regexQueryMatcher) Match(expectedSQL, actualSQL string) error {
|
||||
re, err := regexp.Compile(expectedSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !re.MatchString(actualSQL) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "expected query to match %s, got %s", expectedSQL, actualSQL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestStatementBuilderTimeSeries(t *testing.T) {
|
||||
// Create a test release time
|
||||
releaseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
|
||||
@@ -72,7 +55,7 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS `service.name`, countDistinct(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS `service.name`, countDistinct(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS `service.name`, countDistinct(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS `service.name`, countDistinct(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
||||
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1705397400), uint64(1705485600), "1705399200000000000", uint64(1705397400), "1705485600000000000", uint64(1705485600), 10, "1705399200000000000", uint64(1705397400), "1705485600000000000", uint64(1705485600)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -144,7 +127,7 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(resource.`service.name`::String IS NOT NULL, resource.`service.name`::String, NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
|
||||
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1705397400), uint64(1705485600), "1705399200000000000", uint64(1705397400), "1705485600000000000", uint64(1705485600), 10, "1705399200000000000", uint64(1705397400), "1705485600000000000", uint64(1705485600)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -177,7 +160,7 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(`attribute_string_materialized$$key$$name_exists` = ?, `attribute_string_materialized$$key$$name`, NULL)) AS `materialized.key.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `materialized.key.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(`attribute_string_materialized$$key$$name_exists` = ?, `attribute_string_materialized$$key$$name`, NULL)) AS `materialized.key.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`materialized.key.name`) GLOBAL IN (SELECT `materialized.key.name` FROM __limit_cte) GROUP BY ts, `materialized.key.name`",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(`attribute_string_materialized$$key$$name_exists` = ?, `attribute_string_materialized$$key$$name`, NULL)) AS `materialized.key.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `materialized.key.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(`attribute_string_materialized$$key$$name_exists` = ?, `attribute_string_materialized$$key$$name`, NULL)) AS `materialized.key.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`materialized.key.name`) GLOBAL IN (SELECT `materialized.key.name` FROM __limit_cte) GROUP BY ts, `materialized.key.name`",
|
||||
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1705397400), uint64(1705485600), true, "1705399200000000000", uint64(1705397400), "1705485600000000000", uint64(1705485600), 10, true, "1705399200000000000", uint64(1705397400), "1705485600000000000", uint64(1705485600)},
|
||||
},
|
||||
},
|
||||
@@ -229,9 +212,6 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
|
||||
DefaultFullTextColumn,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
nil,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -273,7 +253,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -301,7 +281,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?",
|
||||
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -373,9 +353,6 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
DefaultFullTextColumn,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
nil,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -447,7 +424,7 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND match(LOWER(body), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND match(LOWER(body), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?",
|
||||
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "hello", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -522,9 +499,6 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) {
|
||||
DefaultFullTextColumn,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
nil,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -601,9 +575,6 @@ func TestStatementBuilderTimeSeriesBodyGroupBy(t *testing.T) {
|
||||
DefaultFullTextColumn,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
nil,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -644,7 +615,7 @@ func TestStatementBuilderListQueryServiceCollision(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (LOWER(body) LIKE LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (LOWER(body) LIKE LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "%error%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -673,7 +644,7 @@ func TestStatementBuilderListQueryServiceCollision(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND LOWER(body) LIKE LOWER(?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND LOWER(body) LIKE LOWER(?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?",
|
||||
Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "%error%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -699,9 +670,6 @@ func TestStatementBuilderListQueryServiceCollision(t *testing.T) {
|
||||
DefaultFullTextColumn,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
nil,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -926,9 +894,6 @@ func TestAdjustKey(t *testing.T) {
|
||||
DefaultFullTextColumn,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
nil,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -1074,9 +1039,6 @@ func TestStmtBuilderBodyField(t *testing.T) {
|
||||
DefaultFullTextColumn,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
nil,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
|
||||
@@ -1176,9 +1138,6 @@ func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
|
||||
DefaultFullTextColumn,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
nil,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
|
||||
@@ -1198,110 +1157,3 @@ func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSkipResourceFingerprintLogs exercises the three resolver outcomes for
|
||||
// logs: use-CTE (count < threshold), fallback (count >= threshold), and the
|
||||
// legacy path (feature disabled).
|
||||
func TestSkipResourceFingerprintLogs(t *testing.T) {
|
||||
const (
|
||||
startMs = uint64(1747947419000)
|
||||
endMs = uint64(1747983448000)
|
||||
threshold = uint64(10)
|
||||
)
|
||||
|
||||
query := qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name = 'redis-manual'",
|
||||
},
|
||||
Limit: 5,
|
||||
}
|
||||
|
||||
t.Run("disabled uses the legacy CTE", func(t *testing.T) {
|
||||
sb := newSkipResourceFingerprintLogsBuilder(t, nil, false, threshold)
|
||||
|
||||
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, stmt.Query, "__resource_filter AS (SELECT fingerprint")
|
||||
require.Contains(t, stmt.Query, "resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
|
||||
})
|
||||
|
||||
t.Run("CTE attached when count below threshold", func(t *testing.T) {
|
||||
mockStore := telemetrystoretest.New(telemetrystore.Config{}, ®exQueryMatcher{})
|
||||
mock := mockStore.Mock()
|
||||
|
||||
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_logs\.distributed_logs_v2_resource`).
|
||||
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
|
||||
{Name: "count", Type: "UInt64"},
|
||||
}, []any{uint64(2)}))
|
||||
|
||||
sb := newSkipResourceFingerprintLogsBuilder(t, mockStore, true, threshold)
|
||||
|
||||
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, stmt.Query, "__resource_filter AS (SELECT fingerprint")
|
||||
require.Contains(t, stmt.Query, "resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
|
||||
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("fallback when count at or above threshold", func(t *testing.T) {
|
||||
mockStore := telemetrystoretest.New(telemetrystore.Config{}, ®exQueryMatcher{})
|
||||
mock := mockStore.Mock()
|
||||
|
||||
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_logs\.distributed_logs_v2_resource`).
|
||||
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
|
||||
{Name: "count", Type: "UInt64"},
|
||||
}, []any{threshold}))
|
||||
|
||||
sb := newSkipResourceFingerprintLogsBuilder(t, mockStore, true, threshold)
|
||||
|
||||
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotContains(t, stmt.Query, "__resource_filter AS")
|
||||
require.NotContains(t, stmt.Query, "resource_fingerprint")
|
||||
require.Contains(t, stmt.Query, "service.name")
|
||||
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
|
||||
func newSkipResourceFingerprintLogsBuilder(
|
||||
t *testing.T,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
skipEnable bool,
|
||||
threshold uint64,
|
||||
) *logQueryStatementBuilder {
|
||||
t.Helper()
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
fm := NewFieldMapper(fl)
|
||||
cb := NewConditionBuilder(fm, fl)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = buildCompleteFieldKeyMap(time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC))
|
||||
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(
|
||||
instrumentationtest.New().ToProviderSettings(),
|
||||
DefaultFullTextColumn,
|
||||
fm,
|
||||
cb,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
)
|
||||
|
||||
return NewLogQueryStatementBuilder(
|
||||
instrumentationtest.New().ToProviderSettings(),
|
||||
mockMetadataStore,
|
||||
fm,
|
||||
cb,
|
||||
aggExprRewriter,
|
||||
DefaultFullTextColumn,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
telemetryStore,
|
||||
skipEnable,
|
||||
threshold,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package telemetryresourcefilter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
type ResourceFingerprintResolver[T any] struct {
|
||||
stmtBuilder *resourceFilterStatementBuilder[T]
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
threshold uint64
|
||||
}
|
||||
|
||||
func NewResolver[T any](
|
||||
settings factory.ProviderSettings,
|
||||
dbName string,
|
||||
tableName string,
|
||||
signal telemetrytypes.Signal,
|
||||
source telemetrytypes.Source,
|
||||
metadataStore telemetrytypes.MetadataStore,
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey,
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
|
||||
fl flagger.Flagger,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
threshold uint64,
|
||||
) *ResourceFingerprintResolver[T] {
|
||||
return &ResourceFingerprintResolver[T]{
|
||||
stmtBuilder: New[T](
|
||||
settings,
|
||||
dbName,
|
||||
tableName,
|
||||
signal,
|
||||
source,
|
||||
metadataStore,
|
||||
fullTextColumn,
|
||||
jsonKeyToKey,
|
||||
fl,
|
||||
),
|
||||
telemetryStore: telemetryStore,
|
||||
threshold: threshold,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ResourceFingerprintResolver[T]) StatementBuilder() qbtypes.StatementBuilder[T] {
|
||||
return r.stmtBuilder
|
||||
}
|
||||
|
||||
func (r *ResourceFingerprintResolver[T]) Resolve(
|
||||
ctx context.Context,
|
||||
query qbtypes.QueryBuilderQuery[T],
|
||||
start, end uint64,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (qbtypes.ResourceFilterResolveKind, error) {
|
||||
countStmt, err := r.stmtBuilder.BuildCount(ctx, start, end, query, variables)
|
||||
if err != nil {
|
||||
return qbtypes.ResourceFilterResolveKindNoOp, err
|
||||
}
|
||||
if countStmt == nil {
|
||||
return qbtypes.ResourceFilterResolveKindNoOp, nil
|
||||
}
|
||||
|
||||
var count uint64
|
||||
row := r.telemetryStore.ClickhouseDB().QueryRow(ctx, countStmt.Query, countStmt.Args...)
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return qbtypes.ResourceFilterResolveKindNoOp, err
|
||||
}
|
||||
|
||||
if count >= r.threshold {
|
||||
return qbtypes.ResourceFilterResolveKindFallback, nil
|
||||
}
|
||||
return qbtypes.ResourceFilterResolveKindUseCTE, nil
|
||||
}
|
||||
@@ -116,10 +116,6 @@ func (b *resourceFilterStatementBuilder[T]) Build(
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
// Group by fingerprint instead of using DISTINCT; on ClickHouse GROUP BY
|
||||
// parallelizes across multiple threads and is faster for deduplication.
|
||||
q.GroupBy("fingerprint")
|
||||
|
||||
stmt, args := q.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return &qbtypes.Statement{
|
||||
Query: stmt,
|
||||
@@ -127,25 +123,6 @@ func (b *resourceFilterStatementBuilder[T]) Build(
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuildCount returns a statement that counts the distinct fingerprints matching
|
||||
// the resource filter. Returns (nil, nil) when the filter is a no-op.
|
||||
func (b *resourceFilterStatementBuilder[T]) BuildCount(
|
||||
ctx context.Context,
|
||||
start uint64,
|
||||
end uint64,
|
||||
query qbtypes.QueryBuilderQuery[T],
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (*qbtypes.Statement, error) {
|
||||
inner, err := b.Build(ctx, start, end, qbtypes.RequestTypeRaw, query, variables)
|
||||
if err != nil || inner == nil {
|
||||
return nil, err
|
||||
}
|
||||
return &qbtypes.Statement{
|
||||
Query: fmt.Sprintf("SELECT count() FROM (%s)", inner.Query),
|
||||
Args: inner.Args,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// addConditions adds both filter and time conditions to the query.
|
||||
// Returns true (isNoOp) when the filter expression evaluated to no resource conditions,
|
||||
// meaning the CTE would select all fingerprints and should be skipped entirely.
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -134,7 +134,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'k8s.namespace.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'k8s.namespace.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", "production", "%k8s.namespace.name%", "%k8s.namespace.name\":\"production%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -183,7 +183,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (LOWER(simpleJSONExtractString(labels, 'service.name')) LIKE LOWER(?) AND labels LIKE ? AND LOWER(labels) LIKE LOWER(?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (LOWER(simpleJSONExtractString(labels, 'service.name')) LIKE LOWER(?) AND labels LIKE ? AND LOWER(labels) LIKE LOWER(?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"redis%", "%service.name%", "%service.name%redis%%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -198,7 +198,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONHas(labels, 'service.name') = ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONHas(labels, 'service.name') = ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{true, "%service.name%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -213,7 +213,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONHas(labels, 'service.name') <> ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONHas(labels, 'service.name') <> ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{true, expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -228,7 +228,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? OR simpleJSONExtractString(labels, 'service.name') = ?) AND labels LIKE ? AND (labels LIKE ? OR labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? OR simpleJSONExtractString(labels, 'service.name') = ?) AND labels LIKE ? AND (labels LIKE ? OR labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"redis", "postgres", "%service.name%", "%service.name\":\"redis%", "%service.name\":\"postgres%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -243,7 +243,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') <> ? AND simpleJSONExtractString(labels, 'service.name') <> ?) AND (labels NOT LIKE ? AND labels NOT LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') <> ? AND simpleJSONExtractString(labels, 'service.name') <> ?) AND (labels NOT LIKE ? AND labels NOT LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"redis", "postgres", "%service.name\":\"redis%", "%service.name\":\"postgres%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -258,7 +258,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (LOWER(simpleJSONExtractString(labels, 'service.name')) LIKE LOWER(?) AND labels LIKE ? AND LOWER(labels) LIKE LOWER(?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (LOWER(simpleJSONExtractString(labels, 'service.name')) LIKE LOWER(?) AND labels LIKE ? AND LOWER(labels) LIKE LOWER(?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"%redis%", "%service.name%", "%service.name%redis%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -273,7 +273,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (match(simpleJSONExtractString(labels, 'service.name'), ?) AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (match(simpleJSONExtractString(labels, 'service.name'), ?) AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"redis.*", "%service.name%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -288,7 +288,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') <> ? AND labels NOT LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') <> ? AND labels NOT LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"redis", "%service.name\":\"redis%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -315,7 +315,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: 0,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ?",
|
||||
Args: []any{"redis", "%service.name%", "%service.name\":\"redis%", expectedBucketStart},
|
||||
},
|
||||
},
|
||||
@@ -330,7 +330,7 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE NOT (((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?))) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE NOT (((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?))) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"redis", "%service.name%", "%service.name\":\"redis%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -406,7 +406,7 @@ func TestResourceFilterStatementBuilder_Logs(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -443,7 +443,7 @@ func TestResourceFilterStatementBuilder_Logs(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'k8s.namespace.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'k8s.namespace.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"redis", "%service.name%", "%service.name\":\"redis%", "default", "%k8s.namespace.name%", "%k8s.namespace.name\":\"default%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
@@ -461,7 +461,7 @@ func TestResourceFilterStatementBuilder_Logs(t *testing.T) {
|
||||
start: uint64(1769976178000000000), // These will give bucket start 1769974378 and end 1770062578
|
||||
end: uint64(1770062578000000000),
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'env') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'k8s.deployment.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'env') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'k8s.deployment.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"prod", "%env%", "%env\":\"prod%", "prod-deployment", "%k8s.deployment.name%", "%k8s.deployment.name\":\"prod-deployment%", uint64(1769974378), uint64(1770062578)},
|
||||
},
|
||||
},
|
||||
@@ -606,7 +606,7 @@ func TestResourceFilterStatementBuilder_Variables(t *testing.T) {
|
||||
start: testStartNs,
|
||||
end: testEndNs,
|
||||
expected: &qbtypes.Statement{
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint",
|
||||
Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", expectedBucketStart, expectedBucketEnd},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,13 +24,13 @@ var (
|
||||
)
|
||||
|
||||
type traceQueryStatementBuilder struct {
|
||||
logger *slog.Logger
|
||||
metadataStore telemetrytypes.MetadataStore
|
||||
fm qbtypes.FieldMapper
|
||||
cb qbtypes.ConditionBuilder
|
||||
resourceFilterResolver *telemetryresourcefilter.ResourceFingerprintResolver[qbtypes.TraceAggregation]
|
||||
aggExprRewriter qbtypes.AggExprRewriter
|
||||
skipResourceFingerprintEnabled bool
|
||||
logger *slog.Logger
|
||||
metadataStore telemetrytypes.MetadataStore
|
||||
fm qbtypes.FieldMapper
|
||||
cb qbtypes.ConditionBuilder
|
||||
resourceFilterStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
|
||||
aggExprRewriter qbtypes.AggExprRewriter
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
}
|
||||
|
||||
var _ qbtypes.StatementBuilder[qbtypes.TraceAggregation] = (*traceQueryStatementBuilder)(nil)
|
||||
@@ -43,12 +43,10 @@ func NewTraceQueryStatementBuilder(
|
||||
aggExprRewriter qbtypes.AggExprRewriter,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
flagger flagger.Flagger,
|
||||
skipResourceFingerprintEnable bool,
|
||||
skipResourceFingerprintThreshold uint64,
|
||||
) *traceQueryStatementBuilder {
|
||||
tracesSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrytraces")
|
||||
|
||||
resourceFilterResolver := telemetryresourcefilter.NewResolver[qbtypes.TraceAggregation](
|
||||
resourceFilterStmtBuilder := telemetryresourcefilter.New[qbtypes.TraceAggregation](
|
||||
settings,
|
||||
DBName,
|
||||
TracesResourceV3TableName,
|
||||
@@ -58,18 +56,16 @@ func NewTraceQueryStatementBuilder(
|
||||
nil,
|
||||
nil,
|
||||
flagger,
|
||||
telemetryStore,
|
||||
skipResourceFingerprintThreshold,
|
||||
)
|
||||
|
||||
return &traceQueryStatementBuilder{
|
||||
logger: tracesSettings.Logger(),
|
||||
metadataStore: metadataStore,
|
||||
fm: fieldMapper,
|
||||
cb: conditionBuilder,
|
||||
resourceFilterResolver: resourceFilterResolver,
|
||||
aggExprRewriter: aggExprRewriter,
|
||||
skipResourceFingerprintEnabled: skipResourceFingerprintEnable,
|
||||
logger: tracesSettings.Logger(),
|
||||
metadataStore: metadataStore,
|
||||
fm: fieldMapper,
|
||||
cb: conditionBuilder,
|
||||
resourceFilterStmtBuilder: resourceFilterStmtBuilder,
|
||||
aggExprRewriter: aggExprRewriter,
|
||||
telemetryStore: telemetryStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,11 +302,9 @@ func (b *traceQueryStatementBuilder) buildListQuery(
|
||||
cteArgs [][]any
|
||||
)
|
||||
|
||||
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
|
||||
if err != nil {
|
||||
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if frag != "" {
|
||||
} else if frag != "" {
|
||||
cteFragments = append(cteFragments, frag)
|
||||
cteArgs = append(cteArgs, args)
|
||||
}
|
||||
@@ -328,7 +322,7 @@ func (b *traceQueryStatementBuilder) buildListQuery(
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
|
||||
|
||||
// Add filter conditions
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -389,17 +383,15 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
|
||||
cteArgs [][]any
|
||||
)
|
||||
|
||||
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, distSB, query, start, end, variables)
|
||||
if err != nil {
|
||||
if frag, args, err := b.maybeAttachResourceFilter(ctx, distSB, query, start, end, variables); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if frag != "" {
|
||||
} else if frag != "" {
|
||||
cteFragments = append(cteFragments, frag)
|
||||
cteArgs = append(cteArgs, args)
|
||||
}
|
||||
|
||||
// Add filter conditions
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, distSB, start, end, query, keys, variables, skipResourceFilter)
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, distSB, start, end, query, keys, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -500,11 +492,9 @@ func (b *traceQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
cteArgs [][]any
|
||||
)
|
||||
|
||||
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
|
||||
if err != nil {
|
||||
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if frag != "" {
|
||||
} else if frag != "" {
|
||||
cteFragments = append(cteFragments, frag)
|
||||
cteArgs = append(cteArgs, args)
|
||||
}
|
||||
@@ -545,7 +535,7 @@ func (b *traceQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
}
|
||||
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -654,11 +644,9 @@ func (b *traceQueryStatementBuilder) buildScalarQuery(
|
||||
cteArgs [][]any
|
||||
)
|
||||
|
||||
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
|
||||
if err != nil {
|
||||
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if frag != "" && !skipResourceCTE {
|
||||
} else if frag != "" && !skipResourceCTE {
|
||||
cteFragments = append(cteFragments, frag)
|
||||
cteArgs = append(cteArgs, args)
|
||||
}
|
||||
@@ -700,7 +688,7 @@ func (b *traceQueryStatementBuilder) buildScalarQuery(
|
||||
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
|
||||
|
||||
// Add filter conditions
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
|
||||
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -763,7 +751,6 @@ func (b *traceQueryStatementBuilder) addFilterCondition(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
skipResourceFilter bool,
|
||||
) (querybuilder.PreparedWhereClause, error) {
|
||||
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
@@ -777,7 +764,7 @@ func (b *traceQueryStatementBuilder) addFilterCondition(
|
||||
FieldMapper: b.fm,
|
||||
ConditionBuilder: b.cb,
|
||||
FieldKeys: keys,
|
||||
SkipResourceFilter: skipResourceFilter,
|
||||
SkipResourceFilter: true,
|
||||
Variables: variables,
|
||||
StartNs: start,
|
||||
EndNs: end,
|
||||
@@ -818,30 +805,34 @@ func (b *traceQueryStatementBuilder) maybeAttachResourceFilter(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
||||
start, end uint64,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (cteSQL string, cteArgs []any, skipResourceFilter bool, err error) {
|
||||
) (cteSQL string, cteArgs []any, err error) {
|
||||
|
||||
if b.skipResourceFingerprintEnabled {
|
||||
decision, err := b.resourceFilterResolver.Resolve(ctx, query, start, end, variables)
|
||||
if err != nil {
|
||||
return "", nil, true, err
|
||||
}
|
||||
switch decision {
|
||||
case qbtypes.ResourceFilterResolveKindNoOp:
|
||||
return "", nil, true, nil
|
||||
case qbtypes.ResourceFilterResolveKindFallback:
|
||||
return "", nil, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
stmt, err := b.resourceFilterResolver.StatementBuilder().Build(
|
||||
ctx, start, end, qbtypes.RequestTypeRaw, query, variables,
|
||||
)
|
||||
stmt, err := b.buildResourceFilterCTE(ctx, query, start, end, variables)
|
||||
if err != nil {
|
||||
return "", nil, true, err
|
||||
return "", nil, err
|
||||
}
|
||||
if stmt == nil {
|
||||
return "", nil, true, nil
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
sb.Where("resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
|
||||
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, true, nil
|
||||
|
||||
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, nil
|
||||
}
|
||||
|
||||
func (b *traceQueryStatementBuilder) buildResourceFilterCTE(
|
||||
ctx context.Context,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
||||
start, end uint64,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (*qbtypes.Statement, error) {
|
||||
|
||||
return b.resourceFilterStmtBuilder.Build(
|
||||
ctx,
|
||||
start,
|
||||
end,
|
||||
qbtypes.RequestTypeRaw,
|
||||
query,
|
||||
variables,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,36 +2,19 @@ package telemetrytraces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cmock "github.com/SigNoz/clickhouse-go-mock"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type regexQueryMatcher struct{}
|
||||
|
||||
func (m *regexQueryMatcher) Match(expectedSQL, actualSQL string) error {
|
||||
re, err := regexp.Compile(expectedSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !re.MatchString(actualSQL) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "expected query to match %s, got %s", expectedSQL, actualSQL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestStatementBuilder(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -64,7 +47,7 @@ func TestStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -153,7 +136,7 @@ func TestStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(attribute_string_http$$route <> ?, attribute_string_http$$route, NULL)) AS `httpRoute`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `httpRoute` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(attribute_string_http$$route <> ?, attribute_string_http$$route, NULL)) AS `httpRoute`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`httpRoute`) GLOBAL IN (SELECT `httpRoute` FROM __limit_cte) GROUP BY ts, `httpRoute`",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(attribute_string_http$$route <> ?, attribute_string_http$$route, NULL)) AS `httpRoute`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `httpRoute` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(attribute_string_http$$route <> ?, attribute_string_http$$route, NULL)) AS `httpRoute`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`httpRoute`) GLOBAL IN (SELECT `httpRoute` FROM __limit_cte) GROUP BY ts, `httpRoute`",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -232,7 +215,7 @@ func TestStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 desc LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY ts desc",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 desc LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(mapContains(attributes_number, 'metric.max_count') = ?, toFloat64(attributes_number['metric.max_count']), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY ts desc",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -261,7 +244,7 @@ func TestStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -300,7 +283,7 @@ func TestStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, sum(multiIf(`attribute_number_cart$$items_count_exists` = ?, toFloat64(`attribute_number_cart$$items_count`), NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -331,7 +314,7 @@ func TestStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(response_status_code <> ?, response_status_code, NULL)) AS `responseStatusCode`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `responseStatusCode` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(response_status_code <> ?, response_status_code, NULL)) AS `responseStatusCode`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`responseStatusCode`) GLOBAL IN (SELECT `responseStatusCode` FROM __limit_cte) GROUP BY ts, `responseStatusCode`",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(response_status_code <> ?, response_status_code, NULL)) AS `responseStatusCode`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `responseStatusCode` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(response_status_code <> ?, response_status_code, NULL)) AS `responseStatusCode`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`responseStatusCode`) GLOBAL IN (SELECT `responseStatusCode` FROM __limit_cte) GROUP BY ts, `responseStatusCode`",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -362,7 +345,7 @@ func TestStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(response_status_code <> ?, response_status_code, NULL)) AS `responseStatusCode`, quantile(0.90)(multiIf(duration_nano <> ?, duration_nano, NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `responseStatusCode` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(response_status_code <> ?, response_status_code, NULL)) AS `responseStatusCode`, quantile(0.90)(multiIf(duration_nano <> ?, duration_nano, NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`responseStatusCode`) GLOBAL IN (SELECT `responseStatusCode` FROM __limit_cte) GROUP BY ts, `responseStatusCode`",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(response_status_code <> ?, response_status_code, NULL)) AS `responseStatusCode`, quantile(0.90)(multiIf(duration_nano <> ?, duration_nano, NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `responseStatusCode` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(response_status_code <> ?, response_status_code, NULL)) AS `responseStatusCode`, quantile(0.90)(multiIf(duration_nano <> ?, duration_nano, NULL)) AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`responseStatusCode`) GLOBAL IN (SELECT `responseStatusCode` FROM __limit_cte) GROUP BY ts, `responseStatusCode`",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "", 0, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "", 0, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -384,8 +367,6 @@ func TestStatementBuilder(t *testing.T) {
|
||||
aggExprRewriter,
|
||||
nil,
|
||||
fl,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
vars := map[string]qbtypes.VariableItem{
|
||||
@@ -458,7 +439,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT name AS `name`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, duration_nano AS `duration_nano`, `attribute_number_cart$$items_count` AS `cart.items_count`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT name AS `name`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, duration_nano AS `duration_nano`, `attribute_number_cart$$items_count` AS `cart.items_count`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -487,7 +468,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT duration_nano AS `duration_nano`, name AS `name`, response_status_code AS `response_status_code`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, span_id AS `span_id`, timestamp AS `timestamp`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY attributes_string['user.id'] AS `user.id` desc LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT duration_nano AS `duration_nano`, name AS `name`, response_status_code AS `response_status_code`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, span_id AS `span_id`, timestamp AS `timestamp`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY attributes_string['user.id'] AS `user.id` desc LIMIT ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -531,7 +512,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, response_status_code AS `responseStatusCode`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, response_status_code AS `responseStatusCode`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -575,7 +556,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, multiIf(toString(`attribute_string_mixed$$materialization$$key`) != '', toString(`attribute_string_mixed$$materialization$$key`), toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)) != '', toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)), NULL) AS `mixed.materialization.key`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, multiIf(toString(`attribute_string_mixed$$materialization$$key`) != '', toString(`attribute_string_mixed$$materialization$$key`), toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)) != '', toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)), NULL) AS `mixed.materialization.key`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -620,7 +601,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, `attribute_string_mixed$$materialization$$key` AS `mixed.materialization.key`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, `attribute_string_mixed$$materialization$$key` AS `mixed.materialization.key`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -681,8 +662,6 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
aggExprRewriter,
|
||||
nil,
|
||||
fl,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -792,8 +771,6 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
|
||||
aggExprRewriter,
|
||||
nil,
|
||||
fl,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
|
||||
@@ -830,7 +807,7 @@ func TestStatementBuilderTraceQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND trace_id GLOBAL IN __toe AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND trace_id GLOBAL IN __toe AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -946,8 +923,6 @@ func TestStatementBuilderTraceQuery(t *testing.T) {
|
||||
aggExprRewriter,
|
||||
nil,
|
||||
fl,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -1163,8 +1138,6 @@ func TestAdjustKey(t *testing.T) {
|
||||
aggExprRewriter,
|
||||
nil,
|
||||
fl,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -1439,8 +1412,6 @@ func TestAdjustKeys(t *testing.T) {
|
||||
aggExprRewriter,
|
||||
nil,
|
||||
fl,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -1512,114 +1483,3 @@ func TestAdjustKeys(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSkipResourceFingerprint exercises the three resolver outcomes when
|
||||
// skip_resource_fingerprint is enabled: use-CTE (count < threshold),
|
||||
// fallback (count >= threshold), and the legacy path (feature disabled).
|
||||
func TestSkipResourceFingerprint(t *testing.T) {
|
||||
const (
|
||||
startMs = uint64(1747947419000)
|
||||
endMs = uint64(1747983448000)
|
||||
threshold = uint64(10)
|
||||
)
|
||||
|
||||
query := qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name = 'redis-manual'",
|
||||
},
|
||||
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
||||
{
|
||||
Name: "name",
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
},
|
||||
Limit: 5,
|
||||
}
|
||||
|
||||
t.Run("disabled uses the legacy CTE", func(t *testing.T) {
|
||||
sb := newSkipResourceFingerprintBuilder(t, nil, false, threshold)
|
||||
|
||||
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, stmt.Query, "__resource_filter AS (SELECT fingerprint")
|
||||
require.Contains(t, stmt.Query, "resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
|
||||
})
|
||||
|
||||
t.Run("CTE attached when count below threshold", func(t *testing.T) {
|
||||
mockStore := telemetrystoretest.New(telemetrystore.Config{}, ®exQueryMatcher{})
|
||||
mock := mockStore.Mock()
|
||||
|
||||
// Only the count query runs against the telemetry store; the CTE
|
||||
// itself is embedded as SQL in the main query (no extra round trip).
|
||||
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_traces\.distributed_traces_v3_resource`).
|
||||
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
|
||||
{Name: "count", Type: "UInt64"},
|
||||
}, []any{uint64(2)}))
|
||||
|
||||
sb := newSkipResourceFingerprintBuilder(t, mockStore, true, threshold)
|
||||
|
||||
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, stmt.Query, "__resource_filter AS (SELECT fingerprint")
|
||||
require.Contains(t, stmt.Query, "resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
|
||||
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
t.Run("fallback when count at or above threshold", func(t *testing.T) {
|
||||
mockStore := telemetrystoretest.New(telemetrystore.Config{}, ®exQueryMatcher{})
|
||||
mock := mockStore.Mock()
|
||||
|
||||
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_traces\.distributed_traces_v3_resource`).
|
||||
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
|
||||
{Name: "count", Type: "UInt64"},
|
||||
}, []any{threshold}))
|
||||
|
||||
sb := newSkipResourceFingerprintBuilder(t, mockStore, true, threshold)
|
||||
|
||||
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotContains(t, stmt.Query, "__resource_filter AS")
|
||||
require.NotContains(t, stmt.Query, "resource_fingerprint")
|
||||
// resource conditions are pushed onto the main table via the
|
||||
// resource.`service.name` / resources_string lookup
|
||||
require.Contains(t, stmt.Query, "service.name")
|
||||
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
}
|
||||
|
||||
func newSkipResourceFingerprintBuilder(
|
||||
t *testing.T,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
skipEnable bool,
|
||||
threshold uint64,
|
||||
) *traceQueryStatementBuilder {
|
||||
t.Helper()
|
||||
|
||||
fm := NewFieldMapper()
|
||||
cb := NewConditionBuilder(fm)
|
||||
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
mockMetadataStore.KeysMap = buildCompleteFieldKeyMap()
|
||||
|
||||
fl := flaggertest.New(t)
|
||||
aggExprRewriter := querybuilder.NewAggExprRewriter(
|
||||
instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl,
|
||||
)
|
||||
|
||||
return NewTraceQueryStatementBuilder(
|
||||
instrumentationtest.New().ToProviderSettings(),
|
||||
mockMetadataStore,
|
||||
fm,
|
||||
cb,
|
||||
aggExprRewriter,
|
||||
telemetryStore,
|
||||
fl,
|
||||
skipEnable,
|
||||
threshold,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name` FROM A_DIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name` FROM A_DIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -104,7 +104,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_INDIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_INDIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -141,7 +141,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_AND_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_AND_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 15},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -178,7 +178,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_OR_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_OR_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 20},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -215,7 +215,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_not_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_not_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -264,7 +264,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT toStartOfInterval(timestamp, INTERVAL 60 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM A_DIR_DESC_B GROUP BY ts, `service.name` ORDER BY ts desc SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id) SELECT toStartOfInterval(timestamp, INTERVAL 60 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM A_DIR_DESC_B GROUP BY ts, `service.name` ORDER BY ts desc SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -323,7 +323,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND toFloat64(response_status_code) < ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, avg(multiIf(duration_nano <> ?, duration_nano, NULL)) AS __result_0 FROM A_AND_B GROUP BY `service.name` ORDER BY __result_0 desc SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND toFloat64(response_status_code) < ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, avg(multiIf(duration_nano <> ?, duration_nano, NULL)) AS __result_0 FROM A_AND_B GROUP BY `service.name` ORDER BY __result_0 desc SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), float64(400), 0},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -380,7 +380,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_DIR_DESC_B_AND_C_DIR_DESC_D ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_DIR_DESC_B_AND_C_DIR_DESC_D ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "auth", "%service.name%", "%service.name\":\"auth%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -414,7 +414,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __return_from_B AS (SELECT * FROM B WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM __return_from_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __return_from_B AS (SELECT * FROM B WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM __return_from_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -456,7 +456,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B_AND_C AS (SELECT l.* FROM A_INDIR_DESC_B AS l INNER JOIN C AS r ON l.trace_id = r.trace_id), __return_from_C AS (SELECT * FROM C WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B_AND_C)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM __return_from_C ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B_AND_C AS (SELECT l.* FROM A_INDIR_DESC_B AS l INNER JOIN C AS r ON l.trace_id = r.trace_id), __return_from_C AS (SELECT * FROM C WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B_AND_C)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM __return_from_C ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "auth", "%service.name%", "%service.name\":\"auth%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
@@ -478,8 +478,6 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
aggExprRewriter,
|
||||
nil,
|
||||
fl,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
statementBuilder := NewTraceOperatorStatementBuilder(
|
||||
@@ -596,8 +594,6 @@ func TestTraceOperatorStatementBuilderErrors(t *testing.T) {
|
||||
aggExprRewriter,
|
||||
nil,
|
||||
fl,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
statementBuilder := NewTraceOperatorStatementBuilder(
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -44,10 +44,8 @@ func TestTraceTimeRangeOptimization(t *testing.T) {
|
||||
fm,
|
||||
cb,
|
||||
aggExprRewriter,
|
||||
nil, // telemetryStore is nil - adaptive path is disabled
|
||||
nil, // telemetryStore is nil - optimization won't happen but code path is tested
|
||||
fl,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
type ResourceFilterResolveKind int
|
||||
|
||||
const (
|
||||
ResourceFilterResolveKindNoOp ResourceFilterResolveKind = iota
|
||||
ResourceFilterResolveKindUseCTE
|
||||
ResourceFilterResolveKindFallback
|
||||
)
|
||||
45
tests/fixtures/querier.py
vendored
45
tests/fixtures/querier.py
vendored
@@ -28,42 +28,6 @@ class TelemetryFieldKey:
|
||||
}
|
||||
|
||||
|
||||
class RequestType:
|
||||
RAW = "raw"
|
||||
TIME_SERIES = "time_series"
|
||||
SCALAR = "scalar"
|
||||
TABLE = "table"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Aggregation:
|
||||
expression: str
|
||||
alias: str | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
agg: dict[str, Any] = {"expression": self.expression}
|
||||
if self.alias:
|
||||
agg["alias"] = self.alias
|
||||
return agg
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetricAggregation:
|
||||
metric_name: str
|
||||
time_aggregation: str
|
||||
space_aggregation: str
|
||||
temporality: str = "cumulative"
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
agg: dict[str, Any] = {
|
||||
"metricName": self.metric_name,
|
||||
"timeAggregation": self.time_aggregation,
|
||||
"spaceAggregation": self.space_aggregation,
|
||||
"temporality": self.temporality,
|
||||
}
|
||||
return agg
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrderBy:
|
||||
key: TelemetryFieldKey
|
||||
@@ -82,8 +46,6 @@ class BuilderQuery:
|
||||
filter_expression: str | None = None
|
||||
select_fields: list[TelemetryFieldKey] | None = None
|
||||
order: list[OrderBy] | None = None
|
||||
aggregations: list[Aggregation | MetricAggregation] | None = None
|
||||
step_interval: int | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
spec: dict[str, Any] = {
|
||||
@@ -100,11 +62,6 @@ class BuilderQuery:
|
||||
spec["selectFields"] = [f.to_dict() for f in self.select_fields]
|
||||
if self.order:
|
||||
spec["order"] = [o.to_dict() if hasattr(o, "to_dict") else o for o in self.order]
|
||||
if self.aggregations:
|
||||
spec["aggregations"] = [agg.to_dict() if hasattr(agg, "to_dict") else agg for agg in self.aggregations]
|
||||
if self.step_interval is not None:
|
||||
spec["stepInterval"] = self.step_interval
|
||||
|
||||
return {"type": "builder_query", "spec": spec}
|
||||
|
||||
|
||||
@@ -160,7 +117,7 @@ def make_query_request(
|
||||
end_ms: int,
|
||||
queries: list[dict],
|
||||
*,
|
||||
request_type: str = RequestType.TIME_SERIES,
|
||||
request_type: str = "time_series",
|
||||
format_options: dict | None = None,
|
||||
variables: dict | None = None,
|
||||
no_cache: bool = True,
|
||||
|
||||
@@ -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")])
|
||||
|
||||
@@ -1,386 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.meter import MeterSample, make_meter_samples
|
||||
from fixtures.querier import (
|
||||
Aggregation,
|
||||
BuilderQuery,
|
||||
MetricAggregation,
|
||||
RequestType,
|
||||
make_query_request,
|
||||
)
|
||||
|
||||
|
||||
def test_resource_default_warning(
|
||||
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),
|
||||
resources={
|
||||
"service.name": "java",
|
||||
},
|
||||
attributes={
|
||||
"service.name": "java",
|
||||
},
|
||||
body="This is a log message, coming from a java application",
|
||||
severity_text="DEBUG",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((datetime.now(tz=UTC) - timedelta(minutes=20)).timestamp() * 1000),
|
||||
end_ms=int(datetime.now(tz=UTC).timestamp() * 1000),
|
||||
request_type=RequestType.RAW,
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
name="A",
|
||||
signal="logs",
|
||||
filter_expression="service.name = 'java'",
|
||||
).to_dict()
|
||||
],
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
expected_service_name_warning = (
|
||||
"Key `service.name` is ambiguous, found 2 different combinations of "
|
||||
"field context / data type: [name=service.name,context=resource,datatype=string "
|
||||
"name=service.name,context=attribute,datatype=string]. Using `resource` context "
|
||||
"by default. To query attributes explicitly, use the fully qualified name "
|
||||
"(e.g., 'attribute.service.name')"
|
||||
)
|
||||
assert warning["warnings"] == [
|
||||
{"message": expected_service_name_warning},
|
||||
]
|
||||
|
||||
|
||||
def test_key_collision_warning(
|
||||
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),
|
||||
resources={
|
||||
"service.name": "java",
|
||||
},
|
||||
attributes={
|
||||
"service.name": "java",
|
||||
"http.status_code": 200,
|
||||
},
|
||||
body="This is a log message, coming from a java application",
|
||||
severity_text="DEBUG",
|
||||
),
|
||||
Logs(
|
||||
timestamp=datetime.now(tz=UTC),
|
||||
resources={
|
||||
"service.name": "java",
|
||||
},
|
||||
attributes={
|
||||
"service.name": "java",
|
||||
"http.status_code": "200",
|
||||
},
|
||||
body="This is a log message, coming from a java application",
|
||||
severity_text="DEBUG",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((datetime.now(tz=UTC) - timedelta(minutes=20)).timestamp() * 1000),
|
||||
end_ms=int(datetime.now(tz=UTC).timestamp() * 1000),
|
||||
request_type=RequestType.RAW,
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
name="A",
|
||||
signal="logs",
|
||||
filter_expression="http.status_code = '200'",
|
||||
).to_dict()
|
||||
],
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
expected_http_status_code_warning = "Key `http.status_code` is ambiguous, found 2 different combinations of field context / data type: [name=http.status_code,context=attribute,datatype=number name=http.status_code,context=attribute,datatype=string]."
|
||||
assert warning["warnings"] == [
|
||||
{"message": expected_http_status_code_warning},
|
||||
]
|
||||
|
||||
|
||||
def test_deduped_warnings_for_single_query(
|
||||
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(minutes=i * 10),
|
||||
resources={
|
||||
"service.name": "java",
|
||||
},
|
||||
attributes={
|
||||
"service.name": "java",
|
||||
"http.status_code": 200,
|
||||
},
|
||||
body="This is a log message, coming from a java application",
|
||||
severity_text="DEBUG",
|
||||
)
|
||||
for i in range(10)
|
||||
],
|
||||
Logs(
|
||||
timestamp=datetime.now(tz=UTC),
|
||||
resources={
|
||||
"service.name": "java",
|
||||
},
|
||||
attributes={
|
||||
"service.name": "java",
|
||||
"http.status_code": "200",
|
||||
},
|
||||
body="This is a log message, coming from a java application",
|
||||
severity_text="DEBUG",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# `service.name` (resource vs attribute) and `http.status_code` (number vs
|
||||
# string) are both ambiguous keys, so referencing them in the filter makes
|
||||
# the querier emit an "is ambiguous" warning while *building* the query.
|
||||
#
|
||||
# This test targets the bucket-cache warning-merge path: the cache stores a
|
||||
# query's warnings alongside its buckets, and on a partial cache hit
|
||||
# executeWithCache merges the cached warnings with the warnings re-emitted by
|
||||
# each freshly executed missing range (querier.mergeResults). Before the
|
||||
# dedup fix the same message appeared once per executed range.
|
||||
query = BuilderQuery(
|
||||
name="A",
|
||||
signal="logs",
|
||||
step_interval=600,
|
||||
filter_expression="service.name = 'java' and http.status_code = 200",
|
||||
aggregations=[Aggregation(expression="count()")],
|
||||
).to_dict()
|
||||
|
||||
# Anchor both windows to a single "now" so their step-aligned boundaries
|
||||
# match. Both windows end well before the 5m flux interval so the results
|
||||
# are cacheable.
|
||||
now_ms = int(datetime.now(tz=UTC).timestamp() * 1000)
|
||||
minute = 60 * 1000
|
||||
|
||||
# First request populates the cache for [-90m, -30m], storing the warnings.
|
||||
first = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=now_ms - 90 * minute,
|
||||
end_ms=now_ms - 30 * minute,
|
||||
request_type=RequestType.TIME_SERIES,
|
||||
queries=[query],
|
||||
no_cache=False,
|
||||
)
|
||||
assert first.status_code == HTTPStatus.OK
|
||||
assert first.json()["status"] == "success"
|
||||
|
||||
# Second request reuses the cached [-90m, -30m] buckets (which carry the
|
||||
# cached warnings) and executes only the trailing [-30m, -20m] step fresh,
|
||||
# which re-emits the same warnings — exercising the cache/fresh merge.
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=now_ms - 90 * minute,
|
||||
end_ms=now_ms - 20 * minute,
|
||||
request_type=RequestType.TIME_SERIES,
|
||||
queries=[query],
|
||||
no_cache=False,
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
# Each ambiguity warning arrives from both the cached portion and the fresh
|
||||
# missing range; after deduplication each distinct message appears once.
|
||||
expected_service_name_warning = (
|
||||
"Key `service.name` is ambiguous, found 2 different combinations of "
|
||||
"field context / data type: [name=service.name,context=resource,datatype=string "
|
||||
"name=service.name,context=attribute,datatype=string]. Using `resource` context "
|
||||
"by default. To query attributes explicitly, use the fully qualified name "
|
||||
"(e.g., 'attribute.service.name')"
|
||||
)
|
||||
expected_status_code_warning = "Key `http.status_code` is ambiguous, found 2 different combinations of field context / data type: [name=http.status_code,context=attribute,datatype=number name=http.status_code,context=attribute,datatype=string]."
|
||||
assert warning["warnings"] == [
|
||||
{"message": expected_service_name_warning},
|
||||
{"message": expected_status_code_warning},
|
||||
]
|
||||
|
||||
|
||||
def test_deduped_warnings_for_multiple_queries(
|
||||
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(minutes=i * 10),
|
||||
resources={
|
||||
"service.name": "java",
|
||||
},
|
||||
attributes={
|
||||
"service.name": "java",
|
||||
"http.status_code": 200,
|
||||
},
|
||||
body="This is a log message, coming from a java application",
|
||||
severity_text="DEBUG",
|
||||
)
|
||||
for i in range(10)
|
||||
],
|
||||
Logs(
|
||||
timestamp=datetime.now(tz=UTC),
|
||||
resources={
|
||||
"service.name": "java",
|
||||
},
|
||||
attributes={
|
||||
"service.name": "java",
|
||||
"http.status_code": "200",
|
||||
},
|
||||
body="This is a log message, coming from a java application",
|
||||
severity_text="DEBUG",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# `service.name` (resource vs attribute) and `http.status_code` (number vs
|
||||
# string) are both ambiguous keys, so referencing them in the filter makes
|
||||
# the querier emit an "is ambiguous" warning while *building* the query.
|
||||
query_1 = BuilderQuery(
|
||||
name="A",
|
||||
signal="logs",
|
||||
step_interval=600,
|
||||
filter_expression="service.name = 'java' and http.status_code = 200",
|
||||
aggregations=[Aggregation(expression="count()")],
|
||||
).to_dict()
|
||||
query_2 = BuilderQuery(
|
||||
name="B",
|
||||
signal="logs",
|
||||
step_interval=600,
|
||||
filter_expression="service.name != '_java' and http.status_code = 200",
|
||||
aggregations=[Aggregation(expression="count()")],
|
||||
).to_dict()
|
||||
|
||||
now_ms = int(datetime.now(tz=UTC).timestamp() * 1000)
|
||||
minute = 60 * 1000
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=now_ms - 90 * minute,
|
||||
end_ms=now_ms - 20 * minute,
|
||||
request_type=RequestType.TIME_SERIES,
|
||||
queries=[query_1, query_2],
|
||||
no_cache=False,
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
expected_service_name_warning = (
|
||||
"Key `service.name` is ambiguous, found 2 different combinations of "
|
||||
"field context / data type: [name=service.name,context=resource,datatype=string "
|
||||
"name=service.name,context=attribute,datatype=string]. Using `resource` context "
|
||||
"by default. To query attributes explicitly, use the fully qualified name "
|
||||
"(e.g., 'attribute.service.name')"
|
||||
)
|
||||
expected_status_code_warning = "Key `http.status_code` is ambiguous, found 2 different combinations of field context / data type: [name=http.status_code,context=attribute,datatype=number name=http.status_code,context=attribute,datatype=string]."
|
||||
assert warning["warnings"] == [
|
||||
{"message": expected_service_name_warning},
|
||||
{"message": expected_status_code_warning},
|
||||
]
|
||||
|
||||
|
||||
def test_no_warnings_for_meter_query(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_meter_samples: Callable[[list[MeterSample]], None],
|
||||
) -> None:
|
||||
# Meter queries deliberately suppress the step-interval clamp warning. The
|
||||
# minimum allowed step for a meter is 1h, so a 10m (600s) step over this
|
||||
# range would normally clamp and emit a warning for a regular metric — for a
|
||||
# meter source the querier discards it. This asserts no warning leaks out.
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
metric_name = "signoz.meter.log.size"
|
||||
insert_meter_samples(
|
||||
make_meter_samples(
|
||||
metric_name,
|
||||
{"service": "test-service"},
|
||||
now,
|
||||
count=60,
|
||||
temporality="Delta",
|
||||
type_="Sum",
|
||||
is_monotonic=True,
|
||||
)
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(minutes=20)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
request_type=RequestType.TIME_SERIES,
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
name="A",
|
||||
signal="metrics",
|
||||
source="meter",
|
||||
step_interval=600,
|
||||
aggregations=[MetricAggregation(metric_name=metric_name, time_aggregation="sum", space_aggregation="sum")],
|
||||
).to_dict()
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
assert "warning" not in response.json()["data"]
|
||||
@@ -1,191 +0,0 @@
|
||||
"""
|
||||
End-to-end coverage for the skip_resource_fingerprint querier optimization.
|
||||
|
||||
The conftest in this package boots SigNoz with:
|
||||
- skip_resource_fingerprint.enabled = true
|
||||
- skip_resource_fingerprint.threshold = 2
|
||||
|
||||
With that configuration the two non-trivial resolver branches are reachable
|
||||
from a single SigNoz instance:
|
||||
|
||||
- count < 2 -> the resolver attaches the fingerprint CTE (same shape as the
|
||||
legacy path; cheap because the fingerprint set is small).
|
||||
- count >= 2 -> fallback path: no fingerprint subquery, resource conditions
|
||||
are evaluated directly on the main spans table.
|
||||
|
||||
These tests assert end-to-end correctness — the optimization must be
|
||||
semantically transparent.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import make_query_request
|
||||
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
|
||||
|
||||
|
||||
def _span(
|
||||
*,
|
||||
timestamp: datetime,
|
||||
service_name: str,
|
||||
name: str = "span",
|
||||
duration_seconds: float = 1.0,
|
||||
extra_resources: dict | None = None,
|
||||
attributes: dict | None = None,
|
||||
) -> Traces:
|
||||
resources = {"service.name": service_name}
|
||||
if extra_resources:
|
||||
resources.update(extra_resources)
|
||||
return Traces(
|
||||
timestamp=timestamp,
|
||||
duration=timedelta(seconds=duration_seconds),
|
||||
trace_id=TraceIdGenerator.trace_id(),
|
||||
span_id=TraceIdGenerator.span_id(),
|
||||
parent_span_id="",
|
||||
name=name,
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources=resources,
|
||||
attributes=attributes or {},
|
||||
)
|
||||
|
||||
|
||||
def test_skip_resource_fingerprint_use_cte_path(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
A filter that matches a single unique resource fingerprint (count = 1 < 2)
|
||||
keeps the legacy CTE attached. The query should still return only the rows
|
||||
belonging to that resource.
|
||||
"""
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
_span(timestamp=now - timedelta(seconds=10), service_name="skip-cte-svc", name="span-1"),
|
||||
_span(timestamp=now - timedelta(seconds=8), service_name="skip-cte-svc", name="span-2"),
|
||||
# Noise from a different resource — must be filtered out.
|
||||
_span(timestamp=now - timedelta(seconds=6), service_name="skip-cte-noise", name="span-noise"),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000),
|
||||
end_ms=int(datetime.now(tz=UTC).timestamp() * 1000),
|
||||
request_type="raw",
|
||||
queries=[
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"limit": 50,
|
||||
"order": [{"key": {"name": "timestamp"}, "direction": "asc"}],
|
||||
"filter": {"expression": "service.name = 'skip-cte-svc'"},
|
||||
"selectFields": [
|
||||
{
|
||||
"name": "service.name",
|
||||
"fieldDataType": "string",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
{"name": "name", "fieldContext": "span"},
|
||||
],
|
||||
"aggregations": [{"expression": "count()"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0]["rows"]
|
||||
assert len(rows) == 2, f"expected only the 2 'skip-cte-svc' spans, got {len(rows)}"
|
||||
|
||||
names = [row["data"]["name"] for row in rows]
|
||||
assert names == ["span-1", "span-2"]
|
||||
|
||||
services = {row["data"]["service.name"] for row in rows}
|
||||
assert services == {"skip-cte-svc"}
|
||||
|
||||
|
||||
def test_skip_resource_fingerprint_fallback_path(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
A filter that matches multiple unique resource fingerprints (count >= 2)
|
||||
drives the resolver down the fallback path: no fingerprint subquery, and
|
||||
the resource condition is evaluated directly on the main spans table.
|
||||
The result must still be correct (no over- or under-matching).
|
||||
"""
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
# 3 services share the same deployment.environment, so the resource filter
|
||||
# selects 3 fingerprints, exceeding our threshold of 2.
|
||||
fallback_env = {"deployment.environment": "skip-fallback"}
|
||||
insert_traces(
|
||||
[
|
||||
_span(timestamp=now - timedelta(seconds=10), service_name="skip-fb-svc-a", extra_resources=fallback_env),
|
||||
_span(timestamp=now - timedelta(seconds=9), service_name="skip-fb-svc-b", extra_resources=fallback_env),
|
||||
_span(timestamp=now - timedelta(seconds=8), service_name="skip-fb-svc-c", extra_resources=fallback_env),
|
||||
# Noise without the fallback env — must be filtered out.
|
||||
_span(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
service_name="skip-fb-other",
|
||||
extra_resources={"deployment.environment": "skip-other"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000),
|
||||
end_ms=int(datetime.now(tz=UTC).timestamp() * 1000),
|
||||
request_type="raw",
|
||||
queries=[
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"limit": 50,
|
||||
"order": [{"key": {"name": "timestamp"}, "direction": "asc"}],
|
||||
"filter": {"expression": "deployment.environment = 'skip-fallback'"},
|
||||
"selectFields": [
|
||||
{
|
||||
"name": "service.name",
|
||||
"fieldDataType": "string",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
],
|
||||
"aggregations": [{"expression": "count()"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0]["rows"]
|
||||
assert len(rows) == 3, f"expected 3 spans tagged with skip-fallback, got {len(rows)}"
|
||||
|
||||
services = sorted(row["data"]["service.name"] for row in rows)
|
||||
assert services == ["skip-fb-svc-a", "skip-fb-svc-b", "skip-fb-svc-c"]
|
||||
@@ -1,175 +0,0 @@
|
||||
"""
|
||||
End-to-end coverage for the skip_resource_fingerprint optimization on logs.
|
||||
|
||||
The conftest boots SigNoz with threshold=2, so:
|
||||
- count < 2 -> resolver attaches the fingerprint CTE (same shape as legacy).
|
||||
- count >= 2 -> fallback: resource conditions evaluated on the main logs table.
|
||||
|
||||
Both branches must return the same correct rows.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.querier import make_query_request
|
||||
|
||||
|
||||
def _log(
|
||||
*,
|
||||
timestamp: datetime,
|
||||
service_name: str,
|
||||
body: str,
|
||||
extra_resources: dict | None = None,
|
||||
) -> Logs:
|
||||
resources = {"service.name": service_name}
|
||||
if extra_resources:
|
||||
resources.update(extra_resources)
|
||||
return Logs(
|
||||
timestamp=timestamp,
|
||||
resources=resources,
|
||||
attributes={},
|
||||
body=body,
|
||||
severity_text="INFO",
|
||||
)
|
||||
|
||||
|
||||
def test_skip_resource_fingerprint_logs_use_cte_path(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
) -> None:
|
||||
"""
|
||||
A filter matching a single resource fingerprint (count = 1 < 2) keeps the
|
||||
legacy CTE attached. The result must only include rows for that resource.
|
||||
"""
|
||||
now = datetime.now(tz=UTC)
|
||||
|
||||
insert_logs(
|
||||
[
|
||||
_log(timestamp=now - timedelta(seconds=10), service_name="skip-logs-cte-svc", body="log-1"),
|
||||
_log(timestamp=now - timedelta(seconds=8), service_name="skip-logs-cte-svc", body="log-2"),
|
||||
# Noise from a different resource — must not appear.
|
||||
_log(timestamp=now - timedelta(seconds=6), service_name="skip-logs-cte-noise", body="noise"),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000),
|
||||
end_ms=int(datetime.now(tz=UTC).timestamp() * 1000),
|
||||
request_type="raw",
|
||||
queries=[
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"limit": 50,
|
||||
"order": [{"key": {"name": "timestamp"}, "direction": "asc"}],
|
||||
"filter": {"expression": "service.name = 'skip-logs-cte-svc'"},
|
||||
"selectFields": [
|
||||
{
|
||||
"name": "service.name",
|
||||
"fieldDataType": "string",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
{"name": "body"},
|
||||
],
|
||||
"aggregations": [{"expression": "count()"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0]["rows"]
|
||||
assert len(rows) == 2, f"expected 2 'skip-logs-cte-svc' rows, got {len(rows)}"
|
||||
|
||||
bodies = [row["data"]["body"] for row in rows]
|
||||
assert bodies == ["log-1", "log-2"]
|
||||
|
||||
services = {row["data"]["service.name"] for row in rows}
|
||||
assert services == {"skip-logs-cte-svc"}
|
||||
|
||||
|
||||
def test_skip_resource_fingerprint_logs_fallback_path(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
) -> None:
|
||||
"""
|
||||
A filter matching multiple resource fingerprints (count >= 2) drives the
|
||||
fallback path: no CTE, resource conditions evaluated on the main logs
|
||||
table. Result must still be correct (no over- or under-matching).
|
||||
"""
|
||||
now = datetime.now(tz=UTC)
|
||||
|
||||
fallback_env = {"deployment.environment": "skip-logs-fallback"}
|
||||
insert_logs(
|
||||
[
|
||||
_log(timestamp=now - timedelta(seconds=10), service_name="skip-logs-fb-svc-a", body="a", extra_resources=fallback_env),
|
||||
_log(timestamp=now - timedelta(seconds=9), service_name="skip-logs-fb-svc-b", body="b", extra_resources=fallback_env),
|
||||
_log(timestamp=now - timedelta(seconds=8), service_name="skip-logs-fb-svc-c", body="c", extra_resources=fallback_env),
|
||||
# Noise without the fallback env — must be filtered out.
|
||||
_log(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
service_name="skip-logs-fb-other",
|
||||
body="noise",
|
||||
extra_resources={"deployment.environment": "skip-logs-other"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000),
|
||||
end_ms=int(datetime.now(tz=UTC).timestamp() * 1000),
|
||||
request_type="raw",
|
||||
queries=[
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"limit": 50,
|
||||
"order": [{"key": {"name": "timestamp"}, "direction": "asc"}],
|
||||
"filter": {"expression": "deployment.environment = 'skip-logs-fallback'"},
|
||||
"selectFields": [
|
||||
{
|
||||
"name": "service.name",
|
||||
"fieldDataType": "string",
|
||||
"fieldContext": "resource",
|
||||
},
|
||||
{"name": "body"},
|
||||
],
|
||||
"aggregations": [{"expression": "count()"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0]["rows"]
|
||||
assert len(rows) == 3, f"expected 3 fallback rows, got {len(rows)}"
|
||||
|
||||
services = sorted(row["data"]["service.name"] for row in rows)
|
||||
assert services == ["skip-logs-fb-svc-a", "skip-logs-fb-svc-b", "skip-logs-fb-svc-c"]
|
||||
|
||||
bodies = sorted(row["data"]["body"] for row in rows)
|
||||
assert bodies == ["a", "b", "c"]
|
||||
@@ -1,37 +0,0 @@
|
||||
import pytest
|
||||
from testcontainers.core.container import Network
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import create_signoz
|
||||
|
||||
|
||||
@pytest.fixture(name="signoz", scope="package")
|
||||
def signoz_skip_resource_fingerprint(
|
||||
network: Network,
|
||||
migrator: types.Operation, # pylint: disable=unused-argument
|
||||
zeus: types.TestContainerDocker,
|
||||
gateway: types.TestContainerDocker,
|
||||
sqlstore: types.TestContainerSQL,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.SigNoz:
|
||||
"""
|
||||
Package-scoped SigNoz instance with the skip_resource_fingerprint
|
||||
optimization enabled and a low threshold so both the materialized and
|
||||
fallback resolver paths are exercised by sibling tests.
|
||||
"""
|
||||
return create_signoz(
|
||||
network=network,
|
||||
zeus=zeus,
|
||||
gateway=gateway,
|
||||
sqlstore=sqlstore,
|
||||
clickhouse=clickhouse,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="signoz-skip-resource-fingerprint",
|
||||
env_overrides={
|
||||
"SIGNOZ_QUERIER_SKIP__RESOURCE__FINGERPRINT_ENABLED": True,
|
||||
"SIGNOZ_QUERIER_SKIP__RESOURCE__FINGERPRINT_THRESHOLD": 2,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user