mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-11 20:50:35 +01:00
Compare commits
12 Commits
feat/alert
...
traceop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
357444c94e | ||
|
|
a8598f3bfa | ||
|
|
bca71f9a33 | ||
|
|
c93660357d | ||
|
|
5651e3b7a8 | ||
|
|
cf2cfbc7d4 | ||
|
|
a969c38224 | ||
|
|
b892a0f0a5 | ||
|
|
4d47762eba | ||
|
|
77396a0bb3 | ||
|
|
28c05e1bab | ||
|
|
2b9e383994 |
@@ -50,7 +50,7 @@
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/ui": "0.0.19",
|
||||
"@signozhq/ui": "0.0.18",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
.labelColumn {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.labelBadge {
|
||||
cursor: default;
|
||||
font-size: 12px;
|
||||
|
||||
--badge-display: inline;
|
||||
|
||||
max-width: 180px;
|
||||
min-width: 100px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.labelPopover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.labelBadgePopover {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.labelValue {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import LabelColumn from './LabelColumn';
|
||||
|
||||
function renderWithProviders(
|
||||
ui: React.ReactElement,
|
||||
): ReturnType<typeof render> {
|
||||
return render(<TooltipProvider>{ui}</TooltipProvider>);
|
||||
}
|
||||
|
||||
describe('LabelColumn', () => {
|
||||
it('should render all labels when 5 or fewer', () => {
|
||||
const labels = ['env', 'service', 'region'];
|
||||
|
||||
renderWithProviders(<LabelColumn labels={labels} />);
|
||||
|
||||
expect(screen.getByTestId('label-tag-env')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('label-tag-service')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('label-tag-region')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should truncate labels and show +N badge when more than 5 labels', () => {
|
||||
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
|
||||
|
||||
renderWithProviders(<LabelColumn labels={labels} />);
|
||||
|
||||
// First 3 visible
|
||||
expect(screen.getByTestId('label-tag-env')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('label-tag-service')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('label-tag-region')).toBeInTheDocument();
|
||||
|
||||
// +3 badge for remaining
|
||||
expect(screen.getByTestId('label-overflow-badge')).toHaveTextContent('+3');
|
||||
});
|
||||
|
||||
it('should render label with value when value prop provided', () => {
|
||||
const labels = ['env'];
|
||||
const value = { env: 'production' };
|
||||
|
||||
renderWithProviders(<LabelColumn labels={labels} value={value} />);
|
||||
|
||||
expect(screen.getByTestId('label-tag-env')).toHaveTextContent(
|
||||
'env: production',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render labels without value when value is not provided for that label', () => {
|
||||
const labels = ['env', 'service'];
|
||||
const value = { env: 'production' };
|
||||
|
||||
renderWithProviders(<LabelColumn labels={labels} value={value} />);
|
||||
|
||||
expect(screen.getByTestId('label-tag-env')).toHaveTextContent(
|
||||
'env: production',
|
||||
);
|
||||
expect(screen.getByTestId('label-tag-service')).toHaveTextContent('service');
|
||||
});
|
||||
|
||||
it('should show popover with all labels when clicking +N badge', async () => {
|
||||
const user = userEvent.setup();
|
||||
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
|
||||
|
||||
renderWithProviders(<LabelColumn labels={labels} />);
|
||||
|
||||
await user.click(screen.getByTestId('label-overflow-badge'));
|
||||
|
||||
// All labels should appear in popover
|
||||
expect(screen.getByTestId('label-popover')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('label-popover-item-env')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('label-popover-item-version')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render empty when no labels provided', () => {
|
||||
renderWithProviders(<LabelColumn labels={[]} />);
|
||||
|
||||
const column = screen.getByTestId('label-column');
|
||||
expect(column.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should use primary color by default', () => {
|
||||
const labels = ['env'];
|
||||
|
||||
renderWithProviders(<LabelColumn labels={labels} />);
|
||||
|
||||
expect(screen.getByTestId('label-tag-env')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,123 +0,0 @@
|
||||
import {
|
||||
Badge,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
TooltipSimple,
|
||||
} from '@signozhq/ui';
|
||||
|
||||
import styles from './LabelColumn.module.scss';
|
||||
|
||||
export interface LabelColumnProps {
|
||||
labels: string[];
|
||||
color?:
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'success'
|
||||
| 'error'
|
||||
| 'warning'
|
||||
| 'robin'
|
||||
| 'forest'
|
||||
| 'amber'
|
||||
| 'sienna'
|
||||
| 'cherry'
|
||||
| 'sakura'
|
||||
| 'aqua'
|
||||
| 'vanilla';
|
||||
value?: { [key: string]: string };
|
||||
}
|
||||
|
||||
function getLabelRenderingValue(label: string, value?: string): JSX.Element {
|
||||
const title = value ? `${label}: ${value}` : label;
|
||||
const content = value ? `${label}: ${value}` : label;
|
||||
|
||||
return (
|
||||
<span title={title} className={styles.labelValue}>
|
||||
{content}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function getLabelAndValueContent(label: string, value?: string): string {
|
||||
return value ? `${label}: ${value}` : label;
|
||||
}
|
||||
|
||||
function LabelTag({
|
||||
label,
|
||||
value,
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
color?: LabelColumnProps['color'];
|
||||
value?: LabelColumnProps['value'];
|
||||
}): JSX.Element {
|
||||
const tooltipTitle = value?.[label] ? `${label}: ${value[label]}` : label;
|
||||
|
||||
return (
|
||||
<TooltipSimple title={tooltipTitle}>
|
||||
<Badge
|
||||
color={color}
|
||||
className={styles.labelBadge}
|
||||
variant="outline"
|
||||
data-testid={`label-tag-${label}`}
|
||||
>
|
||||
{getLabelRenderingValue(label, value?.[label])}
|
||||
</Badge>
|
||||
</TooltipSimple>
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_LABELS_TO_DISPLAY = 5;
|
||||
|
||||
function LabelColumn({
|
||||
labels,
|
||||
value,
|
||||
color = 'primary',
|
||||
}: LabelColumnProps): JSX.Element {
|
||||
const visibleLabels =
|
||||
labels.length > MAX_LABELS_TO_DISPLAY ? labels.slice(0, 3) : labels;
|
||||
const remainingLabels =
|
||||
labels.length > MAX_LABELS_TO_DISPLAY ? labels.slice(3) : [];
|
||||
|
||||
return (
|
||||
<div className={styles.labelColumn} data-testid="label-column">
|
||||
{visibleLabels.map((label) => (
|
||||
<LabelTag key={label} label={label} color={color} value={value} />
|
||||
))}
|
||||
{remainingLabels.length > 0 && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Badge
|
||||
color={color}
|
||||
className={styles.labelBadge}
|
||||
variant="outline"
|
||||
data-testid="label-overflow-badge"
|
||||
>
|
||||
+{remainingLabels.length}
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
className={styles.labelPopover}
|
||||
data-testid="label-popover"
|
||||
>
|
||||
{labels.map((label) => (
|
||||
<Badge
|
||||
key={label}
|
||||
color={color}
|
||||
className={styles.labelBadgePopover}
|
||||
variant="outline"
|
||||
data-testid={`label-popover-item-${label}`}
|
||||
>
|
||||
{getLabelAndValueContent(label, value?.[label])}
|
||||
</Badge>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LabelColumn;
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from './LabelColumn';
|
||||
export type { LabelColumnProps } from './LabelColumn';
|
||||
@@ -1,4 +0,0 @@
|
||||
.lastUpdated {
|
||||
font-size: 12px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
|
||||
import LastUpdatedText from './LastUpdatedText';
|
||||
|
||||
describe('LastUpdatedText', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return null when updatedAt is null', () => {
|
||||
const { container } = render(<LastUpdatedText updatedAt={null} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should render formatted time distance', () => {
|
||||
const now = Date.now();
|
||||
const fiveMinutesAgo = now - 5 * 60 * 1000;
|
||||
|
||||
jest.setSystemTime(now);
|
||||
|
||||
render(<LastUpdatedText updatedAt={fiveMinutesAgo} />);
|
||||
|
||||
expect(screen.getByTestId('last-updated-text')).toHaveTextContent(
|
||||
/Updated.*5 minutes ago/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should have title with ISO formatted date', () => {
|
||||
const now = Date.now();
|
||||
const fiveMinutesAgo = now - 5 * 60 * 1000;
|
||||
|
||||
jest.setSystemTime(now);
|
||||
|
||||
render(<LastUpdatedText updatedAt={fiveMinutesAgo} />);
|
||||
|
||||
expect(screen.getByTestId('last-updated-text').title).toMatch(
|
||||
/^\d{4}-\d{2}-\d{2}/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should update text periodically', () => {
|
||||
const now = Date.now();
|
||||
|
||||
jest.setSystemTime(now);
|
||||
|
||||
render(<LastUpdatedText updatedAt={now} />);
|
||||
|
||||
expect(screen.getByTestId('last-updated-text')).toHaveTextContent(
|
||||
/Updated.*less than a minute ago/,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(61000);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('last-updated-text')).toHaveTextContent(
|
||||
/Updated.*1 minute ago/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should cleanup interval on unmount', () => {
|
||||
const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
|
||||
const now = Date.now();
|
||||
|
||||
jest.setSystemTime(now);
|
||||
|
||||
const { unmount } = render(<LastUpdatedText updatedAt={now} />);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(clearIntervalSpy).toHaveBeenCalled();
|
||||
clearIntervalSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should render with recent timestamp', () => {
|
||||
const now = Date.now();
|
||||
const tenSecondsAgo = now - 10 * 1000;
|
||||
|
||||
jest.setSystemTime(now);
|
||||
|
||||
render(<LastUpdatedText updatedAt={tenSecondsAgo} />);
|
||||
|
||||
expect(screen.getByTestId('last-updated-text')).toHaveTextContent(
|
||||
/Updated.*less than a minute ago/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with hour-old timestamp', () => {
|
||||
const now = Date.now();
|
||||
const oneHourAgo = now - 60 * 60 * 1000;
|
||||
|
||||
jest.setSystemTime(now);
|
||||
|
||||
render(<LastUpdatedText updatedAt={oneHourAgo} />);
|
||||
|
||||
expect(screen.getByTestId('last-updated-text')).toHaveTextContent(
|
||||
/Updated.*1 hour ago/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { formatDistanceToNow, formatISO } from 'date-fns';
|
||||
import styles from './LastUpdatedText.module.scss';
|
||||
|
||||
interface LastUpdatedTextProps {
|
||||
updatedAt: number | null;
|
||||
}
|
||||
|
||||
const LastUpdatedText = memo(function LastUpdatedText({
|
||||
updatedAt,
|
||||
}: LastUpdatedTextProps): JSX.Element | null {
|
||||
const [text, setText] = useState('');
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const lastUpdatedAtDate = useMemo(() => {
|
||||
if (!updatedAt) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
try {
|
||||
return formatISO(updatedAt);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
return 'Failed to parse date.';
|
||||
}
|
||||
}, [updatedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!updatedAt) {
|
||||
setText('');
|
||||
return;
|
||||
}
|
||||
|
||||
const updateText = (): void => {
|
||||
setText(formatDistanceToNow(updatedAt, { addSuffix: true }));
|
||||
};
|
||||
|
||||
updateText();
|
||||
intervalRef.current = setInterval(updateText, 1000);
|
||||
|
||||
return (): void => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [updatedAt]);
|
||||
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={styles.lastUpdated}
|
||||
title={lastUpdatedAtDate}
|
||||
data-testid="last-updated-text"
|
||||
>
|
||||
Updated {text}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
export default LastUpdatedText;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './LastUpdatedText';
|
||||
@@ -1,50 +0,0 @@
|
||||
.statCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-1);
|
||||
padding: var(--spacing-4) var(--spacing-7);
|
||||
background: var(--l2-background);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
min-width: 80px;
|
||||
height: 58px;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
background-color 0.15s ease;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.statCardClickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.statCardActive {
|
||||
border-color: var(--primary);
|
||||
background: color-mix(in srgb, var(--primary) 10%, var(--l2-background));
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--l2-foreground);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--l1-foreground);
|
||||
line-height: 1.2;
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import StatCard from './StatCard';
|
||||
|
||||
describe('StatCard', () => {
|
||||
it('should render label and value', () => {
|
||||
render(<StatCard label="Firing" value={5} />);
|
||||
|
||||
expect(screen.getByTestId('stat-card-label')).toHaveTextContent('Firing');
|
||||
expect(screen.getByTestId('stat-card-value')).toHaveTextContent('5');
|
||||
});
|
||||
|
||||
it('should apply custom color to value', () => {
|
||||
render(<StatCard label="Firing" value={5} color="red" />);
|
||||
|
||||
expect(screen.getByTestId('stat-card-value')).toHaveStyle({ color: 'red' });
|
||||
});
|
||||
|
||||
it('should not have button role when onClick is not provided', () => {
|
||||
render(<StatCard label="Firing" value={5} />);
|
||||
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have button role when onClick is provided', () => {
|
||||
const onClick = jest.fn();
|
||||
|
||||
render(<StatCard label="Firing" value={5} onClick={onClick} />);
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onClick with exclusive: false on regular click', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = jest.fn();
|
||||
|
||||
render(<StatCard label="Firing" value={5} onClick={onClick} />);
|
||||
|
||||
await user.click(screen.getByTestId('stat-card'));
|
||||
|
||||
expect(onClick).toHaveBeenCalledWith({ exclusive: false });
|
||||
});
|
||||
|
||||
it('should call onClick with exclusive: true on alt+click', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = jest.fn();
|
||||
|
||||
render(<StatCard label="Firing" value={5} onClick={onClick} />);
|
||||
|
||||
await user.keyboard('{Alt>}');
|
||||
await user.click(screen.getByTestId('stat-card'));
|
||||
await user.keyboard('{/Alt}');
|
||||
|
||||
expect(onClick).toHaveBeenCalledWith({ exclusive: true });
|
||||
});
|
||||
|
||||
it('should call onClick on Enter key press', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = jest.fn();
|
||||
|
||||
render(<StatCard label="Firing" value={5} onClick={onClick} />);
|
||||
|
||||
const card = screen.getByTestId('stat-card');
|
||||
card.focus();
|
||||
await user.keyboard('{Enter}');
|
||||
|
||||
expect(onClick).toHaveBeenCalledWith({ exclusive: false });
|
||||
});
|
||||
|
||||
it('should call onClick on Space key press', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = jest.fn();
|
||||
|
||||
render(<StatCard label="Firing" value={5} onClick={onClick} />);
|
||||
|
||||
const card = screen.getByTestId('stat-card');
|
||||
card.focus();
|
||||
await user.keyboard(' ');
|
||||
|
||||
expect(onClick).toHaveBeenCalledWith({ exclusive: false });
|
||||
});
|
||||
|
||||
it('should be focusable when onClick is provided', () => {
|
||||
render(<StatCard label="Firing" value={5} onClick={jest.fn()} />);
|
||||
|
||||
expect(screen.getByTestId('stat-card')).toHaveAttribute('tabindex', '0');
|
||||
});
|
||||
|
||||
it('should not be focusable when onClick is not provided', () => {
|
||||
render(<StatCard label="Firing" value={5} />);
|
||||
|
||||
expect(screen.getByTestId('stat-card')).not.toHaveAttribute('tabindex');
|
||||
});
|
||||
|
||||
it('should not have color style when color prop is not provided', () => {
|
||||
render(<StatCard label="Firing" value={5} />);
|
||||
|
||||
expect(screen.getByTestId('stat-card-value')).not.toHaveAttribute('style');
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import styles from './StatCard.module.scss';
|
||||
|
||||
export interface StatCardClickEvent {
|
||||
exclusive: boolean;
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
value: number;
|
||||
color?: string;
|
||||
onClick?: (event: StatCardClickEvent) => void;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
label,
|
||||
value,
|
||||
color,
|
||||
onClick,
|
||||
isActive,
|
||||
}: StatCardProps): JSX.Element {
|
||||
const cardClassName = [
|
||||
styles.statCard,
|
||||
onClick && styles.statCardClickable,
|
||||
isActive && styles.statCardActive,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
const handleClick = (e: React.MouseEvent): void => {
|
||||
if (onClick) {
|
||||
onClick({ exclusive: e.altKey });
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent): void => {
|
||||
if (onClick && (e.key === 'Enter' || e.key === ' ')) {
|
||||
e.preventDefault();
|
||||
onClick({ exclusive: e.altKey });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cardClassName}
|
||||
onClick={onClick ? handleClick : undefined}
|
||||
onKeyDown={onClick ? handleKeyDown : undefined}
|
||||
role={onClick ? 'button' : undefined}
|
||||
tabIndex={onClick ? 0 : undefined}
|
||||
data-testid="stat-card"
|
||||
>
|
||||
<span className={styles.statLabel} data-testid="stat-card-label">
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
className={styles.statValue}
|
||||
style={color ? { color } : undefined}
|
||||
data-testid="stat-card-value"
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatCard;
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from './StatCard';
|
||||
export type { StatCardClickEvent } from './StatCard';
|
||||
@@ -1,87 +0,0 @@
|
||||
import {
|
||||
STATE_ORDER,
|
||||
SEVERITY_ORDER,
|
||||
STATE_LABELS,
|
||||
STATE_COLORS,
|
||||
SEVERITY_COLORS,
|
||||
} from './constants';
|
||||
|
||||
describe('Alerts constants', () => {
|
||||
describe('STATE_ORDER', () => {
|
||||
it('should have correct order of states', () => {
|
||||
expect(STATE_ORDER).toStrictEqual([
|
||||
'firing',
|
||||
'pending',
|
||||
'inactive',
|
||||
'disabled',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have firing as highest priority', () => {
|
||||
expect(STATE_ORDER[0]).toBe('firing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SEVERITY_ORDER', () => {
|
||||
it('should have correct order of severities', () => {
|
||||
expect(SEVERITY_ORDER).toStrictEqual([
|
||||
'critical',
|
||||
'error',
|
||||
'warning',
|
||||
'info',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have critical as highest priority', () => {
|
||||
expect(SEVERITY_ORDER[0]).toBe('critical');
|
||||
});
|
||||
});
|
||||
|
||||
describe('STATE_LABELS', () => {
|
||||
it('should map firing to Firing', () => {
|
||||
expect(STATE_LABELS.firing).toBe('Firing');
|
||||
});
|
||||
|
||||
it('should map pending to Pending', () => {
|
||||
expect(STATE_LABELS.pending).toBe('Pending');
|
||||
});
|
||||
|
||||
it('should map inactive to OK', () => {
|
||||
expect(STATE_LABELS.inactive).toBe('OK');
|
||||
});
|
||||
|
||||
it('should map disabled to Disabled', () => {
|
||||
expect(STATE_LABELS.disabled).toBe('Disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('STATE_COLORS', () => {
|
||||
it('should have colors for all states', () => {
|
||||
expect(STATE_COLORS).toHaveProperty('firing');
|
||||
expect(STATE_COLORS).toHaveProperty('pending');
|
||||
expect(STATE_COLORS).toHaveProperty('inactive');
|
||||
expect(STATE_COLORS).toHaveProperty('disabled');
|
||||
});
|
||||
|
||||
it('should use CSS variables for colors', () => {
|
||||
Object.values(STATE_COLORS).forEach((color) => {
|
||||
expect(color).toMatch(/^var\(--/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SEVERITY_COLORS', () => {
|
||||
it('should have colors for all severities', () => {
|
||||
expect(SEVERITY_COLORS).toHaveProperty('critical');
|
||||
expect(SEVERITY_COLORS).toHaveProperty('error');
|
||||
expect(SEVERITY_COLORS).toHaveProperty('warning');
|
||||
expect(SEVERITY_COLORS).toHaveProperty('info');
|
||||
});
|
||||
|
||||
it('should use CSS variables for colors', () => {
|
||||
Object.values(SEVERITY_COLORS).forEach((color) => {
|
||||
expect(color).toMatch(/^var\(--/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
export const STATE_ORDER = ['firing', 'pending', 'inactive', 'disabled'];
|
||||
export const SEVERITY_ORDER = ['critical', 'error', 'warning', 'info'];
|
||||
|
||||
export const STATE_LABELS: Record<string, string> = {
|
||||
firing: 'Firing',
|
||||
pending: 'Pending',
|
||||
inactive: 'OK',
|
||||
disabled: 'Disabled',
|
||||
};
|
||||
|
||||
export const STATE_COLORS: Record<string, string> = {
|
||||
firing: 'var(--bg-cherry-500)',
|
||||
pending: 'var(--bg-amber-500)',
|
||||
inactive: 'var(--bg-forest-500)',
|
||||
disabled: 'var(--l2-foreground)',
|
||||
};
|
||||
|
||||
export const SEVERITY_COLORS: Record<string, string> = {
|
||||
critical: 'var(--bg-cherry-500)',
|
||||
error: 'var(--bg-cherry-400)',
|
||||
warning: 'var(--bg-amber-500)',
|
||||
info: 'var(--bg-robin-500)',
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
export { default as StatCard } from './StatCard';
|
||||
export type { StatCardClickEvent } from './StatCard';
|
||||
export { default as LastUpdatedText } from './LastUpdatedText';
|
||||
export { default as LabelColumn } from './LabelColumn';
|
||||
export type { LabelColumnProps } from './LabelColumn';
|
||||
export {
|
||||
STATE_ORDER,
|
||||
SEVERITY_ORDER,
|
||||
STATE_LABELS,
|
||||
STATE_COLORS,
|
||||
SEVERITY_COLORS,
|
||||
} from './constants';
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
} from './TanStackTableStateContext';
|
||||
import {
|
||||
FlatItem,
|
||||
SortState,
|
||||
TableRowContext,
|
||||
TanStackTableHandle,
|
||||
TanStackTableProps,
|
||||
@@ -101,7 +100,6 @@ function TanStackTableInner<TData>(
|
||||
onRowClick,
|
||||
onRowClickNewTab,
|
||||
onRowDeactivate,
|
||||
onSort,
|
||||
activeRowIndex,
|
||||
renderExpandedRow,
|
||||
getRowCanExpand,
|
||||
@@ -129,10 +127,10 @@ function TanStackTableInner<TData>(
|
||||
const {
|
||||
page,
|
||||
limit,
|
||||
setPage: internalSetPage,
|
||||
setLimit: internalSetLimit,
|
||||
setPage,
|
||||
setLimit,
|
||||
orderBy,
|
||||
setOrderBy: internalSetOrderBy,
|
||||
setOrderBy,
|
||||
expanded,
|
||||
setExpanded,
|
||||
} = useTableParams(enableQueryParams, {
|
||||
@@ -140,30 +138,6 @@ function TanStackTableInner<TData>(
|
||||
limit: pagination?.defaultLimit,
|
||||
});
|
||||
|
||||
const setPage = useCallback(
|
||||
(p: number) => {
|
||||
internalSetPage(p);
|
||||
pagination?.onPageChange?.(p);
|
||||
},
|
||||
[internalSetPage, pagination],
|
||||
);
|
||||
|
||||
const setLimit = useCallback(
|
||||
(l: number) => {
|
||||
internalSetLimit(l);
|
||||
pagination?.onLimitChange?.(l);
|
||||
},
|
||||
[internalSetLimit, pagination],
|
||||
);
|
||||
|
||||
const setOrderBy = useCallback(
|
||||
(sort: SortState | null) => {
|
||||
internalSetOrderBy(sort);
|
||||
onSort?.(sort);
|
||||
},
|
||||
[internalSetOrderBy, onSort],
|
||||
);
|
||||
|
||||
const isGrouped = (groupBy?.length ?? 0) > 0;
|
||||
|
||||
const {
|
||||
@@ -633,16 +607,14 @@ function TanStackTableInner<TData>(
|
||||
setPage(p);
|
||||
}}
|
||||
/>
|
||||
{(pagination.showPageSize ?? true) && (
|
||||
<div className={viewStyles.paginationPageSize}>
|
||||
<ComboboxSimple
|
||||
value={limit?.toString()}
|
||||
defaultValue="10"
|
||||
onChange={(value): void => setLimit(+value)}
|
||||
items={paginationPageSizeItems}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={viewStyles.paginationPageSize}>
|
||||
<ComboboxSimple
|
||||
value={limit?.toString()}
|
||||
defaultValue="10"
|
||||
onChange={(value): void => setLimit(+value)}
|
||||
items={paginationPageSizeItems}
|
||||
/>
|
||||
</div>
|
||||
{suffixPaginationContent}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -117,10 +117,6 @@ export type PaginationProps = {
|
||||
defaultLimit?: number;
|
||||
showTotalCount?: boolean;
|
||||
totalCountLabel?: string;
|
||||
/** @default true */
|
||||
showPageSize?: boolean;
|
||||
onPageChange?: (page: number) => void;
|
||||
onLimitChange?: (limit: number) => void;
|
||||
};
|
||||
|
||||
export type TanstackTableQueryParamsConfig = {
|
||||
@@ -164,8 +160,6 @@ export type TanStackTableProps<TData> = {
|
||||
/** Called when ctrl+click or cmd+click on a row */
|
||||
onRowClickNewTab?: (row: TData, itemKey: string) => void;
|
||||
onRowDeactivate?: () => void;
|
||||
/** Called when sort state changes */
|
||||
onSort?: (sort: SortState | null) => void;
|
||||
activeRowIndex?: number;
|
||||
renderExpandedRow?: (
|
||||
row: TData,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui';
|
||||
import { Tooltip, TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import { Copy } from '@signozhq/icons';
|
||||
import './CopyIconButton.styles.scss';
|
||||
|
||||
@@ -19,20 +19,22 @@ function CopyIconButton({
|
||||
: 'Copy to clipboard';
|
||||
|
||||
return (
|
||||
<TooltipSimple title={tooltipTitle}>
|
||||
<span>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
className="mcp-copy-btn"
|
||||
prefix={<Copy size={14} />}
|
||||
onClick={onCopy}
|
||||
/>
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
<TooltipProvider>
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<span>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
className="mcp-copy-btn"
|
||||
prefix={<Copy size={14} />}
|
||||
onClick={onCopy}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
TabsRoot,
|
||||
TabsTrigger,
|
||||
} from '@signozhq/ui/tabs';
|
||||
import { TooltipSimple } from '@signozhq/ui';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import {
|
||||
Bookmark,
|
||||
CalendarClock,
|
||||
@@ -492,22 +497,27 @@ function SpanDetailsPanel({
|
||||
actions.push({
|
||||
key: 'dock-toggle',
|
||||
component: (
|
||||
<TooltipSimple
|
||||
title={isDocked ? 'Open as floating panel' : 'Dock at the bottom'}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={(): void =>
|
||||
onVariantChange(
|
||||
isDocked ? SpanDetailVariant.DIALOG : SpanDetailVariant.DOCKED,
|
||||
)
|
||||
}
|
||||
>
|
||||
{isDocked ? <Dock size={14} /> : <PanelBottom size={14} />}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={(): void =>
|
||||
onVariantChange(
|
||||
isDocked ? SpanDetailVariant.DIALOG : SpanDetailVariant.DOCKED,
|
||||
)
|
||||
}
|
||||
>
|
||||
{isDocked ? <Dock size={14} /> : <PanelBottom size={14} />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="dock-toggle-tooltip">
|
||||
{isDocked ? 'Open as floating panel' : 'Dock at the bottom'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import { useCopySpanLink } from 'hooks/trace/useCopySpanLink';
|
||||
import { Link } from 'lucide-react';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
@@ -16,17 +21,24 @@ export default function SpanLineActionButtons({
|
||||
|
||||
return (
|
||||
<div className="span-line-action-buttons">
|
||||
<TooltipSimple title="Copy Span Link">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={onSpanCopy}
|
||||
className="copy-span-btn"
|
||||
>
|
||||
<Link size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={onSpanCopy}
|
||||
className="copy-span-btn"
|
||||
>
|
||||
<Link size={14} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="span-line-action-tooltip">
|
||||
Copy Span Link
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,12 @@ import {
|
||||
} from 'react';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
@@ -104,24 +109,26 @@ const LazyEventDotPopover = memo(function LazyEventDotPopover({
|
||||
const eventTimeMs = event.timeUnixNano / 1e6;
|
||||
|
||||
return (
|
||||
<TooltipSimple
|
||||
open
|
||||
onOpenChange={(open: boolean): void => {
|
||||
if (!open) {
|
||||
setShowPopover(false);
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<EventTooltipContent
|
||||
eventName={event.name}
|
||||
timeOffsetMs={eventTimeMs - spanTimestamp}
|
||||
isError={isError}
|
||||
attributeMap={event.attributeMap || {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{dot}
|
||||
</TooltipSimple>
|
||||
<TooltipProvider>
|
||||
<Tooltip
|
||||
open
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
setShowPopover(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TooltipTrigger asChild>{dot}</TooltipTrigger>
|
||||
<TooltipContent className="span-hover-card-popover">
|
||||
<EventTooltipContent
|
||||
eventName={event.name}
|
||||
timeOffsetMs={eventTimeMs - spanTimestamp}
|
||||
isError={isError}
|
||||
attributeMap={event.attributeMap || {}}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -316,28 +323,40 @@ const SpanOverview = memo(function SpanOverview({
|
||||
|
||||
{/* Action buttons — shown on hover via CSS, right-aligned */}
|
||||
<span className="span-row-actions">
|
||||
<TooltipSimple title="Copy Span Link">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className="span-action-btn"
|
||||
onClick={onSpanCopy}
|
||||
>
|
||||
<Link size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<TooltipSimple title="Add to Trace Funnel">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className="span-action-btn"
|
||||
onClick={handleFunnelClick}
|
||||
>
|
||||
<ListPlus size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className="span-action-btn"
|
||||
onClick={onSpanCopy}
|
||||
>
|
||||
<Link size={12} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="span-action-tooltip">
|
||||
Copy Span Link
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className="span-action-btn"
|
||||
onClick={handleFunnelClick}
|
||||
>
|
||||
<ListPlus size={12} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="span-action-tooltip">
|
||||
Add to Trace Funnel
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
</div>
|
||||
</SpanHoverCard>
|
||||
|
||||
@@ -5614,10 +5614,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/ui@0.0.19":
|
||||
version "0.0.19"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/ui/-/ui-0.0.19.tgz#125cbfb9c6bc39ace7f9a99b2b3fdd291a6bf76e"
|
||||
integrity sha512-2q6aRxN/PR4PlR2xJZAREEuvLPiDFggfFKzCW2Z5vHVVbrgnvZHWD1jPUuwszfEg0ceH3UvkwqceO7wN4uRJAA==
|
||||
"@signozhq/ui@0.0.18":
|
||||
version "0.0.18"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/ui/-/ui-0.0.18.tgz#a96f843aea87d2a435ed0efc68d0a94eaae98baa"
|
||||
integrity sha512-1p3ALh76kafiz5yX7ReNKVcHDt2od7CcZD/Vx9i2adTwTeynkLJcEfVoXoJD3oh1kKTleooOiOjRyxlA7VzmSA==
|
||||
dependencies:
|
||||
"@chenglou/pretext" "^0.0.5"
|
||||
"@radix-ui/react-checkbox" "^1.2.3"
|
||||
|
||||
@@ -413,28 +413,6 @@ func (b *traceOperatorCTEBuilder) buildFinalQuery(ctx context.Context, selectFro
|
||||
}
|
||||
|
||||
func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFromCTE string) (*qbtypes.Statement, error) {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
// Select core fields
|
||||
sb.Select(
|
||||
"timestamp",
|
||||
"trace_id",
|
||||
"span_id",
|
||||
"name",
|
||||
"duration_nano",
|
||||
"parent_span_id",
|
||||
)
|
||||
|
||||
selectedFields := map[string]bool{
|
||||
"timestamp": true,
|
||||
"trace_id": true,
|
||||
"span_id": true,
|
||||
"name": true,
|
||||
"duration_nano": true,
|
||||
"parent_span_id": true,
|
||||
}
|
||||
|
||||
// Get keys for selectFields
|
||||
keySelectors := b.getKeySelectors()
|
||||
for _, field := range b.operator.SelectFields {
|
||||
keySelectors = append(keySelectors, &telemetrytypes.FieldKeySelector{
|
||||
@@ -444,13 +422,38 @@ func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFrom
|
||||
FieldDataType: field.FieldDataType,
|
||||
})
|
||||
}
|
||||
|
||||
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, keySelectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add selectFields using ColumnExpressionFor since we now have all base table columns
|
||||
coreFields := []string{"timestamp", "trace_id", "span_id", "name", "duration_nano", "parent_span_id"}
|
||||
selectedFields := map[string]bool{
|
||||
"timestamp": true,
|
||||
"trace_id": true,
|
||||
"span_id": true,
|
||||
"name": true,
|
||||
"duration_nano": true,
|
||||
"parent_span_id": true,
|
||||
}
|
||||
|
||||
// CH 25.12.5 distributed-analyzer regression (ClickHouse/ClickHouse#103508):
|
||||
// native columns from a Distributed-table-backed CTE get renamed col → col_0
|
||||
// in the shard block, making them invisible to the outer SELECT/ORDER BY.
|
||||
// Fix: rename every core field to a safe alias (_s_<col>) in the inner SELECT
|
||||
// so the analyzer never sees the original name as an ORDER BY target. The
|
||||
// outer SELECT re-exposes them under their original names.
|
||||
innerCoreExprs := make([]string, len(coreFields))
|
||||
outerCoreExprs := make([]string, len(coreFields))
|
||||
for i, f := range coreFields {
|
||||
innerCoreExprs[i] = fmt.Sprintf("%s AS _s_%s", f, f)
|
||||
outerCoreExprs[i] = fmt.Sprintf("_s_%s AS %s", f, f)
|
||||
}
|
||||
|
||||
innerSB := sqlbuilder.NewSelectBuilder()
|
||||
innerSB.Select(innerCoreExprs...)
|
||||
|
||||
var additionalSelectedFields []string
|
||||
for _, field := range b.operator.SelectFields {
|
||||
if selectedFields[field.Name] {
|
||||
continue
|
||||
@@ -461,41 +464,56 @@ func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFrom
|
||||
slog.String("field", field.Name), errors.Attr(err))
|
||||
continue
|
||||
}
|
||||
sb.SelectMore(colExpr)
|
||||
innerSB.SelectMore(colExpr)
|
||||
selectedFields[field.Name] = true
|
||||
additionalSelectedFields = append(additionalSelectedFields, field.Name)
|
||||
}
|
||||
|
||||
sb.From(selectFromCTE)
|
||||
|
||||
// Add order by support using ColumnExpressionFor
|
||||
orderApplied := false
|
||||
for _, orderBy := range b.operator.Order {
|
||||
if selectedFields[orderBy.Key.Name] {
|
||||
continue
|
||||
}
|
||||
colExpr, err := b.stmtBuilder.fm.ColumnExpressionFor(ctx, b.start, b.end, &orderBy.Key.TelemetryFieldKey, keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.OrderBy(fmt.Sprintf("%s %s", colExpr, orderBy.Direction.StringValue()))
|
||||
orderApplied = true
|
||||
innerSB.SelectMore(colExpr)
|
||||
selectedFields[orderBy.Key.Name] = true
|
||||
}
|
||||
|
||||
if !orderApplied {
|
||||
sb.OrderBy("timestamp DESC")
|
||||
innerSB.From(selectFromCTE)
|
||||
innerSQL, innerArgs := innerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// Outer SELECT: re-exposes core fields under their original names and acts as
|
||||
// a projection barrier so ORDER BY-only fields do not leak into the result.
|
||||
outerSB := sqlbuilder.NewSelectBuilder()
|
||||
outerSB.Select(outerCoreExprs...)
|
||||
for _, name := range additionalSelectedFields {
|
||||
outerSB.SelectMore(fmt.Sprintf("`%s`", name))
|
||||
}
|
||||
outerSB.From(fmt.Sprintf("(%s) AS t", innerSQL))
|
||||
|
||||
if len(b.operator.Order) > 0 {
|
||||
for _, orderBy := range b.operator.Order {
|
||||
outerSB.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue()))
|
||||
}
|
||||
} else {
|
||||
outerSB.OrderBy("timestamp DESC")
|
||||
}
|
||||
|
||||
if b.operator.Limit > 0 {
|
||||
sb.Limit(b.operator.Limit)
|
||||
outerSB.Limit(b.operator.Limit)
|
||||
} else {
|
||||
sb.Limit(100)
|
||||
outerSB.Limit(100)
|
||||
}
|
||||
|
||||
if b.operator.Offset > 0 {
|
||||
sb.Offset(b.operator.Offset)
|
||||
outerSB.Offset(b.operator.Offset)
|
||||
}
|
||||
|
||||
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
outerSQL, outerArgs := outerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return &qbtypes.Statement{
|
||||
Query: sql,
|
||||
Args: args,
|
||||
Query: outerSQL,
|
||||
Args: append(innerArgs, outerArgs...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <= ?), 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",
|
||||
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 _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id, `service.name` FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_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) AS t 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 <= ?), 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",
|
||||
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 _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id FROM A_INDIR_DESC_B) AS t 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 <= ?), 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",
|
||||
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 _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id FROM A_AND_B) AS t 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 <= ?), 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",
|
||||
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 _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id FROM A_OR_B) AS t 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 <= ?), 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",
|
||||
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 _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id FROM A_not_B) AS t 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,
|
||||
@@ -380,11 +380,72 @@ 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 <= ?), 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",
|
||||
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 _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_parent_span_id FROM A_DIR_DESC_B_AND_C_DIR_DESC_D) AS t 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,
|
||||
},
|
||||
{
|
||||
// order-by field (http.request.method) is not present in SelectFields;
|
||||
// it must be included in the inner SELECT so the outer ORDER BY can
|
||||
// reference it by alias, but must NOT appear in the outer SELECT list.
|
||||
name: "order by field not in select fields",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
operator: qbtypes.QueryBuilderTraceOperator{
|
||||
Expression: "A => B",
|
||||
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
||||
{
|
||||
Name: "service.name",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
},
|
||||
Order: []qbtypes.OrderBy{
|
||||
{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "http.request.method",
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
Limit: 10,
|
||||
},
|
||||
compositeQuery: &qbtypes.CompositeQuery{
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name = 'frontend'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name = 'backend'",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
// http.request.method is in the inner SELECT (so ORDER BY can reach it)
|
||||
// but is absent from the outer SELECT column list — only the ORDER BY clause references it.
|
||||
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 _s_timestamp AS timestamp, _s_trace_id AS trace_id, _s_span_id AS span_id, _s_name AS name, _s_duration_nano AS duration_nano, _s_parent_span_id AS parent_span_id, `service.name` FROM (SELECT timestamp AS _s_timestamp, trace_id AS _s_trace_id, span_id AS _s_span_id, name AS _s_name, duration_nano AS _s_duration_nano, parent_span_id AS _s_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`, attributes_string['http.request.method'] AS `http.request.method` FROM A_DIR_DESC_B) AS t ORDER BY `http.request.method` 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,
|
||||
},
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
|
||||
3
tests/fixtures/querier.py
vendored
3
tests/fixtures/querier.py
vendored
@@ -72,6 +72,7 @@ class TraceOperatorQuery:
|
||||
return_spans_from: str
|
||||
limit: int | None = None
|
||||
order: list[OrderBy] | None = None
|
||||
select_fields: list[TelemetryFieldKey] | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
spec: dict[str, Any] = {
|
||||
@@ -83,6 +84,8 @@ class TraceOperatorQuery:
|
||||
spec["limit"] = self.limit
|
||||
if self.order:
|
||||
spec["order"] = [o.to_dict() if hasattr(o, "to_dict") else o for o in self.order]
|
||||
if self.select_fields:
|
||||
spec["selectFields"] = [f.to_dict() for f in self.select_fields]
|
||||
return {"type": "builder_trace_operator", "spec": spec}
|
||||
|
||||
|
||||
|
||||
537
tests/integration/tests/querier/15_trace_operator.py
Normal file
537
tests/integration/tests/querier/15_trace_operator.py
Normal file
@@ -0,0 +1,537 @@
|
||||
"""
|
||||
Integration tests for TraceOperatorQuery (builder_trace_operator) through the
|
||||
/api/v5/query_range endpoint.
|
||||
|
||||
Covers:
|
||||
1. Basic trace operator (A => B) — returns matched spans from the correct trace.
|
||||
2. Order by a field absent from selectFields — must not return a server error.
|
||||
Guards against the ClickHouse NOT_FOUND_COLUMN_IN_BLOCK regression where
|
||||
ordering by a column absent from an outer SELECT caused a query failure.
|
||||
"""
|
||||
|
||||
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 (
|
||||
OrderBy,
|
||||
TelemetryFieldKey,
|
||||
TraceOperatorQuery,
|
||||
make_query_request,
|
||||
)
|
||||
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
|
||||
|
||||
|
||||
def _builder_query(name: str, filter_expr: str, limit: int = 100) -> dict:
|
||||
return {
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": name,
|
||||
"signal": "traces",
|
||||
"filter": {"expression": filter_expr},
|
||||
"limit": limit,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_trace_operator_query_basic(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert one parent span and one child span in the same trace.
|
||||
|
||||
Tests:
|
||||
A => B (parent has a direct child) returns the parent span (returnSpansFrom=A)
|
||||
from the correct trace.
|
||||
"""
|
||||
parent_trace_id = TraceIdGenerator.trace_id()
|
||||
parent_span_id = TraceIdGenerator.span_id()
|
||||
child_span_id = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=parent_trace_id,
|
||||
span_id=parent_span_id,
|
||||
parent_span_id="",
|
||||
name="parent-op",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "parent"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=2),
|
||||
trace_id=parent_trace_id,
|
||||
span_id=child_span_id,
|
||||
parent_span_id=parent_span_id,
|
||||
name="child-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'parent'"),
|
||||
_builder_query("B", "operation.type = 'child'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["data"]["trace_id"] == parent_trace_id
|
||||
assert rows[0]["data"]["name"] == "parent-op"
|
||||
|
||||
|
||||
def test_trace_operator_query_order_by_field_not_in_select_fields(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Two traces, each with a grandparent → middle → grandchild chain:
|
||||
Trace 1: grandparent (svc-a, http.method=POST) → middle → grandchild
|
||||
Trace 2: grandparent (svc-b, http.method=GET) → middle → grandchild
|
||||
|
||||
Tests:
|
||||
A -> B (indirect descendant) with selectFields=[service.name] and
|
||||
order=[http.method DESC], where http.method is NOT in selectFields.
|
||||
|
||||
1. Query succeeds (no NOT_FOUND_COLUMN_IN_BLOCK error from ClickHouse).
|
||||
2. Results are actually ordered: POST sorts before GET descending, so
|
||||
svc-a must come before svc-b.
|
||||
"""
|
||||
trace_id_1 = TraceIdGenerator.trace_id()
|
||||
trace_id_2 = TraceIdGenerator.trace_id()
|
||||
|
||||
gp_span_id_1 = TraceIdGenerator.span_id()
|
||||
mid_span_id_1 = TraceIdGenerator.span_id()
|
||||
gc_span_id_1 = TraceIdGenerator.span_id()
|
||||
|
||||
gp_span_id_2 = TraceIdGenerator.span_id()
|
||||
mid_span_id_2 = TraceIdGenerator.span_id()
|
||||
gc_span_id_2 = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
# Trace 1 — grandparent has http.method=POST (sorts first in DESC)
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=trace_id_1,
|
||||
span_id=gp_span_id_1,
|
||||
parent_span_id="",
|
||||
name="gp-op",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "grandparent", "http.method": "POST"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_1,
|
||||
span_id=mid_span_id_1,
|
||||
parent_span_id=gp_span_id_1,
|
||||
name="mid-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "middle"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=8),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_1,
|
||||
span_id=gc_span_id_1,
|
||||
parent_span_id=mid_span_id_1,
|
||||
name="gc-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "grandchild"},
|
||||
),
|
||||
# Trace 2 — grandparent has http.method=GET (sorts second in DESC)
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=trace_id_2,
|
||||
span_id=gp_span_id_2,
|
||||
parent_span_id="",
|
||||
name="gp-op",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-b"},
|
||||
attributes={"operation.type": "grandparent", "http.method": "GET"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=6),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_2,
|
||||
span_id=mid_span_id_2,
|
||||
parent_span_id=gp_span_id_2,
|
||||
name="mid-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-b"},
|
||||
attributes={"operation.type": "middle"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=5),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=gc_span_id_2,
|
||||
parent_span_id=mid_span_id_2,
|
||||
name="gc-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-b"},
|
||||
attributes={"operation.type": "grandchild"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'grandparent'"),
|
||||
_builder_query("B", "operation.type = 'grandchild'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A -> B", # indirect descendant
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
select_fields=[
|
||||
TelemetryFieldKey(name="service.name", field_data_type="string", field_context="resource"),
|
||||
],
|
||||
order=[
|
||||
# http.method is intentionally absent from select_fields
|
||||
OrderBy(
|
||||
key=TelemetryFieldKey(name="http.method", field_data_type="string", field_context="attribute"),
|
||||
direction="desc",
|
||||
),
|
||||
],
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
|
||||
# Both grandparent spans must be returned
|
||||
assert len(rows) == 2
|
||||
|
||||
# Ordering: POST > GET in DESC — svc-a (POST) must come before svc-b (GET)
|
||||
assert rows[0]["data"]["service.name"] == "svc-a", f"Expected svc-a (POST) first in http.method DESC order, got {rows[0]['data']['service.name']}"
|
||||
assert rows[1]["data"]["service.name"] == "svc-b", f"Expected svc-b (GET) second in http.method DESC order, got {rows[1]['data']['service.name']}"
|
||||
|
||||
|
||||
def test_trace_operator_query_order_by_select_field(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Two traces each with a parent → child pair; the parents have distinct durations
|
||||
(5 s and 1 s).
|
||||
|
||||
Tests:
|
||||
A => B with order=[duration_nano DESC] and no explicit selectFields.
|
||||
1. Query succeeds (no NOT_FOUND_COLUMN_IN_BLOCK error).
|
||||
2. Longer-duration parent (5 s) appears before the shorter one (1 s).
|
||||
"""
|
||||
trace_id_1 = TraceIdGenerator.trace_id()
|
||||
trace_id_2 = TraceIdGenerator.trace_id()
|
||||
parent_span_id_1 = TraceIdGenerator.span_id()
|
||||
child_span_id_1 = TraceIdGenerator.span_id()
|
||||
parent_span_id_2 = TraceIdGenerator.span_id()
|
||||
child_span_id_2 = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=trace_id_1,
|
||||
span_id=parent_span_id_1,
|
||||
parent_span_id="",
|
||||
name="parent-long",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-long"},
|
||||
attributes={"operation.type": "parent"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_1,
|
||||
span_id=child_span_id_1,
|
||||
parent_span_id=parent_span_id_1,
|
||||
name="child-long",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-long"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=8),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=parent_span_id_2,
|
||||
parent_span_id="",
|
||||
name="parent-short",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-short"},
|
||||
attributes={"operation.type": "parent"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=child_span_id_2,
|
||||
parent_span_id=parent_span_id_2,
|
||||
name="child-short",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-short"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'parent'"),
|
||||
_builder_query("B", "operation.type = 'child'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
order=[
|
||||
OrderBy(
|
||||
key=TelemetryFieldKey(name="duration_nano", field_context="span"),
|
||||
direction="desc",
|
||||
),
|
||||
],
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
|
||||
assert len(rows) == 2
|
||||
# DESC: 5 s parent first, 1 s parent second
|
||||
assert rows[0]["data"]["name"] == "parent-long", f"Expected parent-long (5s) first in duration_nano DESC, got {rows[0]['data']['name']}"
|
||||
assert rows[1]["data"]["name"] == "parent-short", f"Expected parent-short (1s) second in duration_nano DESC, got {rows[1]['data']['name']}"
|
||||
|
||||
|
||||
def test_trace_operator_query_order_by_non_core_field_in_select(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Two traces each with a parent → child pair; parents have distinct http.method
|
||||
values (POST and GET).
|
||||
|
||||
Tests:
|
||||
A => B with selectFields=[http.method] and order=[http.method DESC].
|
||||
1. Query succeeds (no NOT_FOUND_COLUMN_IN_BLOCK error).
|
||||
2. http.method is present in every result row (it is in selectFields).
|
||||
3. Results are ordered DESC — POST before GET.
|
||||
"""
|
||||
trace_id_1 = TraceIdGenerator.trace_id()
|
||||
trace_id_2 = TraceIdGenerator.trace_id()
|
||||
parent_span_id_1 = TraceIdGenerator.span_id()
|
||||
child_span_id_1 = TraceIdGenerator.span_id()
|
||||
parent_span_id_2 = TraceIdGenerator.span_id()
|
||||
child_span_id_2 = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_1,
|
||||
span_id=parent_span_id_1,
|
||||
parent_span_id="",
|
||||
name="parent-post",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-post"},
|
||||
attributes={"operation.type": "parent", "http.method": "POST"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_1,
|
||||
span_id=child_span_id_1,
|
||||
parent_span_id=parent_span_id_1,
|
||||
name="child-post",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-post"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=8),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_2,
|
||||
span_id=parent_span_id_2,
|
||||
parent_span_id="",
|
||||
name="parent-get",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-get"},
|
||||
attributes={"operation.type": "parent", "http.method": "GET"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=child_span_id_2,
|
||||
parent_span_id=parent_span_id_2,
|
||||
name="child-get",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-get"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
http_method_field = TelemetryFieldKey(
|
||||
name="http.method",
|
||||
field_data_type="string",
|
||||
field_context="attribute",
|
||||
)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'parent'"),
|
||||
_builder_query("B", "operation.type = 'child'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
select_fields=[http_method_field],
|
||||
order=[OrderBy(key=http_method_field, direction="desc")],
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
|
||||
assert len(rows) == 2
|
||||
# http.method must be present in every row (it is in selectFields)
|
||||
for row in rows:
|
||||
assert "http.method" in row["data"], f"http.method missing from row: {row['data']}"
|
||||
# DESC: POST before GET
|
||||
assert rows[0]["data"]["http.method"] == "POST", f"Expected POST first in http.method DESC, got {rows[0]['data']['http.method']}"
|
||||
assert rows[1]["data"]["http.method"] == "GET", f"Expected GET second in http.method DESC, got {rows[1]['data']['http.method']}"
|
||||
Reference in New Issue
Block a user