Compare commits

..

9 Commits

Author SHA1 Message Date
Naman Verma
d3f8d76f98 Merge branch 'main' into nv/7069 2026-02-25 12:39:50 +05:30
Naman Verma
557ac59694 Merge branch 'main' into nv/7069 2026-02-25 09:02:06 +05:30
Naman Verma
7d74b5e637 chore: add uint8 to consume so that has_restarts is read 2026-02-24 23:41:09 +05:30
Naman Verma
860c5e42cb feat: agg tables should use the basic query with check for restarts 2026-02-24 19:32:33 +05:30
Naman Verma
7e6ce8dea2 fix: rate expr was missing an OVER rate_window 2026-02-24 19:32:00 +05:30
Naman Verma
c948b04fa5 Merge branch 'main' into nv/7069 2026-02-24 17:57:04 +05:30
Naman Verma
d6ba98148a fix: use column for latest val based on table being used 2026-02-23 20:26:55 +05:30
Naman Verma
953b9e8216 feat: proper expr for rate 2026-02-23 20:25:36 +05:30
Naman Verma
212f5f4120 feat: use start time in temporal aggregation 2026-02-23 17:06:23 +05:30
29 changed files with 310 additions and 705 deletions

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.113.0
image: signoz/signoz:v0.112.1
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port
@@ -213,7 +213,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.142.1
entrypoint:
- /bin/sh
command:
@@ -241,7 +241,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.113.0
image: signoz/signoz:v0.112.1
ports:
- "8080:8080" # signoz port
volumes:
@@ -139,7 +139,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.142.1
entrypoint:
- /bin/sh
command:
@@ -167,7 +167,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.113.0}
image: signoz/signoz:${VERSION:-v0.112.1}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -204,7 +204,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -229,7 +229,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.113.0}
image: signoz/signoz:${VERSION:-v0.112.1}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -132,7 +132,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -157,7 +157,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -86,13 +86,8 @@ function LogDetailInner({
const handleClickOutside = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
// Don't close if clicking on drawer content, overlays, or portal elements
if (
target.closest('[data-log-detail-ignore="true"]') ||
target.closest('.cm-tooltip-autocomplete') ||
target.closest('.drawer-popover') ||
target.closest('.query-status-popover')
) {
// Don't close if clicking on explicitly ignored regions
if (target.closest('[data-log-detail-ignore="true"]')) {
return;
}
@@ -405,11 +400,7 @@ function LogDetailInner({
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip
title={removeEscapeCharacters(log?.body)}
placement="left"
mouseLeaveDelay={0}
>
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
@@ -475,7 +466,6 @@ function LogDetailInner({
title="Show Filters"
placement="topLeft"
aria-label="Show Filters"
mouseLeaveDelay={0}
>
<Button
className="action-btn"
@@ -491,7 +481,6 @@ function LogDetailInner({
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
mouseLeaveDelay={0}
>
<Button
className="action-btn"

View File

@@ -27,11 +27,7 @@ function AddToQueryHOC({
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className={cx('addToQueryContainer', fontSize)} onClick={handleQueryAdd}>
<Popover
overlayClassName="drawer-popover"
placement="top"
content={popOverContent}
>
<Popover placement="top" content={popOverContent}>
{children}
</Popover>
</div>

View File

@@ -32,7 +32,6 @@ function CopyClipboardHOC({
<span onClick={onClick} role="presentation" tabIndex={-1}>
<Popover
placement="top"
overlayClassName="drawer-popover"
content={<span style={{ fontSize: '0.9rem' }}>{tooltipText}</span>}
>
{children}

View File

@@ -1328,10 +1328,7 @@ function QuerySearch({
)}
<div className="query-where-clause-editor-container">
<Tooltip
title={<div data-log-detail-ignore="true">{getTooltipContent()}</div>}
placement="left"
>
<Tooltip title={getTooltipContent()} placement="left">
<a
href="https://signoz.io/docs/userguide/search-syntax/"
target="_blank"

View File

@@ -9,7 +9,6 @@ import {
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
enqueueDescendantsOfVariable,
enqueueFetchOfAllVariables,
@@ -32,9 +31,6 @@ function DashboardVariableSelection(): JSX.Element | null {
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
const { dashboardVariables } = useDashboardVariables();
const dashboardId = useDashboardVariablesSelector(
(state) => state.dashboardId,
);
const sortedVariablesArray = useDashboardVariablesSelector(
(state) => state.sortedVariablesArray,
);
@@ -100,28 +96,6 @@ function DashboardVariableSelection(): JSX.Element | null {
updateUrlVariable(name || id, value);
}
// Synchronously update the external store with the new variable value so that
// child variables see the updated parent value when they refetch, rather than
// waiting for setSelectedDashboard → useEffect → updateDashboardVariablesStore.
const updatedVariables = { ...dashboardVariables };
if (updatedVariables[id]) {
updatedVariables[id] = {
...updatedVariables[id],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
if (updatedVariables[name]) {
updatedVariables[name] = {
...updatedVariables[name],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
updateDashboardVariablesStore({ dashboardId, variables: updatedVariables });
setSelectedDashboard((prev) => {
if (prev) {
const oldVariables = { ...prev?.data.variables };
@@ -156,12 +130,10 @@ function DashboardVariableSelection(): JSX.Element | null {
return prev;
});
// Cascade: enqueue query-type descendants for refetching.
// Safe to call synchronously now that the store already has the updated value.
// Cascade: enqueue query-type descendants for refetching
enqueueDescendantsOfVariable(name);
},
[
dashboardId,
dashboardVariables,
updateLocalStorageDashboardVariables,
updateUrlVariable,

View File

@@ -5,7 +5,7 @@ import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQ
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useVariableFetchState } from 'hooks/dashboard/useVariableFetchState';
import sortValues from 'lib/dashboardVariables/sortVariableValues';
import { isArray, isEmpty } from 'lodash-es';
import { isArray, isEmpty, isString } from 'lodash-es';
import { AppState } from 'store/reducers';
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -54,7 +54,7 @@ function QueryVariableInput({
onChange,
onDropdownVisibleChange,
handleClear,
getDefaultValue,
applyDefaultIfNeeded,
} = useDashboardVariableSelectHelper({
variableData,
optionsData,
@@ -68,93 +68,81 @@ function QueryVariableInput({
try {
setErrorMessage(null);
// This is just a check given the previously undefined typed name prop. Not significant
// This will be changed when we change the schema
// TODO: @AshwinBhatkal Perses
if (!variableData.name) {
return;
}
// if the response is not an array, premature return
if (
!variablesRes?.variableValues ||
!Array.isArray(variablesRes?.variableValues)
variablesRes?.variableValues &&
Array.isArray(variablesRes?.variableValues)
) {
return;
}
const sortedNewOptions = sortValues(
variablesRes.variableValues,
variableData.sort,
);
const sortedOldOptions = sortValues(optionsData, variableData.sort);
// if options are the same as before, no need to update state or check for selected value validity
// ! selectedValue needs to be set in the first pass though, as options are initially empty array and we need to apply default if needed
// Expecatation is that when oldOptions are not empty, then there is always some selectedValue
if (areArraysEqual(sortedNewOptions, sortedOldOptions)) {
return;
}
setOptionsData(sortedNewOptions);
let isSelectedValueMissingInNewOptions = false;
// Check if currently selected value(s) are present in the new options list
if (isArray(variableData.selectedValue)) {
isSelectedValueMissingInNewOptions = variableData.selectedValue.some(
(val) => !sortedNewOptions.includes(val),
const newOptionsData = sortValues(
variablesRes?.variableValues,
variableData.sort,
);
} else if (
variableData.selectedValue &&
!sortedNewOptions.includes(variableData.selectedValue)
) {
isSelectedValueMissingInNewOptions = true;
}
// If multi-select with ALL option enabled, and ALL is currently selected, we want to maintain that state and select all new options
// This block does not depend on selected value because of ALL and also because we would only come here if options are different from the previous
if (
variableData.multiSelect &&
variableData.showALLOption &&
variableData.allSelected &&
isSelectedValueMissingInNewOptions
) {
onValueUpdate(variableData.name, variableData.id, sortedNewOptions, true);
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
// Update tempSelection to maintain ALL state when dropdown is open
if (tempSelection !== undefined) {
setTempSelection(sortedNewOptions.map((option) => option.toString()));
}
return;
}
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
let valueNotInList = false;
const value = variableData.selectedValue;
let allSelected = false;
if (isArray(variableData.selectedValue)) {
variableData.selectedValue.forEach((val) => {
if (!newOptionsData.includes(val)) {
valueNotInList = true;
}
});
} else if (
isString(variableData.selectedValue) &&
!newOptionsData.includes(variableData.selectedValue)
) {
valueNotInList = true;
}
if (variableData.multiSelect) {
const { selectedValue } = variableData;
allSelected =
sortedNewOptions.length > 0 &&
Array.isArray(selectedValue) &&
sortedNewOptions.every((option) => selectedValue.includes(option));
}
if (variableData.name && (valueNotInList || variableData.allSelected)) {
if (
variableData.allSelected &&
variableData.multiSelect &&
variableData.showALLOption
) {
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(
variableData.name,
variableData.id,
newOptionsData,
true,
);
}
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
} else {
const defaultValue = getDefaultValue(sortedNewOptions);
if (defaultValue !== undefined) {
onValueUpdate(
variableData.name,
variableData.id,
defaultValue,
allSelected,
);
// Update tempSelection to maintain ALL state when dropdown is open
if (tempSelection !== undefined) {
setTempSelection(newOptionsData.map((option) => option.toString()));
}
} else {
const value = variableData.selectedValue;
let allSelected = false;
if (variableData.multiSelect) {
const { selectedValue } = variableData;
allSelected =
newOptionsData.length > 0 &&
Array.isArray(selectedValue) &&
newOptionsData.every((option) => selectedValue.includes(option));
}
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
}
}
}
setOptionsData(newOptionsData);
// Apply default if no value is selected (e.g., new variable, first load)
applyDefaultIfNeeded(newOptionsData);
}
}
} catch (e) {
@@ -167,7 +155,7 @@ function QueryVariableInput({
onValueUpdate,
tempSelection,
setTempSelection,
getDefaultValue,
applyDefaultIfNeeded,
],
);

View File

@@ -1,6 +1,5 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { act, render } from '@testing-library/react';
import * as dashboardVariablesStoreModule from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
dashboardVariablesStore,
setDashboardVariablesStore,
@@ -11,7 +10,6 @@ import {
IDashboardVariablesStoreState,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import {
enqueueDescendantsOfVariable,
enqueueFetchOfAllVariables,
initializeVariableFetchStore,
} from 'providers/Dashboard/store/variableFetchStore';
@@ -19,17 +17,6 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
import DashboardVariableSelection from '../DashboardVariableSelection';
// Mutable container to capture the onValueUpdate callback from VariableItem
const mockVariableItemCallbacks: {
onValueUpdate?: (
name: string,
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
haveCustomValuesSelected?: boolean,
) => void;
} = {};
// Mock providers/Dashboard/Dashboard
const mockSetSelectedDashboard = jest.fn();
const mockUpdateLocalStorageDashboardVariables = jest.fn();
@@ -69,14 +56,10 @@ jest.mock('react-redux', () => ({
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
}));
// VariableItem mock captures the onValueUpdate prop for use in onValueUpdate tests
// Mock VariableItem to avoid rendering complexity
jest.mock('../VariableItem', () => ({
__esModule: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: (props: any): JSX.Element => {
mockVariableItemCallbacks.onValueUpdate = props.onValueUpdate;
return <div data-testid="variable-item" />;
},
default: (): JSX.Element => <div data-testid="variable-item" />,
}));
function createVariable(
@@ -217,162 +200,4 @@ describe('DashboardVariableSelection', () => {
expect(initializeVariableFetchStore).not.toHaveBeenCalled();
expect(enqueueFetchOfAllVariables).not.toHaveBeenCalled();
});
describe('onValueUpdate', () => {
let updateStoreSpy: jest.SpyInstance;
beforeEach(() => {
resetStore();
jest.clearAllMocks();
// Real implementation pass-through — we just want to observe calls
updateStoreSpy = jest.spyOn(
dashboardVariablesStoreModule,
'updateDashboardVariablesStore',
);
});
afterEach(() => {
updateStoreSpy.mockRestore();
});
it('updates dashboardVariablesStore synchronously before enqueueDescendantsOfVariable', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
},
});
render(<DashboardVariableSelection />);
const callOrder: string[] = [];
updateStoreSpy.mockImplementation(() => {
callOrder.push('updateDashboardVariablesStore');
});
(enqueueDescendantsOfVariable as jest.Mock).mockImplementation(() => {
callOrder.push('enqueueDescendantsOfVariable');
});
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
expect(callOrder).toEqual([
'updateDashboardVariablesStore',
'enqueueDescendantsOfVariable',
]);
});
it('passes updated variable value to dashboardVariablesStore', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({
name: 'env',
id: 'env-id',
order: 0,
selectedValue: 'staging',
}),
},
});
render(<DashboardVariableSelection />);
// Clear spy calls that happened during setup/render
updateStoreSpy.mockClear();
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
expect(updateStoreSpy).toHaveBeenCalledWith(
expect.objectContaining({
dashboardId: 'dash-1',
variables: expect.objectContaining({
env: expect.objectContaining({
selectedValue: 'production',
allSelected: false,
}),
}),
}),
);
});
it('calls enqueueDescendantsOfVariable synchronously without a timer', () => {
jest.useFakeTimers();
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
},
});
render(<DashboardVariableSelection />);
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
// Must be called immediately — no timer advancement needed
expect(enqueueDescendantsOfVariable).toHaveBeenCalledWith('env');
jest.useRealTimers();
});
it('propagates allSelected and haveCustomValuesSelected to the store', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({
name: 'env',
id: 'env-id',
order: 0,
multiSelect: true,
showALLOption: true,
}),
},
});
render(<DashboardVariableSelection />);
updateStoreSpy.mockClear();
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
['production', 'staging'],
true,
false,
);
});
expect(updateStoreSpy).toHaveBeenCalledWith(
expect.objectContaining({
variables: expect.objectContaining({
env: expect.objectContaining({
selectedValue: ['production', 'staging'],
allSelected: true,
haveCustomValuesSelected: false,
}),
}),
}),
);
});
});
});

View File

@@ -1,275 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { QueryClient, QueryClientProvider } from 'react-query';
import { act, render, waitFor } from '@testing-library/react';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { variableFetchStore } from 'providers/Dashboard/store/variableFetchStore';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import QueryVariableInput from '../QueryVariableInput';
jest.mock('api/dashboard/variables/dashboardVariablesQuery');
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
}));
function createTestQueryClient(): QueryClient {
return new QueryClient({
defaultOptions: {
queries: { retry: false, refetchOnWindowFocus: false },
},
});
}
function Wrapper({
children,
queryClient,
}: {
children: React.ReactNode;
queryClient: QueryClient;
}): JSX.Element {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
function createVariable(
overrides: Partial<IDashboardVariable> = {},
): IDashboardVariable {
return {
id: 'env-id',
name: 'env',
description: '',
type: 'QUERY',
sort: 'DISABLED',
showALLOption: false,
multiSelect: false,
order: 0,
queryValue: 'SELECT env FROM table',
...overrides,
};
}
/** Put the named variable into 'loading' state so useQuery fires on mount */
function setVariableLoading(name: string): void {
variableFetchStore.update((draft) => {
draft.states[name] = 'loading';
draft.cycleIds[name] = (draft.cycleIds[name] || 0) + 1;
});
}
function resetFetchStore(): void {
variableFetchStore.set(() => ({
states: {},
lastUpdated: {},
cycleIds: {},
}));
}
describe('QueryVariableInput - getOptions logic', () => {
const mockOnValueUpdate = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
resetFetchStore();
});
afterEach(() => {
resetFetchStore();
});
it('applies default value (first option) when selectedValue is empty on first load', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging', 'dev'] },
});
const variable = createVariable({ selectedValue: undefined });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
'production', // first option by default
false,
);
});
});
it('keeps existing selectedValue when it is present in new options', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
const variable = createVariable({ selectedValue: 'staging' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
'staging',
false,
);
});
});
it('selects all new options when allSelected=true and value is missing from new options', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
const variable = createVariable({
selectedValue: ['old-env'],
allSelected: true,
multiSelect: true,
showALLOption: true,
});
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
['production', 'staging'],
true,
);
});
});
it('does not call onValueUpdate a second time when options have not changed', async () => {
const mockQueryFn = jest.fn().mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
(dashboardVariablesQuery as jest.Mock).mockImplementation(mockQueryFn);
const variable = createVariable({ selectedValue: 'production' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
// Wait for first fetch and onValueUpdate call
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
});
mockOnValueUpdate.mockClear();
// Trigger a second fetch cycle with the same API response
act(() => {
variableFetchStore.update((draft) => {
draft.states['env'] = 'revalidating';
draft.cycleIds['env'] = (draft.cycleIds['env'] || 0) + 1;
});
});
// Wait for second query to fire
await waitFor(() => {
expect(mockQueryFn).toHaveBeenCalledTimes(2);
});
// Options are unchanged, so onValueUpdate must not fire again
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
it('does not call onValueUpdate when API returns a non-array response', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: null },
});
const variable = createVariable({ selectedValue: 'production' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(dashboardVariablesQuery).toHaveBeenCalled();
});
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
it('does not fire the query when variableData.name is empty', () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production'] },
});
// Variable with no name — useVariableFetchState will be called with ''
// and the query key will have an empty name, leaving it disabled
const variable = createVariable({ name: '' });
// Note: we do NOT put it in 'loading' state since name is empty
// (no variableFetchStore entry for '' means isVariableFetching=false)
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
expect(dashboardVariablesQuery).not.toHaveBeenCalled();
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
});

View File

@@ -46,9 +46,6 @@ interface UseDashboardVariableSelectHelperReturn {
applyDefaultIfNeeded: (
overrideOptions?: (string | number | boolean)[],
) => void;
getDefaultValue: (
overrideOptions?: (string | number | boolean)[],
) => string | string[] | undefined;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -251,6 +248,5 @@ export function useDashboardVariableSelectHelper({
defaultValue,
onChange,
applyDefaultIfNeeded,
getDefaultValue,
};
}

View File

@@ -121,23 +121,9 @@ function BodyTitleRenderer({
return (
<TitleWrapper onClick={handleNodeClick}>
{typeof value !== 'object' && (
<span
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
onMouseDown={(e): void => e.preventDefault()}
>
<Dropdown
menu={menu}
trigger={['click']}
dropdownRender={(originNode): React.ReactNode => (
<div data-log-detail-ignore="true">{originNode}</div>
)}
>
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
</Dropdown>
</span>
<Dropdown menu={menu} trigger={['click']}>
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
</Dropdown>
)}
{title.toString()}{' '}
{!parentIsArray && typeof value !== 'object' && (

View File

@@ -13,7 +13,7 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
<span className="field-renderer-container">
{dataType && newField && logType ? (
<>
<Tooltip placement="left" title={newField} mouseLeaveDelay={0}>
<Tooltip placement="left" title={newField}>
<Typography.Text ellipsis className="label">
{newField}{' '}
</Typography.Text>

View File

@@ -1,6 +1,7 @@
import { ReactNode, useState } from 'react';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import type { InputRef } from 'antd';
import {
Button,
Collapse,
@@ -52,6 +53,17 @@ function Overview({
);
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
const focusTimerRef = useRef<ReturnType<typeof setTimeout>>();
const searchInputRef = useCallback((node: InputRef | null) => {
clearTimeout(focusTimerRef.current);
if (node) {
focusTimerRef.current = setTimeout(() => node.focus(), 100);
}
}, []);
useEffect(() => (): void => clearTimeout(focusTimerRef.current), []);
const isDarkMode = useIsDarkMode();
const options: EditorProps['options'] = {
@@ -196,7 +208,7 @@ function Overview({
<>
{isSearchVisible && (
<Input
autoFocus
ref={searchInputRef}
placeholder="Search for a field..."
className="search-input"
value={fieldSearchInput}

View File

@@ -245,7 +245,7 @@ function TableView({
<Typography.Text>{renderedField}</Typography.Text>
{traceId && (
<Tooltip title="Inspect in Trace" mouseLeaveDelay={0}>
<Tooltip title="Inspect in Trace">
<Button
className="periscope-btn"
onClick={(

View File

@@ -139,7 +139,8 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
{tableColumns
.filter((column) => column.key)
.map((column) => {
const isDragColumn = column.key !== 'expand';
const isDragColumn =
column.key !== 'expand' && column.key !== 'state-indicator';
return (
<TableHeaderCellStyled

View File

@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next';
import { Form } from 'antd';
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import isEqual from 'lodash-es/isEqual';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
@@ -30,7 +30,7 @@ function TagFilterInput({
};
return (
<QueryBuilderSearchV2
<QueryBuilderSearch
query={query}
onChange={onQueryChange}
placeholder={placeholder}

View File

@@ -86,7 +86,7 @@ jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
}));
const BASE_URL = ENVIRONMENT.baseURL;
const attributeKeysURL = `${BASE_URL}/api/v3/filter_suggestions`;
const attributeKeysURL = `${BASE_URL}/api/v3/autocomplete/attribute_keys`;
describe('PipelinePage container test', () => {
beforeAll(() => {
@@ -333,34 +333,26 @@ describe('PipelinePage container test', () => {
ctx.json({
status: 'success',
data: {
attributes: [
attributeKeys: [
{
key: 'otelServiceName',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
{
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
{
key: 'service.instance.id',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
{
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
},
{
key: 'service.name',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
],
},

View File

@@ -973,7 +973,6 @@ function QueryBuilderSearchV2(
return (
<div className="query-builder-search-v2">
<Select
data-testid={'qb-search-select'}
ref={selectRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(hasPopupContainer ? { getPopupContainer: popupContainer } : {})}

View File

@@ -1,17 +1,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { MetricsType } from 'container/MetricsApplication/constant';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
import { AppState } from 'store/reducers';
@@ -56,20 +54,6 @@ export const useActiveLog = (): UseActiveLog => {
const [activeLog, setActiveLog] = useState<ILog | null>(null);
// Close drawer/clear active log when query in URL changes
const urlQuery = useUrlQuery();
const compositeQuery = urlQuery.get(QueryParams.compositeQuery) ?? '';
const prevQueryRef = useRef<string | null>(null);
useEffect(() => {
if (
prevQueryRef.current !== null &&
prevQueryRef.current !== compositeQuery
) {
setActiveLog(null);
}
prevQueryRef.current = compositeQuery;
}, [compositeQuery]);
const onSetDetailedLogData = useCallback(
(logData: ILog) => {
dispatch({

View File

@@ -87,7 +87,7 @@ func (m *module) ListPromotedAndIndexedPaths(ctx context.Context) ([]promotetype
}
func (m *module) listPromotedPaths(ctx context.Context) ([]string, error) {
paths, err := m.metadataStore.GetPromotedPaths(ctx)
paths, err := m.metadataStore.ListPromotedPaths(ctx)
if err != nil {
return nil, err
}
@@ -142,7 +142,7 @@ func (m *module) PromoteAndIndexPaths(
pathsStr = append(pathsStr, path.Path)
}
existingPromotedPaths, err := m.metadataStore.GetPromotedPaths(ctx, pathsStr...)
existingPromotedPaths, err := m.metadataStore.ListPromotedPaths(ctx, pathsStr...)
if err != nil {
return err
}

View File

@@ -128,7 +128,7 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
case *time.Time:
ts = v.UnixMilli()
case *float64, *float32, *int64, *int32, *uint64, *uint32:
case *float64, *float32, *int64, *int32, *uint64, *uint32, *uint8:
val := numericAsFloat(reflect.ValueOf(ptr).Elem().Interface())
if m := aggRe.FindStringSubmatch(name); m != nil {
id, _ := strconv.Atoi(m[1])
@@ -148,7 +148,7 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
})
}
case **float64, **float32, **int64, **int32, **uint64, **uint32:
case **float64, **float32, **int64, **int32, **uint64, **uint32, **uint8:
tempVal := reflect.ValueOf(ptr)
if tempVal.IsValid() && !tempVal.IsNil() && !tempVal.Elem().IsNil() {
val := numericAsFloat(tempVal.Elem().Elem().Interface())

View File

@@ -10,6 +10,7 @@ import (
"github.com/ClickHouse/clickhouse-go/v2/lib/chcol"
schemamigrator "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
"github.com/SigNoz/signoz-otel-collector/constants"
"github.com/SigNoz/signoz-otel-collector/utils"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
@@ -112,7 +113,7 @@ func (t *telemetryMetaStore) buildBodyJSONPaths(ctx context.Context,
for _, fieldKey := range fieldKeys {
promotedKey := strings.Split(fieldKey.Name, telemetrytypes.ArraySep)[0]
fieldKey.Materialized = promoted[promotedKey]
fieldKey.Materialized = promoted.Contains(promotedKey)
fieldKey.Indexes = indexes[fieldKey.Name]
}
@@ -294,6 +295,33 @@ func (t *telemetryMetaStore) ListLogsJSONIndexes(ctx context.Context, filters ..
return indexes, nil
}
func (t *telemetryMetaStore) ListPromotedPaths(ctx context.Context, paths ...string) (map[string]struct{}, error) {
sb := sqlbuilder.Select("path").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
pathConditions := []string{}
for _, path := range paths {
pathConditions = append(pathConditions, sb.Equal("path", path))
}
sb.Where(sb.Or(pathConditions...))
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.WrapInternalf(err, CodeFailLoadPromotedPaths, "failed to load promoted paths")
}
defer rows.Close()
next := make(map[string]struct{})
for rows.Next() {
var path string
if err := rows.Scan(&path); err != nil {
return nil, errors.WrapInternalf(err, CodeFailLoadPromotedPaths, "failed to scan promoted path")
}
next[path] = struct{}{}
}
return next, nil
}
// TODO(Piyush): Remove this if not used in future
func (t *telemetryMetaStore) ListJSONValues(ctx context.Context, path string, limit int) (*telemetrytypes.TelemetryFieldValues, bool, error) {
path = CleanPathPrefixes(path)
@@ -456,12 +484,11 @@ func derefValue(v any) any {
return val.Interface()
}
// IsPathPromoted checks if a specific path is promoted (Column Evolution table: field_name for logs body).
// IsPathPromoted checks if a specific path is promoted
func (t *telemetryMetaStore) IsPathPromoted(ctx context.Context, path string) (bool, error) {
split := strings.Split(path, telemetrytypes.ArraySep)
pathSegment := split[0]
query := fmt.Sprintf("SELECT 1 FROM %s.%s WHERE signal = ? AND column_name = ? AND field_context = ? AND field_name = ? LIMIT 1", DBName, PromotedPathsTableName)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, telemetrytypes.SignalLogs, telemetrylogs.LogsV2BodyPromotedColumn, telemetrytypes.FieldContextBody, pathSegment)
query := fmt.Sprintf("SELECT 1 FROM %s.%s WHERE path = ? LIMIT 1", DBName, PromotedPathsTableName)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, split[0])
if err != nil {
return false, errors.WrapInternalf(err, CodeFailCheckPathPromoted, "failed to check if path %s is promoted", path)
}
@@ -470,24 +497,15 @@ func (t *telemetryMetaStore) IsPathPromoted(ctx context.Context, path string) (b
return rows.Next(), nil
}
// GetPromotedPaths returns promoted paths from the Column Evolution table (field_name for logs body).
func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...string) (map[string]bool, error) {
sb := sqlbuilder.Select("field_name").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
conditions := []string{
sb.Equal("signal", telemetrytypes.SignalLogs),
sb.Equal("column_name", telemetrylogs.LogsV2BodyPromotedColumn),
sb.Equal("field_context", telemetrytypes.FieldContextBody),
sb.NotEqual("field_name", "__all__"),
// GetPromotedPaths checks if a specific path is promoted
func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...string) (*utils.ConcurrentSet[string], error) {
sb := sqlbuilder.Select("path").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
pathConditions := []string{}
for _, path := range paths {
split := strings.Split(path, telemetrytypes.ArraySep)
pathConditions = append(pathConditions, sb.Equal("path", split[0]))
}
if len(paths) > 0 {
pathArgs := make([]interface{}, len(paths))
for i, path := range paths {
split := strings.Split(path, telemetrytypes.ArraySep)
pathArgs[i] = split[0]
}
conditions = append(conditions, sb.In("field_name", pathArgs))
}
sb.Where(sb.And(conditions...))
sb.Where(sb.Or(pathConditions...))
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
@@ -496,13 +514,13 @@ func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...stri
}
defer rows.Close()
promotedPaths := make(map[string]bool)
promotedPaths := utils.NewConcurrentSet[string]()
for rows.Next() {
var fieldName string
if err := rows.Scan(&fieldName); err != nil {
var path string
if err := rows.Scan(&path); err != nil {
return nil, errors.WrapInternalf(err, CodeFailCheckPathPromoted, "failed to scan promoted path")
}
promotedPaths[fieldName] = true
promotedPaths.Insert(path)
}
return promotedPaths, nil
@@ -516,22 +534,21 @@ func CleanPathPrefixes(path string) string {
return path
}
// PromotePaths inserts promoted paths into the Column Evolution table (same schema as signoz-otel-collector metadata_migrations).
func (t *telemetryMetaStore) PromotePaths(ctx context.Context, paths ...string) error {
batch, err := t.telemetrystore.ClickhouseDB().PrepareBatch(ctx,
fmt.Sprintf("INSERT INTO %s.%s (signal, column_name, column_type, field_context, field_name, version, release_time) VALUES", DBName,
fmt.Sprintf("INSERT INTO %s.%s (path, created_at) VALUES", DBName,
PromotedPathsTableName))
if err != nil {
return errors.WrapInternalf(err, CodeFailedToPrepareBatch, "failed to prepare batch")
}
releaseTime := time.Now().UnixNano()
nowMs := uint64(time.Now().UnixMilli())
for _, p := range paths {
trimmed := strings.TrimSpace(p)
if trimmed == "" {
continue
}
if err := batch.Append(telemetrytypes.SignalLogs, telemetrylogs.LogsV2BodyPromotedColumn, "JSON()", telemetrytypes.FieldContextBody, trimmed, 0, releaseTime); err != nil {
if err := batch.Append(trimmed, nowMs); err != nil {
_ = batch.Abort()
return errors.WrapInternalf(err, CodeFailedToAppendPath, "failed to append path")
}

View File

@@ -7,7 +7,6 @@ const (
AttributesMetadataTableName = "distributed_attributes_metadata"
AttributesMetadataLocalTableName = "attributes_metadata"
PathTypesTableName = otelcollectorconst.DistributedPathTypesTable
// Column Evolution table stores promoted paths as (signal, column_name, field_context, field_name); see signoz-otel-collector metadata_migrations.
PromotedPathsTableName = "distributed_column_evolution_metadata"
PromotedPathsTableName = otelcollectorconst.DistributedPromotedPathsTable
SkipIndexTableName = "system.data_skipping_indices"
)

View File

@@ -26,6 +26,9 @@ const (
IncreaseMultiTemporality = `IF(LOWER(temporality) LIKE LOWER('delta'), %s, multiIf(row_number() OVER rate_window = 1, nan, (%s - lagInFrame(%s, 1) OVER rate_window) < 0, %s, (%s - lagInFrame(%s, 1) OVER rate_window))) AS per_series_value`
OthersMultiTemporality = `IF(LOWER(temporality) LIKE LOWER('delta'), %s, %s) AS per_series_value`
RateWithStartTs = "multiIf(row_number() OVER rate_window = 1 AND earliest_start_ts < %d, NAN, row_number() OVER rate_window = 1, sum_all_values / (ts - earliest_start_ts), earliest_start_ts = lagInFrame(latest_start_ts, 1) OVER rate_window, (sum_all_values - lagInFrame(latest_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window), sum_all_values / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value"
IncreaseWithStartTs = "multiIf(row_number() OVER rate_window = 1 AND earliest_start_ts < %d, NAN, row_number() OVER rate_window = 1, sum_all_values, earliest_start_ts = lagInFrame(latest_start_ts, 1) OVER rate_window, sum_all_values - lagInFrame(latest_value, 1) OVER rate_window, sum_all_values) AS per_series_value"
)
type MetricQueryStatementBuilder struct {
@@ -330,6 +333,9 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggregationCTE(
if query.Aggregations[0].Temporality == metrictypes.Delta {
return b.buildTemporalAggDelta(ctx, start, end, query, timeSeriesCTE, timeSeriesCTEArgs)
} else if query.Aggregations[0].Temporality != metrictypes.Multiple {
if query.Aggregations[0].TimeAggregation == metrictypes.TimeAggregationIncrease || query.Aggregations[0].TimeAggregation == metrictypes.TimeAggregationRate {
return b.buildTemporalAggCumulativeOrUnspecifiedWithStartTs(ctx, start, end, query, timeSeriesCTE, timeSeriesCTEArgs)
}
return b.buildTemporalAggCumulativeOrUnspecified(ctx, start, end, query, timeSeriesCTE, timeSeriesCTEArgs)
}
return b.buildTemporalAggForMultipleTemporalities(ctx, start, end, query, timeSeriesCTE, timeSeriesCTEArgs)
@@ -382,6 +388,115 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggDelta(
return fmt.Sprintf("__temporal_aggregation_cte AS (%s)", q), args, nil
}
func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecifiedWithStartTs(
ctx context.Context,
start, end uint64,
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
timeSeriesCTE string,
timeSeriesCTEArgs []any,
) (string, []any, error) {
tbl := WhichSamplesTableToUse(start, end, query.Aggregations[0].Type, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
if tbl == SamplesV4TableName {
return b.buildTemporalAggCumulativeOrUnspecifiedWithStartTsForSamplesTbl(ctx, start, end, query, timeSeriesCTE, timeSeriesCTEArgs)
}
return b.buildTemporalAggCumulativeOrUnspecified(ctx, start, end, query, timeSeriesCTE, timeSeriesCTEArgs)
}
func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecifiedWithStartTsForSamplesTbl(
_ context.Context,
start, end uint64,
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
timeSeriesCTE string,
timeSeriesCTEArgs []any,
) (string, []any, error) {
stepSec := int64(query.StepInterval.Seconds())
moreInfoQueryBuilder := sqlbuilder.NewSelectBuilder()
moreInfoQueryBuilder.Select("fingerprint")
moreInfoQueryBuilder.SelectMore(fmt.Sprintf(
"toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(%d)) AS ts",
stepSec,
))
moreInfoQueryBuilder.SelectMore("toDateTime(intDiv(start_timestamp_unix_milli, 1000)) AS start_ts")
for _, g := range query.GroupBy {
moreInfoQueryBuilder.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
aggCol, err := AggregationColumnForSamplesTable(start, end, query.Aggregations[0].Type, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
if err != nil {
return "", nil, err
}
moreInfoQueryBuilder.SelectMore(fmt.Sprintf("%s AS max_value", aggCol))
colWithLatestValue, err := AggregationColumnForSamplesTable(start, end, query.Aggregations[0].Type, query.Aggregations[0].Temporality, metrictypes.TimeAggregationLatest, query.Aggregations[0].TableHints)
if err != nil {
return "", nil, err
}
moreInfoQueryBuilder.SelectMore(fmt.Sprintf("%s AS latest_value", colWithLatestValue))
moreInfoQueryBuilder.SelectMore("max(unix_milli) AS latest_timestamp")
tbl := WhichSamplesTableToUse(start, end, query.Aggregations[0].Type, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
moreInfoQueryBuilder.From(fmt.Sprintf("%s.%s AS points", DBName, tbl))
moreInfoQueryBuilder.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
moreInfoQueryBuilder.Where(
moreInfoQueryBuilder.In("metric_name", query.Aggregations[0].MetricName),
moreInfoQueryBuilder.GTE("unix_milli", start),
moreInfoQueryBuilder.LT("unix_milli", end),
)
moreInfoQueryBuilder.GroupBy("fingerprint", "ts", "start_ts")
moreInfoQueryBuilder.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
moreInfoPerRowQuery, moreInfoPerRowArgs := moreInfoQueryBuilder.BuildWithFlavor(sqlbuilder.ClickHouse, timeSeriesCTEArgs...)
innerQueryBuilder := sqlbuilder.NewSelectBuilder()
innerQueryBuilder.Select("fingerprint")
innerQueryBuilder.SelectMore("ts")
for _, g := range query.GroupBy {
innerQueryBuilder.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
innerQueryBuilder.SelectMore("max(start_ts) AS latest_start_ts")
innerQueryBuilder.SelectMore("min(start_ts) AS earliest_start_ts")
innerQueryBuilder.SelectMore("sum(max_value) AS sum_all_values")
innerQueryBuilder.SelectMore("argMax(latest_value, latest_timestamp) AS latest_value")
innerQueryBuilder.From(fmt.Sprintf("(%s)", moreInfoPerRowQuery))
innerQueryBuilder.GroupBy("fingerprint", "ts")
innerQueryBuilder.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
innerQuery, innerQueryArgs := innerQueryBuilder.BuildWithFlavor(sqlbuilder.ClickHouse, moreInfoPerRowArgs...)
switch query.Aggregations[0].TimeAggregation {
case metrictypes.TimeAggregationRate:
rateExpr := fmt.Sprintf(RateWithStartTs, start)
wrapped := sqlbuilder.NewSelectBuilder()
wrapped.Select("ts")
wrapped.SelectMore("latest_value")
for _, g := range query.GroupBy {
wrapped.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
wrapped.SelectMore(rateExpr)
wrapped.From(fmt.Sprintf("(%s) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)", innerQuery))
q, args := wrapped.BuildWithFlavor(sqlbuilder.ClickHouse, moreInfoPerRowArgs...)
return fmt.Sprintf("__temporal_aggregation_cte AS (%s)", q), args, nil
case metrictypes.TimeAggregationIncrease:
incExpr := fmt.Sprintf(IncreaseWithStartTs, start)
wrapped := sqlbuilder.NewSelectBuilder()
wrapped.Select("ts")
wrapped.SelectMore("latest_value")
for _, g := range query.GroupBy {
wrapped.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
wrapped.SelectMore(incExpr)
wrapped.From(fmt.Sprintf("(%s) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)", innerQuery))
q, args := wrapped.BuildWithFlavor(sqlbuilder.ClickHouse, moreInfoPerRowArgs...)
return fmt.Sprintf("__temporal_aggregation_cte AS (%s)", q), args, nil
default:
return fmt.Sprintf("__temporal_aggregation_cte AS (%s)", innerQuery), innerQueryArgs, nil
}
}
func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
_ context.Context,
start, end uint64,
@@ -389,6 +504,7 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
timeSeriesCTE string,
timeSeriesCTEArgs []any,
) (string, []any, error) {
tbl := WhichSamplesTableToUse(start, end, query.Aggregations[0].Type, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
stepSec := int64(query.StepInterval.Seconds())
baseSb := sqlbuilder.NewSelectBuilder()
@@ -400,6 +516,9 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
for _, g := range query.GroupBy {
baseSb.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
if tbl != SamplesV4TableName {
baseSb.SelectMore("uniqMerge(num_start_timestamps) AS num_start_timestamps")
}
aggCol, err := AggregationColumnForSamplesTable(start, end, query.Aggregations[0].Type, query.Aggregations[0].Temporality, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
if err != nil {
@@ -407,7 +526,6 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
}
baseSb.SelectMore(fmt.Sprintf("%s AS per_series_value", aggCol))
tbl := WhichSamplesTableToUse(start, end, query.Aggregations[0].Type, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
baseSb.From(fmt.Sprintf("%s.%s AS points", DBName, tbl))
baseSb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
baseSb.Where(
@@ -428,6 +546,9 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
for _, g := range query.GroupBy {
wrapped.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
if tbl != SamplesV4TableName {
wrapped.SelectMore("num_start_timestamps > 1 AS has_restarts")
}
wrapped.SelectMore(fmt.Sprintf("%s AS per_series_value", RateTmpl))
wrapped.From(fmt.Sprintf("(%s) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)", innerQuery))
q, args := wrapped.BuildWithFlavor(sqlbuilder.ClickHouse, innerArgs...)
@@ -439,6 +560,9 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
for _, g := range query.GroupBy {
wrapped.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
if tbl != SamplesV4TableName {
wrapped.SelectMore("num_start_timestamps > 1 AS has_restarts")
}
wrapped.SelectMore(fmt.Sprintf("%s AS per_series_value", IncreaseTmpl))
wrapped.From(fmt.Sprintf("(%s) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)", innerQuery))
q, args := wrapped.BuildWithFlavor(sqlbuilder.ClickHouse, innerArgs...)
@@ -515,11 +639,12 @@ func (b *MetricQueryStatementBuilder) buildTemporalAggForMultipleTemporalities(
func (b *MetricQueryStatementBuilder) buildSpatialAggregationCTE(
_ context.Context,
_ uint64,
_ uint64,
start uint64,
end uint64,
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
_ map[string][]*telemetrytypes.TelemetryFieldKey,
) (string, []any, error) {
tbl := WhichSamplesTableToUse(start, end, query.Aggregations[0].Type, query.Aggregations[0].TimeAggregation, query.Aggregations[0].TableHints)
if query.Aggregations[0].SpaceAggregation.IsZero() {
return "", nil, errors.Newf(
errors.TypeInvalidInput,
@@ -534,6 +659,9 @@ func (b *MetricQueryStatementBuilder) buildSpatialAggregationCTE(
sb.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
}
sb.SelectMore(fmt.Sprintf("%s(per_series_value) AS value", query.Aggregations[0].SpaceAggregation.StringValue()))
if tbl != SamplesV4TableName {
sb.SelectMore("max(has_restarts) AS has_restarts")
}
sb.From("__temporal_aggregation_cte")
sb.Where(sb.EQ("isNaN(per_series_value)", 0))
if query.Aggregations[0].ValueFilter != nil {

View File

@@ -36,7 +36,7 @@ type MetadataStore interface {
ListLogsJSONIndexes(ctx context.Context, filters ...string) (map[string][]schemamigrator.Index, error)
// ListPromotedPaths lists the promoted paths.
GetPromotedPaths(ctx context.Context, paths ...string) (map[string]bool, error)
ListPromotedPaths(ctx context.Context, paths ...string) (map[string]struct{}, error)
// PromotePaths promotes the paths.
PromotePaths(ctx context.Context, paths ...string) error

View File

@@ -16,7 +16,7 @@ type MockMetadataStore struct {
RelatedValuesMap map[string][]string
AllValuesMap map[string]*telemetrytypes.TelemetryFieldValues
TemporalityMap map[string]metrictypes.Temporality
PromotedPathsMap map[string]bool
PromotedPathsMap map[string]struct{}
LogsJSONIndexesMap map[string][]schemamigrator.Index
LookupKeysMap map[telemetrytypes.MetricMetadataLookupKey]int64
}
@@ -28,7 +28,7 @@ func NewMockMetadataStore() *MockMetadataStore {
RelatedValuesMap: make(map[string][]string),
AllValuesMap: make(map[string]*telemetrytypes.TelemetryFieldValues),
TemporalityMap: make(map[string]metrictypes.Temporality),
PromotedPathsMap: make(map[string]bool),
PromotedPathsMap: make(map[string]struct{}),
LogsJSONIndexesMap: make(map[string][]schemamigrator.Index),
LookupKeysMap: make(map[telemetrytypes.MetricMetadataLookupKey]int64),
}
@@ -295,13 +295,13 @@ func (m *MockMetadataStore) SetTemporality(metricName string, temporality metric
// PromotePaths promotes the paths.
func (m *MockMetadataStore) PromotePaths(ctx context.Context, paths ...string) error {
for _, path := range paths {
m.PromotedPathsMap[path] = true
m.PromotedPathsMap[path] = struct{}{}
}
return nil
}
// GetPromotedPaths returns the promoted paths.
func (m *MockMetadataStore) GetPromotedPaths(ctx context.Context, paths ...string) (map[string]bool, error) {
// ListPromotedPaths lists the promoted paths.
func (m *MockMetadataStore) ListPromotedPaths(ctx context.Context, paths ...string) (map[string]struct{}, error) {
return m.PromotedPathsMap, nil
}