mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-12 16:00:46 +01:00
Compare commits
10 Commits
feat/monac
...
fix/uplot-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0940db0875 | ||
|
|
2616885d22 | ||
|
|
52dd57074e | ||
|
|
fe94b817db | ||
|
|
ea36032d96 | ||
|
|
6ecfa839f3 | ||
|
|
62d382b3cc | ||
|
|
cc07e2fa24 | ||
|
|
d7aa63f1bc | ||
|
|
c36b748370 |
@@ -1499,6 +1499,7 @@ components:
|
||||
- computeengine
|
||||
- gke
|
||||
- cloudstorage
|
||||
- cloudsql_mysql
|
||||
type: string
|
||||
CloudintegrationtypesServiceMetadata:
|
||||
properties:
|
||||
|
||||
@@ -388,6 +388,10 @@ function App(): JSX.Element {
|
||||
if (error?.name === 'AbortError') {
|
||||
return null;
|
||||
}
|
||||
// Ignore benign Monaco cancellation errors (name 'Canceled').
|
||||
if (error?.name === 'Canceled') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Drop the event if its level is 'warning' or 'info'
|
||||
if (event.level === 'warning' || event.level === 'info') {
|
||||
|
||||
@@ -2818,6 +2818,7 @@ export enum CloudintegrationtypesServiceIDDTO {
|
||||
computeengine = 'computeengine',
|
||||
gke = 'gke',
|
||||
cloudstorage = 'cloudstorage',
|
||||
cloudsql_mysql = 'cloudsql_mysql',
|
||||
}
|
||||
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
/**
|
||||
|
||||
@@ -437,6 +437,17 @@ describe('Create Alert Channel', () => {
|
||||
render(<CreateAlertChannels preType={ChannelType.GoogleChat} />);
|
||||
});
|
||||
|
||||
// paste instead of type: a per-keystroke re-render of the whole form
|
||||
// pushes these tests past the 5s jest timeout on slower CI runners
|
||||
async function fillField(
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
testId: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
await user.click(screen.getByTestId(testId));
|
||||
await user.paste(value);
|
||||
}
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Google Chat"', () => {
|
||||
expect(screen.getByText('Google Chat')).toBeInTheDocument();
|
||||
});
|
||||
@@ -463,14 +474,8 @@ describe('Create Alert Channel', () => {
|
||||
it('Should check if saving with a webhook url outside chat.googleapis.com displays error notification', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.type(
|
||||
screen.getByTestId('channel-name-textbox'),
|
||||
'gchat-channel',
|
||||
);
|
||||
await user.type(
|
||||
screen.getByTestId('webhook-url-textbox'),
|
||||
'https://example.com/webhook',
|
||||
);
|
||||
await fillField(user, 'channel-name-textbox', 'gchat-channel');
|
||||
await fillField(user, 'webhook-url-textbox', 'https://example.com/webhook');
|
||||
|
||||
await user.click(screen.getByTestId('save-channel-button'));
|
||||
|
||||
@@ -496,11 +501,8 @@ describe('Create Alert Channel', () => {
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.type(
|
||||
screen.getByTestId('channel-name-textbox'),
|
||||
'gchat-channel',
|
||||
);
|
||||
await user.type(screen.getByTestId('webhook-url-textbox'), validWebhookUrl);
|
||||
await fillField(user, 'channel-name-textbox', 'gchat-channel');
|
||||
await fillField(user, 'webhook-url-textbox', validWebhookUrl);
|
||||
|
||||
await user.click(screen.getByTestId('save-channel-button'));
|
||||
|
||||
|
||||
@@ -130,6 +130,28 @@ describe('Footer utils', () => {
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBeNull();
|
||||
});
|
||||
|
||||
it('when threshold channels are null', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
basicAlertState: {
|
||||
...args.basicAlertState,
|
||||
name: 'test name',
|
||||
},
|
||||
thresholdState: {
|
||||
...args.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...args.thresholdState.thresholds[0],
|
||||
channels: null as unknown as string[],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBe(
|
||||
'Please select at least one channel for each threshold or enable routing policies',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotificationSettingsProps', () => {
|
||||
|
||||
@@ -44,7 +44,8 @@ export function validateCreateAlertState(
|
||||
if (!threshold.label) {
|
||||
return 'Please enter a label for each threshold';
|
||||
}
|
||||
if (!notificationSettings.routingPolicies && !threshold.channels.length) {
|
||||
// this runs during render, so a throw here takes down the whole page
|
||||
if (!notificationSettings.routingPolicies && !threshold.channels?.length) {
|
||||
return 'Please select at least one channel for each threshold or enable routing policies';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,34 @@ describe('CreateAlertV2 utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThresholdStateFromAlertDef null channels', () => {
|
||||
it('falls back to an empty array so downstream consumers never see null', () => {
|
||||
const def: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: [
|
||||
{
|
||||
name: 'critical',
|
||||
target: 1,
|
||||
targetUnit: UniversalYAxisUnit.MINUTES,
|
||||
channels: null as unknown as string[],
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
op: AlertThresholdOperator.IS_ABOVE,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
getThresholdStateFromAlertDef(def).thresholds[0].channels,
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeOperator', () => {
|
||||
it.each([
|
||||
['1', AlertThresholdOperator.IS_ABOVE],
|
||||
|
||||
@@ -258,7 +258,9 @@ export function getThresholdStateFromAlertDef(
|
||||
recoveryThresholdValue: null,
|
||||
unit: threshold.targetUnit,
|
||||
color: getColorForThreshold(threshold.name),
|
||||
channels: threshold.channels,
|
||||
// rules created outside the UI can come back with a null channels
|
||||
// field; drop the guard once the API enforces the schema
|
||||
channels: threshold.channels ?? [],
|
||||
})) || [],
|
||||
selectedQuery: alertDef.condition.selectedQueryName || '',
|
||||
operator:
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface BaseConfigBuilderProps {
|
||||
panelType: PANEL_TYPES;
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
useExactTimeRange?: boolean;
|
||||
stepInterval?: number;
|
||||
isLogScale?: boolean;
|
||||
yAxisUnit?: string;
|
||||
@@ -46,6 +47,7 @@ export function buildBaseConfig({
|
||||
thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
useExactTimeRange,
|
||||
stepInterval,
|
||||
isLogScale,
|
||||
yAxisUnit,
|
||||
@@ -88,6 +90,7 @@ export function buildBaseConfig({
|
||||
time: true,
|
||||
min: minTimeScale,
|
||||
max: maxTimeScale,
|
||||
useExactTimeRange,
|
||||
logBase: isLogScale ? 10 : undefined,
|
||||
distribution: isLogScale
|
||||
? DistributionType.Logarithmic
|
||||
|
||||
@@ -82,7 +82,7 @@ export function getHostMetricsQueryPayload(
|
||||
start: number,
|
||||
end: number,
|
||||
): ReturnType<typeof getHostQueryPayload> {
|
||||
return getHostQueryPayload(host.hostName, start, end);
|
||||
return getHostQueryPayload(host.hostName, start, end, true);
|
||||
}
|
||||
|
||||
export { hostWidgetInfo };
|
||||
|
||||
@@ -562,13 +562,9 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
|
||||
op: '=',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 1`,
|
||||
},
|
||||
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -648,13 +644,9 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
|
||||
op: '=',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 0`,
|
||||
},
|
||||
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
|
||||
@@ -1208,13 +1208,9 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED}) > 0`,
|
||||
},
|
||||
legend: 'desired',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -1261,13 +1257,9 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_AVAILABLE}) > 0`,
|
||||
},
|
||||
legend: 'available',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import type { Having } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import type { Having as HavingV5 } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
|
||||
const buildSumGreaterThanZeroHaving = (
|
||||
metricKey: string,
|
||||
useV5HavingFormat: boolean,
|
||||
): Having[] | HavingV5 =>
|
||||
useV5HavingFormat
|
||||
? { expression: `sum(${metricKey}) > 0` }
|
||||
: [{ columnName: `SUM(${metricKey})`, op: '>', value: 0 }];
|
||||
|
||||
export const getPodQueryPayload = (
|
||||
clusterName: string,
|
||||
podName: string,
|
||||
@@ -1540,6 +1550,7 @@ export const getHostQueryPayload = (
|
||||
hostName: string,
|
||||
start: number,
|
||||
end: number,
|
||||
useV5HavingFormat = false,
|
||||
): GetQueryResultsProps[] => {
|
||||
const hostNameKey = 'host.name';
|
||||
const cpuTimeKey = 'system.cpu.time';
|
||||
@@ -1802,13 +1813,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${fsUsageKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
|
||||
legend: '{{mountpoint}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -1857,13 +1862,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${fsUsageKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
|
||||
legend: '{{mountpoint}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -2089,13 +2088,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${netIoKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(netIoKey, useV5HavingFormat),
|
||||
legend: '{{device}}::{{direction}}',
|
||||
limit: 30,
|
||||
orderBy: [],
|
||||
@@ -2551,13 +2544,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${diskOpsKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(diskOpsKey, useV5HavingFormat),
|
||||
legend: '{{device}}::{{direction}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -2626,13 +2613,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${diskPendingKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(diskPendingKey, useV5HavingFormat),
|
||||
legend: '{{device}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -2708,13 +2689,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${diskOpTimeKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(diskOpTimeKey, useV5HavingFormat),
|
||||
legend: '{{device}}::{{direction}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
|
||||
@@ -387,4 +387,42 @@ describe('useOptionsMenu', () => {
|
||||
expect(remaining).toHaveLength(seedColumns.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fieldsSelector.value drops legacy columns without a name', () => {
|
||||
it('excludes entries missing name while keeping valid columns', () => {
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: { data: { data: { keys: {} } } },
|
||||
isFetching: false,
|
||||
});
|
||||
(usePreferenceContext as jest.Mock).mockReturnValue({
|
||||
traces: {
|
||||
preferences: {
|
||||
columns: [
|
||||
{ name: 'body', fieldContext: 'log' },
|
||||
{ key: 'legacy-key-no-name', fieldContext: 'log' },
|
||||
{ name: 'timestamp', fieldContext: 'log' },
|
||||
],
|
||||
formatting: { format: 'table', maxLines: 1, fontSize: 'small' },
|
||||
},
|
||||
updateColumns: mockUpdateColumns,
|
||||
updateFormatting: mockUpdateFormatting,
|
||||
},
|
||||
logs: {
|
||||
preferences: { columns: [], formatting: {} },
|
||||
updateColumns: mockUpdateColumns,
|
||||
updateFormatting: mockUpdateFormatting,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useOptionsMenu({
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'count',
|
||||
}),
|
||||
);
|
||||
|
||||
const fields = result.current.config.fieldsSelector?.value ?? [];
|
||||
expect(fields.map((f) => f.name)).toStrictEqual(['body', 'timestamp']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -399,7 +399,7 @@ const useOptionsMenu = ({
|
||||
onReorder: reorderSelectColumns,
|
||||
},
|
||||
fieldsSelector: {
|
||||
value: preferences?.columns ?? [],
|
||||
value: preferences?.columns?.filter((item) => has(item, 'name')) ?? [],
|
||||
onFieldsChange: updateColumns,
|
||||
},
|
||||
format: {
|
||||
|
||||
@@ -80,7 +80,6 @@ function ResourceAttributesFilter({
|
||||
<div className="environment-selector">
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
key={selectedEnvironments.join('')}
|
||||
showSearch
|
||||
mode="multiple"
|
||||
value={selectedEnvironments}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { createMemoryHistory, MemoryHistory } from 'history';
|
||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||
import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
|
||||
import { encode } from 'js-base64';
|
||||
|
||||
import ResourceAttributesFilter from '../ResourceAttributesFilter';
|
||||
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: { search: '', pathname: '/' },
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('api/metrics/getResourceAttributes', () => ({
|
||||
getResourceAttributesTagKeys: jest.fn(),
|
||||
getResourceAttributesTagValues: jest.fn(),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line import/first, import/order
|
||||
import {
|
||||
getResourceAttributesTagKeys,
|
||||
getResourceAttributesTagValues,
|
||||
// eslint-disable-next-line import/newline-after-import
|
||||
} from 'api/metrics/getResourceAttributes';
|
||||
// eslint-disable-next-line import/first, import/order
|
||||
import history from 'lib/history';
|
||||
|
||||
const mockTagKeys = getResourceAttributesTagKeys as jest.MockedFunction<
|
||||
typeof getResourceAttributesTagKeys
|
||||
>;
|
||||
const mockTagValues = getResourceAttributesTagValues as jest.MockedFunction<
|
||||
typeof getResourceAttributesTagValues
|
||||
>;
|
||||
|
||||
function tagKeysPayload(keys: string[]): never {
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'ok',
|
||||
payload: {
|
||||
data: {
|
||||
attributeKeys: keys.map((key) => ({
|
||||
key,
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
})),
|
||||
},
|
||||
},
|
||||
} as unknown as never;
|
||||
}
|
||||
|
||||
function tagValuesPayload(values: string[]): never {
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'ok',
|
||||
payload: { data: { stringAttributeValues: values } },
|
||||
} as unknown as never;
|
||||
}
|
||||
|
||||
function seedUrl(queries: IResourceAttribute[], pathname: string): void {
|
||||
const location = history.location as { search: string; pathname: string };
|
||||
location.search = queries.length
|
||||
? `?resourceAttribute=${encode(JSON.stringify(queries))}`
|
||||
: '';
|
||||
location.pathname = pathname;
|
||||
}
|
||||
|
||||
function renderFilter(pathname: string): MemoryHistory {
|
||||
const routerHistory = createMemoryHistory({
|
||||
initialEntries: [`${pathname}${history.location.search}`],
|
||||
});
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Router history={routerHistory}>
|
||||
<ResourceProvider>{children}</ResourceProvider>
|
||||
</Router>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<ResourceAttributesFilter />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
return routerHistory;
|
||||
}
|
||||
|
||||
describe('ResourceAttributesFilter', () => {
|
||||
beforeEach(() => {
|
||||
mockTagKeys.mockReset();
|
||||
mockTagValues.mockReset();
|
||||
mockTagKeys.mockResolvedValue(
|
||||
tagKeysPayload(['resource_deployment.environment']),
|
||||
);
|
||||
mockTagValues.mockResolvedValue(tagValuesPayload(['production', 'staging']));
|
||||
seedUrl([], '/');
|
||||
});
|
||||
|
||||
it('shows every applied filter on the service map, including ones it cannot apply', async () => {
|
||||
seedUrl(
|
||||
[
|
||||
{
|
||||
id: 'svc',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'env',
|
||||
tagKey: 'resource_deployment.environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
},
|
||||
],
|
||||
ROUTES.SERVICE_MAP,
|
||||
);
|
||||
|
||||
renderFilter(ROUTES.SERVICE_MAP);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/service\.name/)).toBeInTheDocument(),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('resource-environment-filter')
|
||||
.querySelector('.ant-select-selection-item'),
|
||||
).toHaveTextContent('production'),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the environment dropdown open so more than one environment can be picked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderFilter('/services');
|
||||
|
||||
const environmentFilter = screen.getByTestId('resource-environment-filter');
|
||||
await user.click(
|
||||
environmentFilter.querySelector('input') as HTMLInputElement,
|
||||
);
|
||||
|
||||
await user.click(await screen.findByTitle('production'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByTitle('staging').closest('.ant-select-dropdown'),
|
||||
).not.toHaveClass('ant-select-dropdown-hidden'),
|
||||
);
|
||||
|
||||
await user.click(screen.getByTitle('staging'));
|
||||
|
||||
await waitFor(() => {
|
||||
const selected = Array.from(
|
||||
environmentFilter.querySelectorAll('.ant-select-selection-item-content'),
|
||||
).map((node) => node.textContent);
|
||||
expect(selected).toStrictEqual(['production', 'staging']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,10 @@
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { encode } from 'js-base64';
|
||||
|
||||
import { whilelistedKeys } from './config';
|
||||
import { ResourceContext } from './context';
|
||||
import {
|
||||
IResourceAttribute,
|
||||
@@ -195,16 +193,9 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
setOptionsData({ mode: undefined, options: [] });
|
||||
}, [dispatchQueries]);
|
||||
|
||||
const getVisibleQueries = useMemo(() => {
|
||||
if (pathname === ROUTES.SERVICE_MAP) {
|
||||
return queries.filter((query) => whilelistedKeys.includes(query.tagKey));
|
||||
}
|
||||
return queries;
|
||||
}, [queries, pathname]);
|
||||
|
||||
const value: IResourceAttributeProps = useMemo(
|
||||
() => ({
|
||||
queries: getVisibleQueries,
|
||||
queries,
|
||||
staging,
|
||||
handleClearAll,
|
||||
handleClose,
|
||||
@@ -227,7 +218,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
staging,
|
||||
selectedQuery,
|
||||
optionsData,
|
||||
getVisibleQueries,
|
||||
queries,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -504,22 +504,23 @@ describe('ResourceProvider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVisibleQueries (SERVICE_MAP filtering)', () => {
|
||||
it('filters queries down to whitelisted keys on SERVICE_MAP', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'a',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
tagKey: 'resource_k8s_cluster_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['prod'],
|
||||
},
|
||||
];
|
||||
describe('SERVICE_MAP', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'a',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
tagKey: 'resource_k8s_cluster_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['prod'],
|
||||
},
|
||||
];
|
||||
|
||||
it('exposes every query from the URL, including ones the map cannot apply', () => {
|
||||
mockLibHistory(
|
||||
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
|
||||
ROUTES.SERVICE_MAP,
|
||||
@@ -532,24 +533,10 @@ describe('ResourceProvider', () => {
|
||||
wrapper: createWrapper({ routerHistory }),
|
||||
});
|
||||
|
||||
expect(result.current.queries).toStrictEqual([seeded[1]]);
|
||||
expect(result.current.queries).toStrictEqual(seeded);
|
||||
});
|
||||
|
||||
it('returns all queries on non-SERVICE_MAP routes', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'a',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
tagKey: 'resource_k8s_cluster_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['prod'],
|
||||
},
|
||||
];
|
||||
mockLibHistory(
|
||||
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
|
||||
'/services',
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import { whilelistedKeys } from '../config';
|
||||
import { mappingWithRoutesAndKeys } from '../utils';
|
||||
import {
|
||||
filterServiceMapSupportedQueries,
|
||||
mappingWithRoutesAndKeys,
|
||||
} from '../utils';
|
||||
|
||||
describe('useResourceAttribute config', () => {
|
||||
describe('whilelistedKeys', () => {
|
||||
@@ -74,4 +77,29 @@ describe('useResourceAttribute config', () => {
|
||||
expect(result).toStrictEqual(allFilters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterServiceMapSupportedQueries', () => {
|
||||
const environmentQuery = {
|
||||
id: 'env',
|
||||
tagKey: 'resource_deployment_environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
};
|
||||
const serviceQuery = {
|
||||
id: 'svc',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
};
|
||||
|
||||
it('should keep only the queries the service map can filter on', () => {
|
||||
expect(
|
||||
filterServiceMapSupportedQueries([environmentQuery, serviceQuery]),
|
||||
).toStrictEqual([environmentQuery]);
|
||||
});
|
||||
|
||||
it('should return an empty list when no query is supported', () => {
|
||||
expect(filterServiceMapSupportedQueries([serviceQuery])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -281,3 +281,8 @@ export const mappingWithRoutesAndKeys = (
|
||||
}
|
||||
return filters;
|
||||
};
|
||||
|
||||
export const filterServiceMapSupportedQueries = (
|
||||
queries: IResourceAttribute[],
|
||||
): IResourceAttribute[] =>
|
||||
queries.filter((query) => whilelistedKeys.includes(query.tagKey));
|
||||
|
||||
@@ -42,6 +42,7 @@ export class UPlotScaleBuilder extends ConfigBuilder<
|
||||
logBase = 10,
|
||||
padMinBy = 0,
|
||||
padMaxBy = 0.05,
|
||||
useExactTimeRange = false,
|
||||
} = this.props;
|
||||
|
||||
// Special handling for time scales (X axis)
|
||||
@@ -58,14 +59,20 @@ export class UPlotScaleBuilder extends ConfigBuilder<
|
||||
|
||||
// Align max time to "endTime - 1 minute", rounded down to minute precision
|
||||
// This matches legacy getXAxisScale behavior and avoids empty space at the right edge
|
||||
const oneMinuteAgoTimestamp = (maxTime - 60) * 1000;
|
||||
const currentDate = new Date(oneMinuteAgoTimestamp);
|
||||
if (!useExactTimeRange) {
|
||||
const oneMinuteAgoTimestamp = (maxTime - 60) * 1000;
|
||||
const currentDate = new Date(oneMinuteAgoTimestamp);
|
||||
|
||||
currentDate.setSeconds(0);
|
||||
currentDate.setMilliseconds(0);
|
||||
currentDate.setSeconds(0);
|
||||
currentDate.setMilliseconds(0);
|
||||
|
||||
const unixTimestampSeconds = Math.floor(currentDate.getTime() / 1000);
|
||||
maxTime = unixTimestampSeconds;
|
||||
const unixTimestampSeconds = Math.floor(currentDate.getTime() / 1000);
|
||||
|
||||
// Trimming past min inverts the range, which uPlot draws as an empty plot.
|
||||
if (unixTimestampSeconds > minTime) {
|
||||
maxTime = unixTimestampSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
[scaleKey]: {
|
||||
|
||||
@@ -79,6 +79,44 @@ describe('UPlotScaleBuilder', () => {
|
||||
expect(resolvedMax).toBe(expectedMax);
|
||||
});
|
||||
|
||||
it('plots min/max as given when useExactTimeRange is set', () => {
|
||||
const min = 1_700_000_000;
|
||||
const max = 1_700_000_630;
|
||||
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
scaleKey: 'x',
|
||||
time: true,
|
||||
min,
|
||||
max,
|
||||
useExactTimeRange: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.x.range).toStrictEqual([min, max]);
|
||||
});
|
||||
|
||||
it('keeps the requested end when the window is shorter than the trim', () => {
|
||||
// 23 second window: trimming a minute off the end would put max before min.
|
||||
const min = 1_786_527_160;
|
||||
const max = 1_786_527_183;
|
||||
|
||||
const builder = new UPlotScaleBuilder(
|
||||
createScaleProps({
|
||||
scaleKey: 'x',
|
||||
time: true,
|
||||
min,
|
||||
max,
|
||||
}),
|
||||
);
|
||||
|
||||
const config = builder.getConfig();
|
||||
|
||||
expect(config.x.range).toStrictEqual([min, max]);
|
||||
});
|
||||
|
||||
it('falls back to getFallbackMinMaxTimeStamp when time scale has no min/max', () => {
|
||||
getFallbackMinMaxSpy.mockReturnValue({
|
||||
fallbackMin: 100,
|
||||
|
||||
@@ -97,6 +97,8 @@ export interface ScaleProps {
|
||||
auto?: boolean;
|
||||
logBase?: uPlot.Scale.LogBase;
|
||||
distribution?: DistributionType;
|
||||
/** Plots a time scale's `min`/`max` as given, skipping the trim below. */
|
||||
useExactTimeRange?: boolean;
|
||||
}
|
||||
|
||||
export enum DisconnectedValuesMode {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//@ts-nocheck
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
@@ -11,6 +11,7 @@ import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
|
||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { whilelistedKeys } from 'hooks/useResourceAttribute/config';
|
||||
import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
|
||||
import { filterServiceMapSupportedQueries } from 'hooks/useResourceAttribute/utils';
|
||||
import { getDetailedServiceMapItems, ServiceMapStore } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import styled from 'styled-components';
|
||||
@@ -70,32 +71,37 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
|
||||
|
||||
const { queries } = useResourceAttribute();
|
||||
|
||||
const supportedQueries = useMemo(
|
||||
() => filterServiceMapSupportedQueries(queries),
|
||||
[queries],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
Call the apis only when the route is loaded.
|
||||
Check this issue: https://github.com/SigNoz/signoz/issues/110
|
||||
*/
|
||||
getDetailedServiceMapItems(globalTime, queries);
|
||||
}, [globalTime, getDetailedServiceMapItems, queries]);
|
||||
getDetailedServiceMapItems(globalTime, supportedQueries);
|
||||
}, [globalTime, getDetailedServiceMapItems, supportedQueries]);
|
||||
|
||||
useEffect(() => {
|
||||
fgRef.current && fgRef.current.d3Force('charge').strength(-400);
|
||||
});
|
||||
|
||||
if (serviceMap.loading) {
|
||||
return <Spinner size="large" tip="Loading..." />;
|
||||
}
|
||||
const renderBody = (): JSX.Element => {
|
||||
if (serviceMap.loading) {
|
||||
return <Spinner size="large" tip="Loading..." />;
|
||||
}
|
||||
|
||||
if (serviceMap.items.length === 0) {
|
||||
return <Card>No Service Found</Card>;
|
||||
}
|
||||
|
||||
return <Map fgRef={fgRef} serviceMap={serviceMap} />;
|
||||
};
|
||||
|
||||
if (!serviceMap.loading && serviceMap.items.length === 0) {
|
||||
return (
|
||||
<Container>
|
||||
<ResourceAttributesFilter />
|
||||
<Card>No Service Found</Card>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="service-map-container">
|
||||
<Container className="service-map-container">
|
||||
<ResourceAttributesFilter
|
||||
suffixIcon={
|
||||
<TextToolTip
|
||||
@@ -108,8 +114,8 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
|
||||
}
|
||||
/>
|
||||
|
||||
<Map fgRef={fgRef} serviceMap={serviceMap} />
|
||||
</div>
|
||||
{renderBody()}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#aecbfa;}.cls-1,.cls-2,.cls-3{fill-rule:evenodd;}.cls-2{fill:#669df6;}.cls-3{fill:#4285f4;}</style></defs><title>Icon_24px_SQL_Color</title><g data-name="Product Icons"><g ><polygon class="cls-1" points="4.67 10.44 4.67 13.45 12 17.35 12 14.34 4.67 10.44"/><polygon class="cls-1" points="4.67 15.09 4.67 18.1 12 22 12 18.99 4.67 15.09"/><polygon class="cls-2" points="12 17.35 19.33 13.45 19.33 10.44 12 14.34 12 17.35"/><polygon class="cls-2" points="12 22 19.33 18.1 19.33 15.09 12 18.99 12 22"/><polygon class="cls-3" points="19.33 8.91 19.33 5.9 12 2 12 5.01 19.33 8.91"/><polygon class="cls-2" points="12 2 4.67 5.9 4.67 8.91 12 5.01 12 2"/><polygon class="cls-1" points="4.67 5.87 4.67 8.89 12 12.79 12 9.77 4.67 5.87"/><polygon class="cls-2" points="12 12.79 19.33 8.89 19.33 5.87 12 9.77 12 12.79"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 933 B |
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"id": "cloudsql_mysql",
|
||||
"title": "GCP Cloud SQL for MySQL",
|
||||
"icon": "file://icon.svg",
|
||||
"overview": "file://overview.md",
|
||||
"supportedSignals": {
|
||||
"metrics": true,
|
||||
"logs": true
|
||||
},
|
||||
"dataCollected": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/up",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/instance_state",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/replication/replica_lag",
|
||||
"unit": "Seconds",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/cpu/utilization",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/memory/utilization",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/disk/utilization",
|
||||
"unit": "Percent",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/network/connections",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/mysql/queries",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/mysql/dml_operations_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/mysql/threads",
|
||||
"unit": "Count",
|
||||
"type": "Gauge",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/mysql/innodb/buffer_pool_reads_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/mysql/innodb/buffer_pool_read_requests_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/mysql/slow_queries_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/mysql/aborted_connects_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/mysql/innodb/deadlocks_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/mysql/innodb/row_lock_waits_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/disk/read_ops_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"name": "cloudsql.googleapis.com/database/disk/write_ops_count",
|
||||
"unit": "Count",
|
||||
"type": "Sum",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"logs": []
|
||||
},
|
||||
"telemetryCollectionStrategy": {
|
||||
"gcp": {}
|
||||
},
|
||||
"assets": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "GCP Cloud SQL for MySQL Overview",
|
||||
"description": "Overview of GCP Cloud SQL for MySQL metrics",
|
||||
"definition": "file://assets/dashboards/overview.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
### Monitor GCP Cloud SQL for MySQL with SigNoz
|
||||
|
||||
Collect key GCP Cloud SQL for MySQL metrics and view them with an out of the box dashboard.
|
||||
@@ -784,40 +784,57 @@
|
||||
{
|
||||
"kind": "time_series",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"kind": "signoz/CompositeQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"source": "",
|
||||
"aggregations": [
|
||||
"queries": [
|
||||
{
|
||||
"metricName": "cloudsql.googleapis.com/database/cpu/utilization",
|
||||
"temporality": "",
|
||||
"timeAggregation": "max",
|
||||
"spaceAggregation": "max",
|
||||
"reduceTo": "avg"
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "project_id = $project_id AND database_id in $database_id AND gcp.resource_type = 'cloudsql_database' "
|
||||
},
|
||||
"groupBy": [
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"stepInterval": 0,
|
||||
"signal": "metrics",
|
||||
"source": "",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "cloudsql.googleapis.com/database/cpu/utilization",
|
||||
"temporality": "",
|
||||
"timeAggregation": "max",
|
||||
"spaceAggregation": "max",
|
||||
"reduceTo": ""
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"filter": {
|
||||
"expression": "project_id = $project_id AND database_id in $database_id AND gcp.resource_type = 'cloudsql_database' "
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "database_id",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
}
|
||||
],
|
||||
"order": null,
|
||||
"selectFields": null,
|
||||
"secondaryAggregations": null,
|
||||
"functions": null,
|
||||
"legend": "{{database_id}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "database_id",
|
||||
"signal": "",
|
||||
"fieldContext": "attribute",
|
||||
"fieldDataType": "string"
|
||||
"type": "builder_formula",
|
||||
"spec": {
|
||||
"name": "F1",
|
||||
"expression": "100 * A",
|
||||
"disabled": false,
|
||||
"order": null,
|
||||
"functions": null,
|
||||
"legend": "{{database_id}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"order": [],
|
||||
"having": {
|
||||
"expression": ""
|
||||
},
|
||||
"functions": [],
|
||||
"legend": "{{database_id}}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1417,4 +1434,4 @@
|
||||
"refreshInterval": "",
|
||||
"links": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -478,11 +479,19 @@ func (q *promqlQuery) toResult(matrix promql.Matrix, warnings []string, began ti
|
||||
|
||||
for idx := range v.Floats {
|
||||
p := v.Floats[idx]
|
||||
// NaN and +/-Inf have no JSON number form and nothing to plot; the
|
||||
// builder path drops them while scanning rows (see consume.go).
|
||||
if math.IsNaN(p.F) || math.IsInf(p.F, 0) {
|
||||
continue
|
||||
}
|
||||
s.Values = append(s.Values, &qbv5.TimeSeriesValue{
|
||||
Timestamp: p.T,
|
||||
Value: p.F,
|
||||
})
|
||||
}
|
||||
if len(s.Values) == 0 {
|
||||
continue
|
||||
}
|
||||
series = append(series, &s)
|
||||
}
|
||||
|
||||
@@ -494,13 +503,11 @@ func (q *promqlQuery) toResult(matrix promql.Matrix, warnings []string, began ti
|
||||
}
|
||||
statsMu.Unlock()
|
||||
|
||||
tsData := &qbv5.TimeSeriesData{
|
||||
QueryName: q.query.Name,
|
||||
Aggregations: []*qbv5.AggregationBucket{
|
||||
{
|
||||
Series: series,
|
||||
},
|
||||
},
|
||||
tsData := &qbv5.TimeSeriesData{QueryName: q.query.Name}
|
||||
// No bucket at all when nothing survived: a bucket holding no series reads
|
||||
// as "filtered to empty" to the cache, which stores it as a real result.
|
||||
if len(series) > 0 {
|
||||
tsData.Aggregations = []*qbv5.AggregationBucket{{Series: series}}
|
||||
}
|
||||
|
||||
var payload any = tsData
|
||||
|
||||
@@ -2,14 +2,21 @@ package querier
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus/prometheustest"
|
||||
qbv5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRemoveAllVarMatchers(t *testing.T) {
|
||||
@@ -453,3 +460,82 @@ func TestFingerprint_PinnedProviderBypassesCache(t *testing.T) {
|
||||
}
|
||||
assert.Empty(t, q.Fingerprint())
|
||||
}
|
||||
|
||||
func TestToResultDropsNonFiniteValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
floats []promql.FPoint
|
||||
expectedTimestamps []int64
|
||||
expectedValues []float64
|
||||
}{
|
||||
{
|
||||
description: "finite values pass through untouched",
|
||||
floats: []promql.FPoint{{T: 1000, F: 1.5}, {T: 2000, F: 2.5}},
|
||||
expectedTimestamps: []int64{1000, 2000},
|
||||
expectedValues: []float64{1.5, 2.5},
|
||||
},
|
||||
{
|
||||
description: "a ratio's 0/0 points are dropped, the rest kept",
|
||||
floats: []promql.FPoint{{T: 1000, F: 1.5}, {T: 2000, F: math.NaN()}, {T: 3000, F: 2.5}},
|
||||
expectedTimestamps: []int64{1000, 3000},
|
||||
expectedValues: []float64{1.5, 2.5},
|
||||
},
|
||||
{
|
||||
description: "both infinities are dropped",
|
||||
floats: []promql.FPoint{{T: 1000, F: math.Inf(1)}, {T: 2000, F: 4.5}, {T: 3000, F: math.Inf(-1)}},
|
||||
expectedTimestamps: []int64{2000},
|
||||
expectedValues: []float64{4.5},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
q := &promqlQuery{query: qbv5.PromQuery{Name: "A"}, requestType: qbv5.RequestTypeTimeSeries}
|
||||
matrix := promql.Matrix{{Metric: labels.FromStrings("job_name", "dbBloatMonitorJob"), Floats: test.floats}}
|
||||
|
||||
var mu sync.Mutex
|
||||
var rows, bytes uint64
|
||||
result := q.toResult(matrix, nil, time.Now(), &mu, &rows, &bytes)
|
||||
|
||||
tsData, ok := result.Value.(*qbv5.TimeSeriesData)
|
||||
require.True(t, ok)
|
||||
require.Len(t, tsData.Aggregations, 1)
|
||||
require.Len(t, tsData.Aggregations[0].Series, 1)
|
||||
|
||||
timestamps := make([]int64, 0, len(test.expectedTimestamps))
|
||||
values := make([]float64, 0, len(test.expectedValues))
|
||||
for _, v := range tsData.Aggregations[0].Series[0].Values {
|
||||
timestamps = append(timestamps, v.Timestamp)
|
||||
values = append(values, v.Value)
|
||||
}
|
||||
assert.Equal(t, test.expectedTimestamps, timestamps)
|
||||
assert.Equal(t, test.expectedValues, values)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A series left with nothing must not surface as an empty series, and a result
|
||||
// left with no series must carry no aggregation bucket at all — the cache reads
|
||||
// a bucket holding no series as a real, filtered-to-empty result and stores it.
|
||||
func TestToResultDropsSeriesAndBucketLeftEmpty(t *testing.T) {
|
||||
q := &promqlQuery{query: qbv5.PromQuery{Name: "A"}, requestType: qbv5.RequestTypeTimeSeries}
|
||||
matrix := promql.Matrix{
|
||||
{Metric: labels.FromStrings("job_name", "idleJob"), Floats: []promql.FPoint{{T: 1000, F: math.NaN()}}},
|
||||
{Metric: labels.FromStrings("job_name", "activeJob"), Floats: []promql.FPoint{{T: 1000, F: 7.5}}},
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
var rows, bytes uint64
|
||||
tsData, ok := q.toResult(matrix, nil, time.Now(), &mu, &rows, &bytes).Value.(*qbv5.TimeSeriesData)
|
||||
require.True(t, ok)
|
||||
require.Len(t, tsData.Aggregations, 1)
|
||||
require.Len(t, tsData.Aggregations[0].Series, 1, "the all-NaN series is gone")
|
||||
assert.Equal(t, "activeJob", tsData.Aggregations[0].Series[0].Labels[0].Value)
|
||||
|
||||
allNaN := promql.Matrix{
|
||||
{Metric: labels.FromStrings("job_name", "idleJob"), Floats: []promql.FPoint{{T: 1000, F: math.NaN()}}},
|
||||
}
|
||||
tsData, ok = q.toResult(allNaN, nil, time.Now(), &mu, &rows, &bytes).Value.(*qbv5.TimeSeriesData)
|
||||
require.True(t, ok)
|
||||
assert.Empty(t, tsData.Aggregations)
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ var (
|
||||
GCPServiceComputeEngine = ServiceID{valuer.NewString("computeengine")}
|
||||
GCPServiceGKE = ServiceID{valuer.NewString("gke")}
|
||||
GCPServiceCloudStorage = ServiceID{valuer.NewString("cloudstorage")}
|
||||
GCPServiceCloudSQLMySQL = ServiceID{valuer.NewString("cloudsql_mysql")}
|
||||
)
|
||||
|
||||
func (ServiceID) Enum() []any {
|
||||
@@ -82,6 +83,7 @@ func (ServiceID) Enum() []any {
|
||||
GCPServiceComputeEngine,
|
||||
GCPServiceGKE,
|
||||
GCPServiceCloudStorage,
|
||||
GCPServiceCloudSQLMySQL,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +126,7 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
|
||||
GCPServiceComputeEngine,
|
||||
GCPServiceGKE,
|
||||
GCPServiceCloudStorage,
|
||||
GCPServiceCloudSQLMySQL,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
@@ -31,11 +32,12 @@ func (enum *Signal) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
var (
|
||||
SignalTraces = Signal{valuer.NewString("traces")}
|
||||
SignalLogs = Signal{valuer.NewString("logs")}
|
||||
SignalApiMonitoring = Signal{valuer.NewString("api_monitoring")}
|
||||
SignalExceptions = Signal{valuer.NewString("exceptions")}
|
||||
SignalMeter = Signal{valuer.NewString("meter")}
|
||||
SignalTraces = Signal{valuer.NewString("traces")}
|
||||
SignalLogs = Signal{valuer.NewString("logs")}
|
||||
SignalApiMonitoring = Signal{valuer.NewString("api_monitoring")}
|
||||
SignalExceptions = Signal{valuer.NewString("exceptions")}
|
||||
SignalMeter = Signal{valuer.NewString("meter")}
|
||||
SignalAiObservability = Signal{valuer.NewString("ai_observability")}
|
||||
)
|
||||
|
||||
// NewSignal creates a Signal from a string.
|
||||
@@ -51,6 +53,8 @@ func NewSignal(s string) (Signal, error) {
|
||||
return SignalExceptions, nil
|
||||
case "meter":
|
||||
return SignalMeter, nil
|
||||
case "ai_observability":
|
||||
return SignalAiObservability, nil
|
||||
default:
|
||||
return Signal{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid signal: %s", s)
|
||||
}
|
||||
@@ -187,6 +191,18 @@ func NewDefaultQuickFilter(orgID valuer.UUID) ([]*StorableQuickFilter, error) {
|
||||
{"key": "host.name", "dataType": "float64", "type": "Sum"},
|
||||
}
|
||||
|
||||
// AI observability (builder_ai_query trace explorer), ordered by expected
|
||||
// usage: env scoping, the LLM identity keys, then service and the rest.
|
||||
aiObservabilityFilters := []map[string]interface{}{
|
||||
{"key": "deployment.environment", "dataType": "string", "type": "resource"},
|
||||
{"key": telemetrytypes.GenAIOperationName, "dataType": "string", "type": "tag"},
|
||||
{"key": telemetrytypes.GenAIProviderName, "dataType": "string", "type": "tag"},
|
||||
{"key": telemetrytypes.GenAIRequestModel, "dataType": "string", "type": "tag"},
|
||||
{"key": "service.name", "dataType": "string", "type": "resource"},
|
||||
{"key": telemetrytypes.GenAIToolName, "dataType": "string", "type": "tag"},
|
||||
{"key": telemetrytypes.GenAIAgentName, "dataType": "string", "type": "tag"},
|
||||
}
|
||||
|
||||
tracesJSON, err := json.Marshal(tracesFilters)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal traces filters")
|
||||
@@ -212,6 +228,11 @@ func NewDefaultQuickFilter(orgID valuer.UUID) ([]*StorableQuickFilter, error) {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal meter filters")
|
||||
}
|
||||
|
||||
aiObservabilityJSON, err := json.Marshal(aiObservabilityFilters)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal ai observability filters")
|
||||
}
|
||||
|
||||
timeRightNow := time.Now()
|
||||
|
||||
return []*StorableQuickFilter{
|
||||
@@ -275,5 +296,17 @@ func NewDefaultQuickFilter(orgID valuer.UUID) ([]*StorableQuickFilter, error) {
|
||||
UpdatedAt: timeRightNow,
|
||||
},
|
||||
},
|
||||
{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
OrgID: orgID,
|
||||
Filter: string(aiObservabilityJSON),
|
||||
Signal: SignalAiObservability,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: timeRightNow,
|
||||
UpdatedAt: timeRightNow,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package telemetrytypes
|
||||
// OpenTelemetry gen_ai semantic-convention attribute keys. Single source of truth
|
||||
// shared by the AI query builder and the LLM pricing pipeline.
|
||||
const (
|
||||
GenAIRequestModel = "gen_ai.request.model"
|
||||
GenAIToolName = "gen_ai.tool.name"
|
||||
GenAIAgentName = "gen_ai.agent.name"
|
||||
GenAIProviderName = "gen_ai.provider.name"
|
||||
GenAIRequestModel = "gen_ai.request.model"
|
||||
GenAIOperationName = "gen_ai.operation.name"
|
||||
GenAIToolName = "gen_ai.tool.name"
|
||||
GenAIAgentName = "gen_ai.agent.name"
|
||||
GenAIProviderName = "gen_ai.provider.name"
|
||||
|
||||
GenAIUsageInputTokens = "gen_ai.usage.input_tokens"
|
||||
GenAIUsageOutputTokens = "gen_ai.usage.output_tokens"
|
||||
@@ -25,10 +26,11 @@ const (
|
||||
// on, surfaced by the metadata store even before ingestion so the AI gate/columns
|
||||
// resolve on a fresh install.
|
||||
var GenAIFieldDefinitions = map[string]TelemetryFieldKey{
|
||||
GenAIRequestModel: {Name: GenAIRequestModel, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
|
||||
GenAIToolName: {Name: GenAIToolName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
|
||||
GenAIAgentName: {Name: GenAIAgentName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
|
||||
GenAIProviderName: {Name: GenAIProviderName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
|
||||
GenAIRequestModel: {Name: GenAIRequestModel, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
|
||||
GenAIOperationName: {Name: GenAIOperationName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
|
||||
GenAIToolName: {Name: GenAIToolName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
|
||||
GenAIAgentName: {Name: GenAIAgentName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
|
||||
GenAIProviderName: {Name: GenAIProviderName, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeString},
|
||||
|
||||
GenAIUsageInputTokens: {Name: GenAIUsageInputTokens, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeFloat64},
|
||||
GenAIUsageOutputTokens: {Name: GenAIUsageOutputTokens, Signal: SignalTraces, FieldContext: FieldContextAttribute, FieldDataType: FieldDataTypeFloat64},
|
||||
|
||||
@@ -1,4 +1,116 @@
|
||||
{
|
||||
"note": "Divergences of the CURRENT promql serving path from the upstream reference engine, enforced exactly by 01_upstream_corpus.py in both directions. These document shipped defects, not test debt: the dominant class is the v1 remote-read fetch injecting a synthetic 'fingerprint' label into every series (pkg/prometheus/clickhouseprometheus/json.go), which breaks without() grouping and default vector matching. Entries must be REMOVED as the serving path is fixed or swapped.",
|
||||
"divergences": {}
|
||||
}
|
||||
"note": "Divergences of the CURRENT promql serving path from the upstream reference engine, enforced exactly by 01_upstream_corpus.py in both directions. These document shipped defects, not test debt: the dominant class is the v1 remote-read fetch injecting a synthetic 'fingerprint' label into every series (pkg/prometheus/clickhouseprometheus/json.go), which breaks without() grouping and default vector matching. Entries must be REMOVED as the serving path is fixed or swapped. Second class, and the bulk of the entries below: promql_query.go drops NaN and +/-Inf from results, mirroring the builder path in consume.go, so every case whose expected output carries a non-finite value diverges on both legs. That class is a product decision rather than a defect, so it is not part of the burn-down.",
|
||||
"divergences": {
|
||||
"aggregators.test:630[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:630[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:633[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:633[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:636[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:636[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:639[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:639[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:642[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:642[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:645[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:645[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:648[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:648[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:661[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:661[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:698[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:698[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:702[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:702[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:706[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:706[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:710[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:710[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:714[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:714[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:717[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:717[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:720[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:720[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:724[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:724[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:862[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:862[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:865[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:865[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:868[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:868[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:873[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:873[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:885[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:885[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:888[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:888[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:891[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:891[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:896[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:896[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:906[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:906[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:909[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:909[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:919[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:919[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:922[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:922[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:925[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:925[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:930[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:930[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:942[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:942[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:945[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:945[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:948[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:948[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:953[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:953[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:963[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:963[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:966[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:966[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"operators.test:533[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"operators.test:533[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"operators.test:539[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"trig_functions.test:13[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:13[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:18[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:18[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:23[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:23[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:28[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:28[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:33[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:33[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:38[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:38[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:43[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:43[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:48[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:48[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:53[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:53[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:58[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:58[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:63[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:63[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:68[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:68[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:73[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:73[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:78[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:78[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:83[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:83[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:88[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:88[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:8[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:8[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:93[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:93[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,128 @@
|
||||
{
|
||||
"note": "Divergences of the clickhousev2 provider (pinned via X-SigNoz-PromQL-Provider) from the upstream reference engine, enforced exactly by 01_upstream_corpus.py in both directions. This ledger is the rollout scorecard for the provider swap: the default provider cannot be replaced by clickhousev2 while anything is listed here. Entries must carry the defect's cause and be REMOVED as the provider is fixed. Current class: the engine aggregates floats with Kahan compensated summation (sum, sum_over_time) and an overflow-free incremental mean (avg); ClickHouse's sumForEach/avgForEach/arraySum are naive, so extreme-magnitude corpus data (±1e100 cancellation, ±1.8e308 overflow) diverges on transpiled plans. Burn-down candidates: sumKahanForEach for the cancellation class; the overflow class needs an incremental-mean aggregate ClickHouse does not have.",
|
||||
"note": "Divergences of the clickhousev2 provider (pinned via X-SigNoz-PromQL-Provider) from the upstream reference engine, enforced exactly by 01_upstream_corpus.py in both directions. This ledger is the rollout scorecard for the provider swap: the default provider cannot be replaced by clickhousev2 while anything is listed here. Entries must carry the defect's cause and be REMOVED as the provider is fixed. Current class: the engine aggregates floats with Kahan compensated summation (sum, sum_over_time) and an overflow-free incremental mean (avg); ClickHouse's sumForEach/avgForEach/arraySum are naive, so extreme-magnitude corpus data (±1e100 cancellation, ±1.8e308 overflow) diverges on transpiled plans. Burn-down candidates: sumKahanForEach for the cancellation class; the overflow class needs an incremental-mean aggregate ClickHouse does not have. Second class, and the bulk of the entries below: promql_query.go drops NaN and +/-Inf from results, mirroring the builder path in consume.go, so every case whose expected output carries a non-finite value diverges on both legs. That class is a product decision rather than a defect, so it is not part of the burn-down.",
|
||||
"divergences": {
|
||||
"aggregators.test:630[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:630[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:633[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:633[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:636[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:636[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:639[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:639[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:642[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:642[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:645[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:645[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:648[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:648[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:651[base]": "avg over near-max-float64 values: engine's incremental mean never forms the overflowing sum; avgForEach sums then divides, overflowing to +Inf",
|
||||
"aggregators.test:651[instant-coarse]": "same as aggregators.test:651[base] on the coarse-step grid variant",
|
||||
"aggregators.test:654[base]": "avg over near-min-float64 values: engine's incremental mean never forms the overflowing sum; avgForEach overflows to -Inf",
|
||||
"aggregators.test:654[instant-coarse]": "same as aggregators.test:654[base] on the coarse-step grid variant",
|
||||
"aggregators.test:661[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:661[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:687[base]": "sum over {1e100, -1e100, small}: engine uses Kahan compensated summation; sumForEach's naive summation loses the small terms to cancellation and returns 0",
|
||||
"aggregators.test:687[instant-coarse]": "same as aggregators.test:687[base] on the coarse-step grid variant",
|
||||
"aggregators.test:695[base]": "avg over {1e100, -1e100, small}: same Kahan-vs-naive cancellation as aggregators.test:687, divided by count",
|
||||
"aggregators.test:695[instant-coarse]": "same as aggregators.test:695[base] on the coarse-step grid variant",
|
||||
"aggregators.test:698[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:698[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:702[base]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:702[instant-coarse]": "expected output is entirely Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:706[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:706[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:710[base]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:710[instant-coarse]": "expected output is entirely -Inf; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:714[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:714[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:717[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:717[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:720[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:720[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:724[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:724[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:862[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:862[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:865[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:865[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:868[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:868[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:873[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:873[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:885[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:885[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:888[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:888[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:891[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:891[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:896[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:896[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:906[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:906[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:909[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:909[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:919[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:919[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:922[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:922[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:925[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:925[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:930[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:930[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:942[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:942[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:945[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:945[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:948[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:948[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:953[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:953[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"aggregators.test:963[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:963[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:966[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"aggregators.test:966[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"functions.test:1084[instant-coarse]": "sum_over_time over a window containing ±1e100: the disjoint coarse-step form's arraySum slide is naive summation, cancelling to 0 (the base variant's W>64 shape falls back to the engine and is exact)",
|
||||
"functions.test:1087[instant-coarse]": "avg_over_time, same window and cancellation as functions.test:1084[instant-coarse]",
|
||||
"functions.test:1149[base]": "avg_over_time over ±2.258e220-magnitude samples: engine's Kahan-compensated incremental mean cancels exactly to 0; the bucketed form's naive slide summation leaves a ~1e202 residue",
|
||||
"functions.test:1149[instant-coarse]": "same as functions.test:1149[base] through the disjoint coarse-step form"
|
||||
"functions.test:1149[instant-coarse]": "same as functions.test:1149[base] through the disjoint coarse-step form",
|
||||
"operators.test:533[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"operators.test:533[instant-coarse]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"operators.test:539[base]": "expected output is entirely NaN; promql_query.go drops non-finite values, so no series is emitted",
|
||||
"trig_functions.test:13[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:13[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:18[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:18[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:23[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:23[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:28[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:28[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:33[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:33[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:38[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:38[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:43[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:43[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:48[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:48[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:53[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:53[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:58[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:58[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:63[base]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:63[instant-coarse]": "2 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:68[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:68[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:73[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:73[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:78[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:78[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:83[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:83[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:88[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:88[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:8[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:8[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:93[base]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing",
|
||||
"trig_functions.test:93[instant-coarse]": "1 of 3 expected points are NaN; promql_query.go drops non-finite values, so those timestamps are missing"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
from uuid import uuid4
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import get_all_series, make_query_request
|
||||
|
||||
HOUR_MS = 3_600_000
|
||||
SAMPLE_INTERVAL_MS = 60_000
|
||||
|
||||
|
||||
def test_promql_ratio_with_zero_denominator_is_dropped_and_cached(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
# 12h ending on an hour boundary 15m ago — old enough to be cached.
|
||||
end_ms = (int((datetime.now(tz=UTC) - timedelta(minutes=15)).timestamp() * 1000) // HOUR_MS) * HOUR_MS
|
||||
start_ms = end_ms - 12 * HOUR_MS
|
||||
|
||||
sum_metric = f"job_duration_sum_{uuid4().hex[:8]}"
|
||||
count_metric = f"job_duration_count_{uuid4().hex[:8]}"
|
||||
|
||||
# active_job divides finite; idle_job is 0/0 at every step.
|
||||
series = {"active_job": (100.0, 4.0), "idle_job": (0.0, 0.0)}
|
||||
metrics: list[Metrics] = []
|
||||
for job_name, (sum_value, count_value) in series.items():
|
||||
for ts_ms in range(start_ms, end_ms + 1, SAMPLE_INTERVAL_MS):
|
||||
timestamp = datetime.fromtimestamp(ts_ms / 1000, tz=UTC)
|
||||
metrics.append(Metrics(metric_name=sum_metric, labels={"job_name": job_name}, timestamp=timestamp, value=sum_value))
|
||||
metrics.append(Metrics(metric_name=count_metric, labels={"job_name": job_name}, timestamp=timestamp, value=count_value))
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
promql = f"sum by (job_name) ({sum_metric}) / sum by (job_name) ({count_metric})"
|
||||
|
||||
def run() -> tuple[dict[str, dict[int, object]], int]:
|
||||
query = {"type": "promql", "spec": {"name": "A", "query": promql}}
|
||||
response = make_query_request(signoz, token, start_ms, end_ms, [query], no_cache=False)
|
||||
assert response.status_code == HTTPStatus.OK, response.text[:300]
|
||||
body = response.json()
|
||||
out: dict[str, dict[int, object]] = {}
|
||||
for entry in get_all_series(body, "A") or []:
|
||||
labels = {l["key"]["name"]: str(l["value"]) for l in entry.get("labels") or []}
|
||||
out[labels["job_name"]] = {v["timestamp"]: v["value"] for v in entry.get("values") or []}
|
||||
return out, int(body["data"]["meta"]["stepIntervals"]["A"])
|
||||
|
||||
# First populates the cache, second must be served from it.
|
||||
first, step_seconds = run()
|
||||
second, _ = run()
|
||||
|
||||
expected_points = (end_ms - start_ms) // (step_seconds * 1000) + 1
|
||||
assert set(first) == {"active_job"}, f"the 0/0 series must not reach the response: {sorted(first)}"
|
||||
assert set(first["active_job"].values()) == {25.0}, sorted(set(first["active_job"].values()))
|
||||
assert len(first["active_job"]) == expected_points, f"expected {expected_points} points, got {len(first['active_job'])}"
|
||||
|
||||
# The cached read excludes end_ms, the one legitimate difference.
|
||||
assert set(second) == set(first), sorted(second)
|
||||
for job_name, points in first.items():
|
||||
expected = {ts: value for ts, value in points.items() if ts < end_ms}
|
||||
assert second[job_name] == expected, f"{job_name}: got {len(second[job_name])} of {len(expected)} points"
|
||||
Reference in New Issue
Block a user