Compare commits

..

6 Commits

Author SHA1 Message Date
Srikanth Chekuri
4b40b119d9 Merge branch 'main' into issue-5121-5636 2026-07-16 02:51:16 +05:30
srikanthccv
ddb50bb692 test(alertmanager): fix flaky test 2026-07-16 01:20:39 +05:30
Vinicius Lourenço
fbf2e62044 feat(infrastructure-monitoring-v2): add onboarding checks (#12119)
* feat(infrastructure-monitoring): add onboarding checks

* refactor(infrastructure-monitoring): use error content to show error message

* test(infrastructure-monitoring): add tests for onboarding checks
2026-07-15 19:47:30 +00:00
Tushar Vats
62c44f5eac chore: pass org id for flagger (#12126) 2026-07-15 19:47:24 +00:00
Vinicius Lourenço
65fde71b72 feat(infrastructure-monitoring-v2): add counts cards (#12120)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(entity-count): add base structure to show count

* feat(entity-count): add count for clusters

* feat(entity-count): add count for namespaces

* chore(infra-monitoring): move component to be under components folder

* fix(pr): address comments
2026-07-15 13:39:48 +00:00
Vinicius Lourenço
7f5f63b20a feat(infra-monitoring): add docs for every chart (#12037)
* feat(infra-monitoring): add docs for every chart

* fix(docs): add missing docs for each column/chart

* fix(infra-monitoring): keep referer

---------

Co-authored-by: Nikhil Mantri <nikhil.mantri1999@gmail.com>
2026-07-15 13:25:53 +00:00
106 changed files with 2256 additions and 1894 deletions

View File

@@ -1,13 +1,17 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listHosts } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import APIError from 'types/api/error';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
@@ -122,13 +126,12 @@ function Hosts(): JSX.Element {
endTimeBeforeRetention: data.endTimeBeforeRetention,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch hosts';
return {
type: 'list' as const,
records: [] as InframonitoringtypesHostRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -141,7 +144,7 @@ function Hosts(): JSX.Element {
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesHostRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listHosts(
@@ -159,11 +162,10 @@ function Hosts(): JSX.Element {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch host';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},

View File

@@ -51,7 +51,7 @@ function ColumnHeader({
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener noreferrer"
rel="noopener"
onClick={(e): void => e.stopPropagation()}
>
Learn more.

View File

@@ -30,7 +30,7 @@ function EntityGroupHeader({
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener noreferrer"
rel="noopener"
onClick={(e): void => e.stopPropagation()}
>
Learn more.

View File

@@ -12,6 +12,8 @@ import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import APIError from 'types/api/error';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
@@ -63,6 +65,10 @@ import LoadingContainer from '../LoadingContainer';
import '../EntityDetailsUtils/entityDetails.styles.scss';
import { parseAsString, useQueryState } from 'nuqs';
import {
EntityCountConfig,
EntityCountsSection,
} from './components/EntityCountsSection/EntityCountsSection';
const TimeRangeOffset = 1000000000;
@@ -72,6 +78,8 @@ export interface K8sDetailsMetadataConfig<T> {
render?: (value: string | number, entity: T) => React.ReactNode;
}
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
export interface K8sDetailsFilters {
filter: { expression: string };
start: number;
@@ -86,15 +94,18 @@ export interface K8sBaseDetailsProps<T> {
fetchEntityData: (
filters: K8sDetailsFilters,
signal?: AbortSignal,
) => Promise<{ data: T | null; error?: string | null }>;
) => Promise<{ data: T | null; error?: APIError | null }>;
// Entity configuration
getEntityName: (entity: T) => string;
getInitialLogTracesExpression: (entity: T) => string;
getInitialEventsExpression: (entity: T) => string;
metadataConfig: K8sDetailsMetadataConfig<T>[];
countsConfig?: K8sDetailsCountConfig<T>[];
getCountsFilterExpression?: (entity: T) => string;
entityWidgetInfo: {
title: string;
yAxisUnit: string;
docPath?: string;
}[];
getEntityQueryPayload: (
entity: T,
@@ -137,6 +148,8 @@ export default function K8sBaseDetails<T>({
getInitialLogTracesExpression,
getInitialEventsExpression,
metadataConfig,
countsConfig,
getCountsFilterExpression,
entityWidgetInfo,
getEntityQueryPayload,
queryKeyPrefix,
@@ -436,12 +449,18 @@ export default function K8sBaseDetails<T>({
{isEntityLoading && <LoadingContainer />}
{(isEntityError || hasResponseError) && (
<div className="entity-error-container">
<Typography.Text color="danger">
{entityResponse?.error ||
(entityError instanceof Error
? entityError.message
: 'Failed to load entity details')}
</Typography.Text>
<ErrorContent
error={
entityResponse?.error ??
(entityError instanceof APIError ? entityError : null) ?? {
code: 500,
message:
entityError instanceof Error
? entityError.message
: 'Failed to load entity details',
}
}
/>
</div>
)}
{entity && !isEntityLoading && !hasResponseError && (
@@ -479,6 +498,19 @@ export default function K8sBaseDetails<T>({
})}
</div>
</div>
{countsConfig &&
countsConfig.length > 0 &&
selectedItem &&
getCountsFilterExpression && (
<EntityCountsSection
entity={entity}
countsConfig={countsConfig}
selectedItem={selectedItem}
filterExpression={getCountsFilterExpression(entity)}
closeDrawer={handleClose}
/>
)}
</div>
{!hideDetailViewTabs && (

View File

@@ -14,6 +14,7 @@ import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
import { openInNewTab } from 'utils/navigation';
import APIError from 'types/api/error';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -33,13 +34,14 @@ import K8sHeader from './K8sHeader';
import { K8sPaginationWarning } from './K8sPaginationWarning';
import { K8sBaseFilters } from './types';
import { getGroupedByMeta } from './utils';
import { K8sInstrumentationChecksCallout } from './components/K8sInstrumentationChecksCallout/K8sInstrumentationChecksCallout';
import styles from './K8sBaseList.module.scss';
import cx from 'classnames';
export type K8sBaseListEmptyStateContext = {
isError: boolean;
error?: string | null;
error?: APIError | null;
totalCount: number;
hasFilters: boolean;
isLoading: boolean;
@@ -66,7 +68,7 @@ export type K8sBaseListProps<
records?: T[];
data?: T[];
total: number;
error?: string | null;
error?: APIError | null;
rawData?: unknown;
endTimeBeforeRetention?: boolean;
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
@@ -377,6 +379,8 @@ export function K8sBaseList<
cancelQuery={cancelQuery}
/>
<div ref={containerRef} className={styles.tableContainer}>
<K8sInstrumentationChecksCallout entity={entity} />
{isError && (
<Typography>
{data?.error?.toString() || 'Something went wrong'}

View File

@@ -44,13 +44,11 @@
width: 32px;
}
.errorIcon {
color: var(--danger-background);
}
.actions {
.errorContent {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
margin-top: 16px;
gap: var(--spacing-3);
max-width: 500px;
padding: 24px;
}

View File

@@ -1,8 +1,4 @@
import { useCallback } from 'react';
import { Button } from '@signozhq/ui/button';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { LifeBuoy, TriangleAlert } from '@signozhq/icons';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
import eyesEmojiUrl from '@/assets/Images/eyesEmoji.svg';
@@ -13,26 +9,12 @@ import styles from './K8sEmptyState.module.scss';
type K8sEmptyStateProps = Partial<K8sBaseListEmptyStateContext>;
const handleContactSupport = (isCloudUser: boolean): void => {
if (isCloudUser) {
history.push('/support');
} else {
window.open('https://signoz.io/slack', '_blank');
}
};
export function K8sEmptyState({
isError,
error,
isLoading,
endTimeBeforeRetention,
}: K8sEmptyStateProps): JSX.Element | null {
const { isCloudUser } = useGetTenantLicense();
const handleSupport = useCallback(() => {
handleContactSupport(isCloudUser);
}, [isCloudUser]);
if (isLoading) {
return null;
}
@@ -40,25 +22,15 @@ export function K8sEmptyState({
if (isError || error) {
return (
<div className={styles.container}>
<div className={styles.content}>
<TriangleAlert size={32} className={styles.errorIcon} />
<span className={styles.message}>
{error || 'An error occurred while fetching data.'}
</span>
<p>
Our team is getting on top to resolve this. Please reach out to support if
the issue persists.
</p>
<div className={styles.actions}>
<Button
onClick={handleSupport}
variant="solid"
color="secondary"
prefix={<LifeBuoy size={14} />}
>
Contact Support
</Button>
</div>
<div className={styles.errorContent}>
<ErrorContent
error={
error ?? {
code: 500,
message: 'An error occurred while fetching data.',
}
}
/>
</div>
</div>
);

View File

@@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import APIError from 'types/api/error';
import TanStackTable, {
SortState,
TableColumnDef,
@@ -51,7 +52,7 @@ export type K8sExpandedRowProps<T, TItemKey = string> = {
records?: T[];
data?: T[];
total: number;
error?: string | null;
error?: APIError | null;
rawData?: unknown;
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
}>;

View File

@@ -9,6 +9,8 @@ import { TooltipProvider } from '@signozhq/ui/tooltip';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { InfraMonitoringEvents } from 'constants/events';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
NuqsTestingAdapter,
OnUrlUpdateFunction,
@@ -17,6 +19,7 @@ import {
import { AppProvider } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import store from 'store';
import APIError from 'types/api/error';
import { openInNewTab } from 'utils/navigation';
import { TableColumnDef } from 'components/TanStackTableView';
@@ -630,7 +633,15 @@ describe('K8sBaseList', () => {
fetchListDataMock.mockResolvedValue({
data: [],
total: 0,
error: 'Failed to fetch pods',
error: new APIError({
httpStatusCode: 500,
error: {
code: '500',
message: 'Failed to fetch pods',
url: '',
errors: [],
},
}),
});
renderComponent<TestItem>({
@@ -1054,4 +1065,304 @@ describe('K8sBaseList', () => {
expect(url).not.toContain('selectedItemNamespaceName');
});
});
describe('instrumentation checks callout', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
>();
beforeEach(() => {
fetchListDataMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [{ id: 'item-1' }],
total: 1,
error: null,
});
});
it('should not render callout when ready is true', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: true,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage'],
},
],
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('item-1');
expect(screen.queryByText('Instrumentation checks')).not.toBeInTheDocument();
});
it('should not render callout when no entries exist', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: null,
presentOptionalMetrics: null,
presentRequiredAttributes: null,
missingDefaultEnabledMetrics: null,
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('item-1');
expect(screen.queryByText('Instrumentation checks')).not.toBeInTheDocument();
});
it('should render callout with present entries', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage', 'k8s.pod.memory.usage'],
},
],
presentOptionalMetrics: null,
presentRequiredAttributes: null,
missingDefaultEnabledMetrics: null,
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
await expect(
screen.findByText('Default enabled metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.cpu.usage, k8s.pod.memory.usage'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('otel-collector'),
).resolves.toBeInTheDocument();
});
it('should render callout with missing entries', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: null,
presentOptionalMetrics: null,
presentRequiredAttributes: null,
missingDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.limit'],
documentationLink: 'https://example.com/docs',
},
],
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
await expect(
screen.findByText('Missing default metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.cpu.limit'),
).resolves.toBeInTheDocument();
await expect(screen.findByText('Learn here')).resolves.toBeInTheDocument();
});
it('should trigger recheck on button click', async () => {
let recheckCallCount = 0;
server.use(
rest.get(
'http://localhost/api/v2/infra_monitoring/checks',
(_, res, ctx) => {
recheckCallCount++;
return res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage'],
},
],
},
}),
);
},
),
);
const user = userEvent.setup();
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
const initialCallCount = recheckCallCount;
const recheckBtn = screen.getByTestId('instrumentation-checks-recheck-btn');
await user.click(recheckBtn);
await waitFor(() => {
expect(recheckCallCount).toBeGreaterThan(initialCallCount);
});
});
it('should render both present and missing entries', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage'],
},
],
presentOptionalMetrics: null,
presentRequiredAttributes: [
{
associatedComponent: { name: 'otel-collector' },
attributes: ['k8s.namespace.name'],
},
],
missingDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.memory.limit'],
},
],
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
// Present entries
await expect(
screen.findByText('Default enabled metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.cpu.usage'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('Required attributes'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.namespace.name'),
).resolves.toBeInTheDocument();
// Missing entries
await expect(
screen.findByText('Missing default metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.memory.limit'),
).resolves.toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,41 @@
.countsContainer {
display: flex;
gap: var(--spacing-6);
margin-top: var(--spacing-8);
flex-wrap: wrap;
}
.countCard {
position: relative;
flex: 1;
min-width: 120px;
max-width: 180px;
border: 1px solid var(--l3-border);
border-radius: 4px;
display: flex;
flex-direction: column;
gap: var(--spacing-2);
padding: var(--spacing-6);
}
.countLabel {
color: var(--l2-foreground);
letter-spacing: var(--letter-spacing-wide);
text-transform: uppercase;
}
.countValue {
color: var(--l1-foreground);
font-family: var(--periscope-font-family-mono);
font-size: var(--font-size-xl);
}
.navigateButton {
position: absolute;
top: var(--spacing-4);
right: var(--spacing-4);
--button-padding: var(--spacing-1);
--button-height: 24px;
--button-width: 24px;
}

View File

@@ -0,0 +1,107 @@
import { Tooltip } from 'antd';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import { Compass } from '@signozhq/icons';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { Link } from 'react-router-dom';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
InfraMonitoringEntity,
} from '../../../constants';
import styles from './EntityCountsSection.module.scss';
export interface EntityCountConfig<T> {
label: string;
getValue: (entity: T) => number;
targetCategory: InfraMonitoringEntity;
}
interface EntityCountsSectionProps<T> {
entity: T;
countsConfig: EntityCountConfig<T>[];
selectedItem: string;
filterExpression: string;
closeDrawer: () => void;
}
export function EntityCountsSection<T>({
entity,
countsConfig,
selectedItem,
filterExpression,
closeDrawer,
}: EntityCountsSectionProps<T>): JSX.Element {
const buildNavigationUrl = (targetCategory: InfraMonitoringEntity): string => {
const defaultQuery = initialQueriesMap[DataSource.METRICS];
const compositeQuery = {
...defaultQuery,
id: uuid(),
builder: {
...defaultQuery.builder,
queryData: defaultQuery.builder.queryData.map((query) => ({
...query,
filter: { expression: filterExpression },
filters: { items: [], op: 'AND' as const },
})),
},
};
// TODO(H4ad): After https://github.com/SigNoz/signoz/pull/12038, inherit custom time of drawer to list
const urlParams = new URLSearchParams();
urlParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY, targetCategory);
urlParams.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(compositeQuery)),
);
return `${ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES}?${urlParams.toString()}`;
};
return (
<div className={styles.countsContainer}>
{countsConfig.map((config) => (
<div
key={config.label}
className={styles.countCard}
data-testid={`count-card-${config.label.toLowerCase().replace(/\s+/g, '-')}`}
>
<Typography.Text
color="muted"
size="small"
weight="medium"
className={styles.countLabel}
>
{config.label}
</Typography.Text>
<Typography.Text className={styles.countValue} size="xl" weight="semibold">
{config.getValue(entity) || '-'}
</Typography.Text>
<Link
to={buildNavigationUrl(config.targetCategory)}
onClick={closeDrawer}
data-testid={`navigate-${config.label.toLowerCase().replace(/\s+/g, '-')}`}
>
<Tooltip
title={`View ${config.label.toLowerCase()} of '${selectedItem}'`}
placement="top"
>
<Button
size="icon"
variant="ghost"
color="secondary"
className={styles.navigateButton}
prefix={<Compass size={14} />}
/>
</Tooltip>
</Link>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,24 @@
.checkContainer {
padding: 0px var(--spacing-4) var(--spacing-4) var(--spacing-4);
position: relative;
}
.container {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.entriesList {
display: flex;
flex-direction: column;
gap: 4px;
}

View File

@@ -0,0 +1,162 @@
import { MouseEvent, useCallback, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import { RefreshCw } from '@signozhq/icons';
import setLocalStorage from 'api/browser/localstorage/set';
import {
invalidateGetChecks,
useGetChecks,
} from 'api/generated/services/inframonitoring';
import { InframonitoringtypesCheckTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEntity } from '../../../constants';
import styles from './K8sInstrumentationChecksCallout.module.scss';
import { MissingEntryRow } from './MissingEntryRow';
import { PresentEntryRow } from './PresentEntryRow';
import {
ENTITY_TO_CHECK_TYPE,
getStorageKey,
hasAnyEntries,
hasMissingEntries,
readExpandedState,
} from './utils';
export interface InstrumentationChecksCalloutProps {
entity: InfraMonitoringEntity;
}
export function K8sInstrumentationChecksCallout({
entity,
}: InstrumentationChecksCalloutProps): JSX.Element | null {
const checkType = ENTITY_TO_CHECK_TYPE[entity];
const queryClient = useQueryClient();
const [isExpanded, setIsExpanded] = useState(() => readExpandedState(entity));
const { data, isLoading, isFetching } = useGetChecks(
{ type: checkType as InframonitoringtypesCheckTypeDTO },
{ query: { enabled: !!checkType } },
);
const handleRecheck = useCallback(
(e: MouseEvent): void => {
e.preventDefault();
e.stopPropagation();
if (checkType) {
void invalidateGetChecks(queryClient, { type: checkType });
}
},
[queryClient, checkType],
);
const handleExpandToggle = useCallback((): void => {
setIsExpanded((prev) => {
const next = !prev;
setLocalStorage(getStorageKey(entity), String(next));
return next;
});
}, [entity]);
const checksData = data?.data;
const hasMissingItems = useMemo(
() => (checksData ? hasMissingEntries(checksData) : false),
[checksData],
);
if (!checkType || isLoading) {
return null;
}
if (!checksData || checksData.ready) {
return null;
}
if (!hasAnyEntries(checksData)) {
return null;
}
return (
<div className={styles.checkContainer}>
<Callout
type={hasMissingItems ? 'warning' : 'info'}
showIcon
size="medium"
title={
<div className={styles.header}>
Instrumentation checks
<Button
variant="outlined"
color="warning"
size="sm"
onClick={handleRecheck}
loading={isFetching}
prefix={<RefreshCw size={12} />}
data-testid="instrumentation-checks-recheck-btn"
>
Recheck
</Button>
</div>
}
action="expandable"
defaultExpanded={isExpanded}
onClick={handleExpandToggle}
>
<div className={styles.container} onClick={(e) => e.stopPropagation()}>
<div className={styles.entriesList}>
{checksData.presentDefaultEnabledMetrics?.map((entry) => (
<PresentEntryRow
key={`present-default-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Default enabled metrics"
itemType="metrics"
/>
))}
{checksData.presentOptionalMetrics?.map((entry) => (
<PresentEntryRow
key={`present-optional-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Optional metrics"
itemType="metrics"
/>
))}
{checksData.presentRequiredAttributes?.map((entry) => (
<PresentEntryRow
key={`present-attrs-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Required attributes"
itemType="attributes"
/>
))}
{checksData.missingDefaultEnabledMetrics?.map((entry) => (
<MissingEntryRow
key={`missing-default-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Missing default metrics"
itemType="metrics"
/>
))}
{checksData.missingOptionalMetrics?.map((entry) => (
<MissingEntryRow
key={`missing-optional-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Missing optional metrics"
itemType="metrics"
/>
))}
{checksData.missingRequiredAttributes?.map((entry) => (
<MissingEntryRow
key={`missing-attrs-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Missing required attributes"
itemType="attributes"
/>
))}
</div>
</div>
</Callout>
</div>
);
}

View File

@@ -0,0 +1,27 @@
.entryRow {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
color: var(--l1-foreground);
}
.missingIcon {
color: var(--warning-background);
flex-shrink: 0;
}
.learnMoreLink {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 4px;
svg {
margin-left: 4px;
}
}
.divider {
--divider-color: var(--callout-warning-border);
}

View File

@@ -0,0 +1,66 @@
import styles from './MissingEntryRow.module.scss';
import { ExternalLink, TriangleAlert } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import { Divider } from '@signozhq/ui/divider';
import type {
InframonitoringtypesMissingAttributesComponentEntryDTO,
InframonitoringtypesMissingMetricsComponentEntryDTO,
} from 'api/generated/services/sigNoz.schemas';
type MissingEntryRowProps =
| {
entry: InframonitoringtypesMissingMetricsComponentEntryDTO;
typeLabel: string;
itemType: 'metrics';
}
| {
entry: InframonitoringtypesMissingAttributesComponentEntryDTO;
typeLabel: string;
itemType: 'attributes';
};
export function MissingEntryRow({
entry,
typeLabel,
itemType,
}: MissingEntryRowProps): JSX.Element {
const items = itemType === 'metrics' ? entry.metrics : entry.attributes;
return (
<div className={styles.entryRow}>
<TriangleAlert size={14} className={styles.missingIcon} />
<Typography.Text size="base" weight="medium">
{typeLabel}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text
size="base"
weight="semibold"
className={styles.entryRowMetric}
>
{items?.join(', ')}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text size="base" italic color="muted">
{entry.associatedComponent.name}
</Typography.Text>
{entry.documentationLink && (
<Typography.Link
href={entry.documentationLink}
target="_blank"
rel="noopener noreferrer"
size="base"
className={styles.learnMoreLink}
>
Learn here
<ExternalLink size={12} />
</Typography.Link>
)}
<Divider className={styles.divider} />
</div>
);
}

View File

@@ -0,0 +1,16 @@
.entryRow {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
color: var(--l1-foreground);
}
.presentIcon {
color: var(--success-background);
flex-shrink: 0;
}
.divider {
--divider-color: var(--callout-warning-border);
}

View File

@@ -0,0 +1,54 @@
import styles from './PresentEntryRow.module.scss';
import { CircleCheck } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import { Divider } from '@signozhq/ui/divider';
import type {
InframonitoringtypesAttributesComponentEntryDTO,
InframonitoringtypesMetricsComponentEntryDTO,
} from 'api/generated/services/sigNoz.schemas';
type PresentEntryRowProps =
| {
entry: InframonitoringtypesMetricsComponentEntryDTO;
typeLabel: string;
itemType: 'metrics';
}
| {
entry: InframonitoringtypesAttributesComponentEntryDTO;
typeLabel: string;
itemType: 'attributes';
};
export function PresentEntryRow({
entry,
typeLabel,
itemType,
}: PresentEntryRowProps): JSX.Element {
const items = itemType === 'metrics' ? entry.metrics : entry.attributes;
return (
<div className={styles.entryRow}>
<CircleCheck size={14} className={styles.presentIcon} />
<Typography.Text size="base" weight="medium">
{typeLabel}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text
size="base"
weight="semibold"
className={styles.entryRowMetric}
>
{items?.join(', ')}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text size="base" italic color="muted">
{entry.associatedComponent.name}
</Typography.Text>
<Divider className={styles.divider} />
</div>
);
}

View File

@@ -0,0 +1,60 @@
import getLocalStorage from 'api/browser/localstorage/get';
import {
InframonitoringtypesChecksDTO,
InframonitoringtypesCheckTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEntity } from '../../../constants';
export const ENTITY_TO_CHECK_TYPE: Record<
InfraMonitoringEntity,
InframonitoringtypesCheckTypeDTO
> = {
[InfraMonitoringEntity.HOSTS]: InframonitoringtypesCheckTypeDTO.hosts,
[InfraMonitoringEntity.PODS]: InframonitoringtypesCheckTypeDTO.pods,
[InfraMonitoringEntity.NODES]: InframonitoringtypesCheckTypeDTO.nodes,
[InfraMonitoringEntity.NAMESPACES]:
InframonitoringtypesCheckTypeDTO.namespaces,
[InfraMonitoringEntity.CLUSTERS]: InframonitoringtypesCheckTypeDTO.clusters,
[InfraMonitoringEntity.DEPLOYMENTS]:
InframonitoringtypesCheckTypeDTO.deployments,
[InfraMonitoringEntity.STATEFULSETS]:
InframonitoringtypesCheckTypeDTO.statefulsets,
[InfraMonitoringEntity.DAEMONSETS]:
InframonitoringtypesCheckTypeDTO.daemonsets,
[InfraMonitoringEntity.JOBS]: InframonitoringtypesCheckTypeDTO.jobs,
[InfraMonitoringEntity.VOLUMES]: InframonitoringtypesCheckTypeDTO.volumes,
[InfraMonitoringEntity.CONTAINERS]:
InframonitoringtypesCheckTypeDTO.kube_containers,
};
export function hasMissingEntries(
data: InframonitoringtypesChecksDTO,
): boolean {
return (
(data.missingDefaultEnabledMetrics?.length ?? 0) > 0 ||
(data.missingOptionalMetrics?.length ?? 0) > 0 ||
(data.missingRequiredAttributes?.length ?? 0) > 0
);
}
export function hasAnyEntries(data: InframonitoringtypesChecksDTO): boolean {
return (
(data.presentDefaultEnabledMetrics?.length ?? 0) > 0 ||
(data.presentOptionalMetrics?.length ?? 0) > 0 ||
(data.presentRequiredAttributes?.length ?? 0) > 0 ||
(data.missingDefaultEnabledMetrics?.length ?? 0) > 0 ||
(data.missingOptionalMetrics?.length ?? 0) > 0 ||
(data.missingRequiredAttributes?.length ?? 0) > 0
);
}
const STORAGE_PREFIX = '@signozhq/k8s-instrumentation-checks-expanded';
export function getStorageKey(entity: InfraMonitoringEntity): string {
return `${STORAGE_PREFIX}-${entity}`;
}
export function readExpandedState(entity: InfraMonitoringEntity): boolean {
const stored = getLocalStorage(getStorageKey(entity));
return stored === null ? true : stored === 'true';
}

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listClusters } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesClusterRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -14,7 +18,9 @@ import { InfraMonitoringEntity } from '../constants';
import {
clusterWidgetInfo,
getClusterMetricsQueryPayload,
k8sClusterDetailsCountsConfig,
k8sClusterDetailsMetadataConfig,
k8sClusterGetCountsFilterExpression,
k8sClusterGetEntityName,
k8sClusterGetSelectedItemExpression,
k8sClusterInitialEventsExpression,
@@ -67,13 +73,12 @@ function K8sClustersList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch clusters';
return {
type: 'list' as const,
records: [] as InframonitoringtypesClusterRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -86,7 +91,7 @@ function K8sClustersList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesClusterRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listClusters(
@@ -104,11 +109,10 @@ function K8sClustersList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch cluster';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -136,6 +140,8 @@ function K8sClustersList({
getInitialLogTracesExpression={k8sClusterInitialLogTracesExpression}
getInitialEventsExpression={k8sClusterInitialEventsExpression}
metadataConfig={k8sClusterDetailsMetadataConfig}
countsConfig={k8sClusterDetailsCountsConfig}
getCountsFilterExpression={k8sClusterGetCountsFilterExpression}
entityWidgetInfo={clusterWidgetInfo}
getEntityQueryPayload={getClusterMetricsQueryPayload}
queryKeyPrefix="cluster"

View File

@@ -6,9 +6,15 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import {
K8sDetailsCountConfig,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
@@ -23,6 +29,40 @@ export const k8sClusterGetSelectedItemExpression = (
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesClusterRecordDTO>[] =
[{ label: 'Cluster Name', getValue: (p): string => p.clusterName || '' }];
export const k8sClusterDetailsCountsConfig: K8sDetailsCountConfig<InframonitoringtypesClusterRecordDTO>[] =
[
{
label: 'Namespaces',
getValue: (p): number => p.counts?.namespaces ?? 0,
targetCategory: InfraMonitoringEntity.NAMESPACES,
},
{
label: 'Nodes',
getValue: (p): number => p.counts?.nodes ?? 0,
targetCategory: InfraMonitoringEntity.NODES,
},
{
label: 'Deployments',
getValue: (p): number => p.counts?.deployments ?? 0,
targetCategory: InfraMonitoringEntity.DEPLOYMENTS,
},
{
label: 'StatefulSets',
getValue: (p): number => p.counts?.statefulSets ?? 0,
targetCategory: InfraMonitoringEntity.STATEFULSETS,
},
{
label: 'DaemonSets',
getValue: (p): number => p.counts?.daemonSets ?? 0,
targetCategory: InfraMonitoringEntity.DAEMONSETS,
},
{
label: 'Jobs',
getValue: (p): number => p.counts?.jobs ?? 0,
targetCategory: InfraMonitoringEntity.JOBS,
},
];
export const k8sClusterInitialEventsExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string =>
@@ -43,38 +83,54 @@ export const k8sClusterGetEntityName = (
item: InframonitoringtypesClusterRecordDTO,
): string => item.clusterName || '';
export const k8sClusterGetCountsFilterExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string =>
`k8s.cluster.name = ${formatValueForExpression(item.clusterName ?? '')}`;
export const clusterWidgetInfo = [
{
title: 'CPU Usage, allocatable',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/clusters/#cpu-usage-allocatable',
},
{
title: 'Memory Usage, allocatable',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/clusters/#memory-usage-allocatable',
},
{
title: 'Ready Nodes',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#ready-nodes',
},
{
title: 'NotReady Nodes',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#notready-nodes',
},
{
title: 'Deployments available and desired',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/clusters/#deployments-available-and-desired',
},
{
title: 'Statefulset pods',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#statefulset-pods',
},
{
title: 'Daemonset nodes',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#daemonset-nodes',
},
{
title: 'Jobs',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#jobs',
},
];

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listDaemonSets } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesDaemonSetRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
@@ -65,13 +69,12 @@ function K8sDaemonSetsList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch daemonsets';
return {
type: 'list' as const,
records: [] as InframonitoringtypesDaemonSetRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -83,7 +86,7 @@ function K8sDaemonSetsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesDaemonSetRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listDaemonSets(
@@ -100,11 +103,10 @@ function K8sDaemonSetsList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch daemonset';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},

View File

@@ -71,18 +71,25 @@ export const daemonSetWidgetInfo = [
{
title: 'CPU usage, request, limits',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/daemonsets/#cpu-usage-request-limits',
},
{
title: 'Memory usage, request, limits',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/daemonsets/#memory-usage-request-limits',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/daemonsets/#network-io',
},
{
title: 'Network errors count',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/daemonsets/#network-errors-count',
},
];

View File

@@ -140,10 +140,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
},
},
{
id: 'node_status',
id: 'scheduled_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#node-status">
Node Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#scheduled-nodes">
Scheduled Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.currentNodes,

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listDeployments } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesDeploymentRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -68,13 +72,12 @@ function K8sDeploymentsList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch deployments';
return {
type: 'list' as const,
records: [] as InframonitoringtypesDeploymentRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -87,7 +90,7 @@ function K8sDeploymentsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesDeploymentRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listDeployments(
@@ -105,11 +108,10 @@ function K8sDeploymentsList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch deployment';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},

View File

@@ -71,18 +71,25 @@ export const deploymentWidgetInfo = [
{
title: 'CPU usage, request, limits',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/deployments/#cpu-usage-request-limits',
},
{
title: 'Memory usage, request, limits',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/deployments/#memory-usage-request-limits',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/deployments/#network-io',
},
{
title: 'Network error count',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/deployments/#network-error-count',
},
];

View File

@@ -133,10 +133,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
},
},
{
id: 'replica_status',
id: 'pod_replicas',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#replica-status">
Replica Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#pod-replicas">
Pod Replicas
</ColumnHeader>
),
accessorFn: (row): number => row.availablePods,

View File

@@ -0,0 +1,32 @@
.chartHeader {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
}
.infoIcon {
display: inline-flex;
align-items: center;
color: var(--l2-foreground);
cursor: pointer;
&:hover {
color: var(--text-slate-primary);
}
}
.metricsExplorerLink {
display: inline-flex;
align-items: center;
color: var(--l2-foreground);
transition: opacity 0.2s;
&:hover {
color: var(--l3-foreground);
}
}
.chartHeaderLabel {
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
}

View File

@@ -0,0 +1,83 @@
import { Link } from 'react-router-dom';
import { Compass, Info } from '@signozhq/icons';
import { Tooltip } from 'antd';
import styles from './ChartHeader.module.scss';
const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
interface ChartHeaderProps {
title: string;
docPath?: string;
tooltip?: string;
metricsExplorerUrl?: string;
metricsExplorerTestId?: string;
}
function ChartHeader({
title,
docPath,
tooltip,
metricsExplorerUrl,
metricsExplorerTestId = 'open-metrics-explorer',
}: ChartHeaderProps): JSX.Element {
const renderInfoIcon = (): React.ReactNode => {
if (docPath) {
const tooltipTitle = tooltip || 'Not sure what this represents?';
return (
<Tooltip
arrow
title={
<>
{tooltipTitle}{' '}
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener"
onClick={(e): void => e.stopPropagation()}
>
Learn more.
</a>
</>
}
>
<span className={styles.infoIcon} data-testid="chart-header-info-icon">
<Info size="md" />
</span>
</Tooltip>
);
}
if (tooltip) {
return (
<Tooltip title={tooltip}>
<span className={styles.infoIcon} data-testid="chart-header-info-icon">
<Info size="md" />
</span>
</Tooltip>
);
}
return null;
};
return (
<div className={styles.chartHeader} data-testid="chart-header">
<span className={styles.chartHeaderLabel}>{title}</span>
{renderInfoIcon()}
{metricsExplorerUrl && (
<Tooltip title="Open in Metrics Explorer">
<Link
to={metricsExplorerUrl}
className={styles.metricsExplorerLink}
data-testid={metricsExplorerTestId}
>
<Compass size={14} />
</Link>
</Tooltip>
)}
</div>
);
}
export default ChartHeader;

View File

@@ -14,28 +14,6 @@
box-sizing: border-box;
}
.entityMetricsTitleContainer {
display: flex;
align-items: center;
gap: 8px;
}
.entityMetricsTitle {
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
}
.metricsExplorerLink {
display: flex;
align-items: center;
color: var(--l2-foreground);
transition: opacity 0.2s;
&:hover {
color: var(--l3-foreground);
}
}
.metricsHeader {
display: flex;
justify-content: flex-end;

View File

@@ -1,8 +1,6 @@
import { useCallback, useMemo, useRef } from 'react';
import { UseQueryResult } from 'react-query';
import { Link } from 'react-router-dom';
import { Compass } from '@signozhq/icons';
import { Skeleton, Tooltip } from 'antd';
import { Skeleton } from 'antd';
import cx from 'classnames';
import { PANEL_TYPES } from 'constants/queryBuilder';
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
@@ -24,6 +22,7 @@ import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { getMetricsExplorerUrl } from 'utils/explorerUtils';
import { buildEntityMetricsChartConfig } from './configBuilder';
import ChartHeader from './ChartHeader';
import { useEntityMetrics } from './hooks';
import { isKeyNotFoundError } from '../utils';
@@ -47,6 +46,7 @@ interface EntityMetricsProps<T> {
entityWidgetInfo: {
title: string;
yAxisUnit: string;
docPath?: string;
}[];
getEntityQueryPayload: (
node: T,
@@ -207,31 +207,24 @@ function EntityMetrics<T>({
key={entityWidgetInfo[idx].title}
className={styles.entityMetricsCol}
>
<div className={styles.entityMetricsTitleContainer}>
<span className={styles.entityMetricsTitle}>
{entityWidgetInfo[idx].title}
</span>
{queryPayloads[idx] &&
queryPayloads[idx].graphType !== PANEL_TYPES.TABLE && (
<Tooltip title="Open in Metrics Explorer">
<Link
to={getMetricsExplorerUrl({
query: queryPayloads[idx].query,
...(selectedInterval && selectedInterval !== 'custom'
? { relativeTime: selectedInterval }
: {
startTimeMs: timeRange.startTime * 1000,
endTimeMs: timeRange.endTime * 1000,
}),
})}
className={styles.metricsExplorerLink}
data-testid={`open-metrics-explorer-${idx}`}
>
<Compass size={14} />
</Link>
</Tooltip>
)}
</div>
<ChartHeader
title={entityWidgetInfo[idx].title}
docPath={entityWidgetInfo[idx].docPath}
metricsExplorerUrl={
queryPayloads[idx] && queryPayloads[idx].graphType !== PANEL_TYPES.TABLE
? getMetricsExplorerUrl({
query: queryPayloads[idx].query,
...(selectedInterval && selectedInterval !== 'custom'
? { relativeTime: selectedInterval }
: {
startTimeMs: timeRange.startTime * 1000,
endTimeMs: timeRange.endTime * 1000,
}),
})
: undefined
}
metricsExplorerTestId={`open-metrics-explorer-${idx}`}
/>
<div className={styles.entityMetricsCard} ref={graphRef}>
{renderCardContent(query, idx)}
</div>

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listJobs } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesJobRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -68,13 +72,12 @@ function K8sJobsList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch jobs';
return {
type: 'list' as const,
records: [] as InframonitoringtypesJobRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -87,7 +90,7 @@ function K8sJobsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesJobRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listJobs(
@@ -105,11 +108,10 @@ function K8sJobsList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch job';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},

View File

@@ -70,18 +70,22 @@ export const jobWidgetInfo = [
{
title: 'CPU usage',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#cpu-usage',
},
{
title: 'Memory Usage',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#memory-usage',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#network-io',
},
{
title: 'Network errors count',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#network-errors-count',
},
];

View File

@@ -132,10 +132,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
},
},
{
id: 'completion_status',
id: 'completion',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#completion-status">
Completion Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#completion">
Completions
</ColumnHeader>
),
accessorFn: (row): number => row.successfulPods,
@@ -155,7 +155,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
{
value: row.desiredSuccessfulPods,
label: 'Desired',
color: Color.BG_ROBIN_500,
color: Color.BG_AMBER_500,
},
]}
/>

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listNamespaces } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesNamespaceRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -14,7 +18,9 @@ import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getNamespaceMetricsQueryPayload,
k8sNamespaceDetailsCountsConfig,
k8sNamespaceDetailsMetadataConfig,
k8sNamespaceGetCountsFilterExpression,
k8sNamespaceGetEntityName,
k8sNamespaceGetSelectedItemExpression,
k8sNamespaceInitialEventsExpression,
@@ -68,13 +74,12 @@ function K8sNamespacesList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch namespaces';
return {
type: 'list' as const,
records: [] as InframonitoringtypesNamespaceRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -87,7 +92,7 @@ function K8sNamespacesList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesNamespaceRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listNamespaces(
@@ -105,11 +110,10 @@ function K8sNamespacesList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch namespace';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -137,6 +141,8 @@ function K8sNamespacesList({
getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression}
getInitialEventsExpression={k8sNamespaceInitialEventsExpression}
metadataConfig={k8sNamespaceDetailsMetadataConfig}
countsConfig={k8sNamespaceDetailsCountsConfig}
getCountsFilterExpression={k8sNamespaceGetCountsFilterExpression}
entityWidgetInfo={namespaceWidgetInfo}
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
queryKeyPrefix="namespace"

View File

@@ -6,9 +6,16 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
K8sDetailsCountConfig,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
@@ -33,6 +40,30 @@ export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframo
},
];
export const k8sNamespaceDetailsCountsConfig: K8sDetailsCountConfig<InframonitoringtypesNamespaceRecordDTO>[] =
[
{
label: 'Deployments',
getValue: (p): number => p.counts?.deployments ?? 0,
targetCategory: InfraMonitoringEntity.DEPLOYMENTS,
},
{
label: 'StatefulSets',
getValue: (p): number => p.counts?.statefulSets ?? 0,
targetCategory: InfraMonitoringEntity.STATEFULSETS,
},
{
label: 'DaemonSets',
getValue: (p): number => p.counts?.daemonSets ?? 0,
targetCategory: InfraMonitoringEntity.DAEMONSETS,
},
{
label: 'Jobs',
getValue: (p): number => p.counts?.jobs ?? 0,
targetCategory: InfraMonitoringEntity.JOBS,
},
];
export const k8sNamespaceInitialEventsExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string =>
@@ -55,46 +86,78 @@ export const k8sNamespaceGetEntityName = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => item.namespaceName || '';
export const k8sNamespaceGetCountsFilterExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => {
const clusterName = item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME];
const clauses: string[] = [];
if (clusterName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(clusterName)}`,
);
}
if (item.namespaceName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(item.namespaceName)}`,
);
}
return clauses.join(' AND ');
};
export const namespaceWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#cpu-usage-cores',
},
{
title: 'Memory Usage (bytes)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/namespaces/#memory-usage-bytes',
},
{
title: 'Pods CPU (top 10)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#pods-cpu-top-10',
},
{
title: 'Pods Memory (top 10)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/namespaces/#pods-memory-top-10',
},
{
title: 'Network rate',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#network-rate',
},
{
title: 'Network errors',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#network-errors',
},
{
title: 'StatefulSets',
title: 'StatefulSets (pods)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#statefulsets',
},
{
title: 'ReplicaSets',
title: 'ReplicaSets (pods)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#replicasets',
},
{
title: 'DaemonSets',
title: 'DaemonSets (nodes)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#daemonsets',
},
{
title: 'Deployments',
title: 'Deployments (pods)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#deployments',
},
];

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listNodes } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesNodeRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -67,13 +71,12 @@ function K8sNodesList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch nodes';
return {
type: 'list' as const,
records: [] as InframonitoringtypesNodeRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -86,7 +89,7 @@ function K8sNodesList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesNodeRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listNodes(
@@ -104,11 +107,10 @@ function K8sNodesList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch node';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},

View File

@@ -57,42 +57,53 @@ export const nodeWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-cores',
},
{
title: 'Memory Usage (bytes)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#memory-usage-bytes',
},
{
title: 'CPU Usage (%)',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-',
},
{
title: 'Memory Usage (%)',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#memory-usage-',
},
{
title: 'Pods by CPU (top 10)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#pods-by-cpu-top-10',
},
{
title: 'Pods by Memory (top 10)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#pods-by-memory-top-10',
},
{
title: 'Network error count',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#network-error-count',
},
{
title: 'Network IO rate',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#network-io-rate',
},
{
title: 'Filesystem usage (bytes)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/nodes/#filesystem-usage-bytes',
},
{
title: 'Filesystem usage (%)',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#filesystem-usage-',
},
];

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listPods } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesPodRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -67,13 +71,12 @@ function K8sPodsList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch pods';
return {
type: 'list' as const,
records: [] as InframonitoringtypesPodRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -86,7 +89,7 @@ function K8sPodsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesPodRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listPods(
@@ -104,11 +107,10 @@ function K8sPodsList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch pod';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},

View File

@@ -67,54 +67,74 @@ export const podWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/pods/#cpu-usage-cores-1',
},
{
title: 'CPU Request, Limit Utilization',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#cpu-request-limit-utilization',
},
{
title: 'Memory Usage (bytes)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/pods/#memory-usage-bytes',
},
{
title: 'Memory Request, Limit Utilization',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-request-limit-utilization',
},
{
title: 'Memory by State',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/pods/#memory-by-state',
},
{
title: 'Memory Major Page Faults',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-major-page-faults',
},
{
title: 'CPU Usage by Container (cores)',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#cpu-usage-by-container-cores',
},
{
title: 'CPU Request, Limit Utilization by Container',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#cpu-request-limit-utilization-by-container',
},
{
title: 'Memory Usage by Container (bytes)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-usage-by-container-bytes',
},
{
title: 'Memory Request, Limit Utilization by Container',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-request-limit-utilization-by-container',
},
{
title: 'Network rate',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/pods/#network-rate',
},
{
title: 'Network errors',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/pods/#network-errors',
},
{
title: 'File system (bytes)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/pods/#file-system-bytes',
},
];

View File

@@ -152,7 +152,11 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'podAge',
header: 'Age',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#age">
Age
</ColumnHeader>
),
accessorFn: (row): number => row.podAge,
width: { min: 100 },
enableSort: false,
@@ -316,7 +320,11 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'namespace',
header: 'Namespace',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#additional-columns">
Namespace
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { default: 100 },
@@ -328,7 +336,11 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'node',
header: 'Node',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#additional-columns">
Node
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME] || '',
width: { default: 100 },
@@ -340,7 +352,11 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'cluster',
header: 'Cluster',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#additional-columns">
Cluster
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
width: { default: 100 },

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listStatefulSets } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesStatefulSetRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -68,13 +72,12 @@ function K8sStatefulSetsList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch statefulsets';
return {
type: 'list' as const,
records: [] as InframonitoringtypesStatefulSetRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -87,7 +90,7 @@ function K8sStatefulSetsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesStatefulSetRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listStatefulSets(
@@ -105,11 +108,10 @@ function K8sStatefulSetsList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch statefulset';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},

View File

@@ -72,26 +72,37 @@ export const statefulSetWidgetInfo = [
{
title: 'CPU usage, request, limits',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#cpu-usage-request-limits',
},
{
title: 'CPU request, limit util (%)',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#cpu-request-limit-utilization-',
},
{
title: 'Memory usage, request, limits',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#memory-usage-request-limits',
},
{
title: 'Memory request, limit util (%)',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#memory-request-limit-utilization-',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/statefulsets/#network-io',
},
{
title: 'Network errors count',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#network-errors-count',
},
];

View File

@@ -141,10 +141,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
},
},
{
id: 'pod_status',
id: 'pod_replicas',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-status">
Pod Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-replicas">
Pod Replicas
</ColumnHeader>
),
accessorFn: (row): number => row.currentPods,

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listVolumes } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesVolumeRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -68,13 +72,12 @@ function K8sVolumesList({
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch volumes';
return {
type: 'list' as const,
records: [] as InframonitoringtypesVolumeRecordDTO[],
total: 0,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},
@@ -87,7 +90,7 @@ function K8sVolumesList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesVolumeRecordDTO | null;
error?: string | null;
error?: APIError | null;
}> => {
try {
const response = await listVolumes(
@@ -105,11 +108,10 @@ function K8sVolumesList({
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch volume';
return {
data: null,
error: errMsg,
error:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
};
}
},

View File

@@ -69,22 +69,27 @@ export const volumeWidgetInfo = [
{
title: 'Volume available',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-available',
},
{
title: 'Volume capacity',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-capacity',
},
{
title: 'Volume inodes used',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes-used',
},
{
title: 'Volume inodes',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes',
},
{
title: 'Volume inodes free',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes-free',
},
];

View File

@@ -2872,17 +2872,72 @@ export const nodeWidgetInfo = [
];
export const hostWidgetInfo = [
{ title: 'CPU Usage', yAxisUnit: 'percentunit' },
{ title: 'Memory Usage', yAxisUnit: 'bytes' },
{ title: 'System Load Average', yAxisUnit: '' },
{ title: 'Network usage (bytes)', yAxisUnit: 'bytes' },
{ title: 'Network usage (packet/s)', yAxisUnit: 'pps' },
{ title: 'Network errors', yAxisUnit: 'short' },
{ title: 'Network drops', yAxisUnit: 'short' },
{ title: 'Network connections', yAxisUnit: 'short' },
{ title: 'System disk io (bytes transferred)', yAxisUnit: 'bytes' },
{ title: 'System disk operations/s', yAxisUnit: 'short' },
{ title: 'Queue size', yAxisUnit: 'short' },
{ title: 'System disk operation time/s', yAxisUnit: 's' },
{ title: 'Disk Usage (%) by mountpoint', yAxisUnit: 'percentunit' },
{
title: 'CPU Usage',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/host-monitoring/#cpu-usage',
},
{
title: 'Memory Usage',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/host-monitoring/#memory-usage',
},
{
title: 'System Load Average',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/host-monitoring/#system-load-average',
},
{
title: 'Network usage (bytes)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/host-monitoring/#network-usage-bytes',
},
{
title: 'Network usage (packet/s)',
yAxisUnit: 'pps',
docPath: '/infrastructure-monitoring/host-monitoring/#network-usage-packetss',
},
{
title: 'Network errors',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/host-monitoring/#network-errors',
},
{
title: 'Network drops',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/host-monitoring/#network-drops',
},
{
title: 'Network connections',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/host-monitoring/#network-connections',
},
{
title: 'System disk io (bytes transferred)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/host-monitoring/#system-disk-io-bytes',
},
{
title: 'System disk operations/s',
yAxisUnit: 'short',
docPath:
'/infrastructure-monitoring/host-monitoring/#system-disk-operationss',
},
{
title: 'Queue size',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/host-monitoring/#queue-size',
},
{
title: 'System disk operation time/s',
yAxisUnit: 's',
docPath:
'/infrastructure-monitoring/host-monitoring/#system-disk-operation-times',
},
{
title: 'Disk Usage (%) by mountpoint',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/host-monitoring/#disk-usage--by-mountpoint',
},
];

View File

@@ -32,12 +32,11 @@ unaryExpression
;
// Primary constructs: grouped expressions, a comparison (key op value),
// a function call, a search() full-text call, or a free-text string
// a function call, or a full-text string
primary
: LPAREN orExpression RPAREN
| comparison
| functionCall
| searchCall
| fullText
| key
| value
@@ -111,18 +110,6 @@ functionCall
: (HASTOKEN | HAS | HASANY | HASALL) LPAREN functionParamList RPAREN
;
/*
* Full-text search call: search('needle')
*
* Uses the shared functionParamList so future scoped forms like
* search(body, 'abc') / search(attribute, 'abc') need no grammar change. Today
* only a single needle is supported. Unlike bare/quoted free text (`fullText`),
* which only targets the body column, search() fans out across every field.
*/
searchCall
: SEARCH LPAREN functionParamList RPAREN
;
// Function parameters can be keys, single scalar values, or arrays
functionParamList
: functionParam ( COMMA functionParam )*
@@ -197,7 +184,6 @@ HASTOKEN : [Hh][Aa][Ss][Tt][Oo][Kk][Ee][Nn];
HAS : [Hh][Aa][Ss] ;
HASANY : [Hh][Aa][Ss][Aa][Nn][Yy] ;
HASALL : [Hh][Aa][Ss][Aa][Ll][Ll] ;
SEARCH : [Ss][Ee][Aa][Rr][Cc][Hh] ;
// Potential boolean constants
BOOL

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"log/slog"
"reflect"
"sort"
"sync"
"testing"
@@ -28,6 +27,7 @@ import (
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -110,42 +110,44 @@ func TestAggrGroup(t *testing.T) {
}
)
var (
last = time.Now()
current = time.Now()
lastCurMtx = &sync.Mutex{}
alertsCh = make(chan alertmanagertypes.AlertSlice)
)
type notification struct {
alerts alertmanagertypes.AlertSlice
notifiedAt time.Time
}
alertsCh := make(chan notification)
ntfy := func(ctx context.Context, alerts ...*alertmanagertypes.Alert) bool {
// Validate that the context is properly populated.
if _, ok := notify.Now(ctx); !ok {
t.Errorf("now missing")
notifiedAt, ok := notify.Now(ctx)
assert.True(t, ok, "now missing")
_, ok = notify.GroupKey(ctx)
assert.True(t, ok, "group key missing")
lbls, ok := notify.GroupLabels(ctx)
if assert.True(t, ok, "group labels missing") {
assert.Equal(t, lset, lbls, "wrong group labels")
}
if _, ok := notify.GroupKey(ctx); !ok {
t.Errorf("group key missing")
rcv, ok := notify.ReceiverName(ctx)
if assert.True(t, ok, "receiver missing") {
assert.Equal(t, opts.Receiver, rcv, "wrong receiver")
}
if lbls, ok := notify.GroupLabels(ctx); !ok || !reflect.DeepEqual(lbls, lset) {
t.Errorf("wrong group labels: %q", lbls)
}
if rcv, ok := notify.ReceiverName(ctx); !ok || rcv != opts.Receiver {
t.Errorf("wrong receiver: %q", rcv)
}
if ri, ok := notify.RepeatInterval(ctx); !ok || ri != notificationConfig.Renotify.RenotifyInterval {
t.Errorf("wrong repeat interval: %q", ri)
ri, ok := notify.RepeatInterval(ctx)
if assert.True(t, ok, "repeat interval missing") {
assert.Equal(t, notificationConfig.Renotify.RenotifyInterval, ri, "wrong repeat interval")
}
lastCurMtx.Lock()
last = current
// Subtract a millisecond to allow for races.
current = time.Now().Add(-time.Millisecond)
lastCurMtx.Unlock()
alertsCh <- alertmanagertypes.AlertSlice(alerts)
alertsCh <- notification{
alerts: alertmanagertypes.AlertSlice(alerts),
notifiedAt: notifiedAt,
}
return true
}
assertNotifiedAfter := func(previous, current time.Time, interval time.Duration) {
t.Helper()
require.GreaterOrEqual(t, current.Sub(previous), interval, "received batch too early")
}
removeEndsAt := func(as alertmanagertypes.AlertSlice) alertmanagertypes.AlertSlice {
for i, a := range as {
ac := *a
@@ -156,29 +158,26 @@ func TestAggrGroup(t *testing.T) {
}
// Test regular situation where we wait for group_wait to send out alerts.
groupStartedAt := time.Now()
ag := newAggrGroup(context.Background(), lset, route, nil, promslog.NewNopLogger(), notificationConfig.Renotify.RenotifyInterval)
go ag.run(ntfy)
ag.insert(a1)
var lastNotificationAt time.Time
select {
case <-time.After(2 * opts.GroupWait):
t.Fatalf("expected initial batch after group_wait")
require.FailNow(t, "expected initial batch after group_wait")
case batch := <-alertsCh:
lastCurMtx.Lock()
s := time.Since(last)
lastCurMtx.Unlock()
if s < opts.GroupWait {
t.Fatalf("received batch too early after %v", s)
}
case notification := <-alertsCh:
assertNotifiedAfter(groupStartedAt, notification.notifiedAt, opts.GroupWait)
lastNotificationAt = notification.notifiedAt
batch := notification.alerts
exp := removeEndsAt(alertmanagertypes.AlertSlice{a1})
sort.Sort(batch)
if !reflect.DeepEqual(batch, exp) {
t.Fatalf("expected alerts %v but got %v", exp, batch)
}
require.Equal(t, exp, batch)
}
for i := 0; i < 3; i++ {
@@ -187,21 +186,16 @@ func TestAggrGroup(t *testing.T) {
select {
case <-time.After(2 * opts.GroupInterval):
t.Fatalf("expected new batch after group interval but received none")
require.FailNow(t, "expected new batch after group interval but received none")
case batch := <-alertsCh:
lastCurMtx.Lock()
s := time.Since(last)
lastCurMtx.Unlock()
if s < opts.GroupInterval {
t.Fatalf("received batch too early after %v", s)
}
case notification := <-alertsCh:
assertNotifiedAfter(lastNotificationAt, notification.notifiedAt, opts.GroupInterval)
lastNotificationAt = notification.notifiedAt
batch := notification.alerts
exp := removeEndsAt(alertmanagertypes.AlertSlice{a1, a3})
sort.Sort(batch)
if !reflect.DeepEqual(batch, exp) {
t.Fatalf("expected alerts %v but got %v", exp, batch)
}
require.Equal(t, exp, batch)
}
}
@@ -220,15 +214,15 @@ func TestAggrGroup(t *testing.T) {
// a2 lies way in the past so the initial group_wait should be skipped.
select {
case <-time.After(opts.GroupWait / 2):
t.Fatalf("expected immediate alert but received none")
require.FailNow(t, "expected immediate alert but received none")
case batch := <-alertsCh:
case notification := <-alertsCh:
lastNotificationAt = notification.notifiedAt
batch := notification.alerts
exp := removeEndsAt(alertmanagertypes.AlertSlice{a1, a2})
sort.Sort(batch)
if !reflect.DeepEqual(batch, exp) {
t.Fatalf("expected alerts %v but got %v", exp, batch)
}
require.Equal(t, exp, batch)
}
for i := 0; i < 3; i++ {
@@ -237,21 +231,16 @@ func TestAggrGroup(t *testing.T) {
select {
case <-time.After(2 * opts.GroupInterval):
t.Fatalf("expected new batch after group interval but received none")
require.FailNow(t, "expected new batch after group interval but received none")
case batch := <-alertsCh:
lastCurMtx.Lock()
s := time.Since(last)
lastCurMtx.Unlock()
if s < opts.GroupInterval {
t.Fatalf("received batch too early after %v", s)
}
case notification := <-alertsCh:
assertNotifiedAfter(lastNotificationAt, notification.notifiedAt, opts.GroupInterval)
lastNotificationAt = notification.notifiedAt
batch := notification.alerts
exp := removeEndsAt(alertmanagertypes.AlertSlice{a1, a2, a3})
sort.Sort(batch)
if !reflect.DeepEqual(batch, exp) {
t.Fatalf("expected alerts %v but got %v", exp, batch)
}
require.Equal(t, exp, batch)
}
}
@@ -263,19 +252,14 @@ func TestAggrGroup(t *testing.T) {
select {
case <-time.After(2 * opts.GroupInterval):
t.Fatalf("expected new batch after group interval but received none")
case batch := <-alertsCh:
lastCurMtx.Lock()
s := time.Since(last)
lastCurMtx.Unlock()
if s < opts.GroupInterval {
t.Fatalf("received batch too early after %v", s)
}
require.FailNow(t, "expected new batch after group interval but received none")
case notification := <-alertsCh:
assertNotifiedAfter(lastNotificationAt, notification.notifiedAt, opts.GroupInterval)
lastNotificationAt = notification.notifiedAt
batch := notification.alerts
sort.Sort(batch)
if !reflect.DeepEqual(batch, exp) {
t.Fatalf("expected alerts %v but got %v", exp, batch)
}
require.Equal(t, exp, batch)
}
// Resolve all remaining alerts, they should be removed after the next batch was sent.
@@ -289,24 +273,16 @@ func TestAggrGroup(t *testing.T) {
select {
case <-time.After(2 * opts.GroupInterval):
t.Fatalf("expected new batch after group interval but received none")
require.FailNow(t, "expected new batch after group interval but received none")
case batch := <-alertsCh:
lastCurMtx.Lock()
s := time.Since(last)
lastCurMtx.Unlock()
if s < opts.GroupInterval {
t.Fatalf("received batch too early after %v", s)
}
case notification := <-alertsCh:
assertNotifiedAfter(lastNotificationAt, notification.notifiedAt, opts.GroupInterval)
batch := notification.alerts
sort.Sort(batch)
if !reflect.DeepEqual(batch, resolved) {
t.Fatalf("expected alerts %v but got %v", resolved, batch)
}
require.Equal(t, resolved, batch)
if !ag.empty() {
t.Fatalf("Expected aggregation group to be empty after resolving alerts: %v", ag)
}
require.Eventually(t, ag.empty, 2*opts.GroupInterval, 10*time.Millisecond, "expected aggregation group to be empty after resolving alerts: %v", ag)
}
ag.stop()
@@ -340,9 +316,7 @@ func TestGroupLabels(t *testing.T) {
ls := getGroupLabels(a, route.RouteOpts.GroupBy, false)
if !reflect.DeepEqual(ls, expLs) {
t.Fatalf("expected labels are %v, but got %v", expLs, ls)
}
require.Equal(t, expLs, ls)
}
func TestAggrRouteMap(t *testing.T) {
@@ -358,17 +332,13 @@ route:
group_interval: 1m
receiver: 'slack'`
conf, err := config.Load(confData)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer alerts.Close()
timeout := func(d time.Duration) time.Duration { return time.Duration(0) }
@@ -377,9 +347,7 @@ route:
store := nfroutingstoretest.NewMockSQLRouteStore()
store.MatchExpectationsInOrder(false)
nfManager, err := rulebasednotification.New(context.Background(), providerSettings, nfmanager.Config{}, store)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
orgId := "test-org"
ctx := context.Background()
@@ -497,9 +465,7 @@ route:
require.NoError(t, err)
}
err = alerts.Put(ctx, inputAlerts...)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// Let alerts get processed.
for i := 0; len(recorder.Alerts()) != 4; i++ {
@@ -631,17 +597,13 @@ route:
group_interval: 10ms
receiver: 'slack'`
conf, err := config.Load(confData)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer alerts.Close()
timeout := func(d time.Duration) time.Duration { return time.Duration(0) }
@@ -650,9 +612,7 @@ route:
store := nfroutingstoretest.NewMockSQLRouteStore()
store.MatchExpectationsInOrder(false)
nfManager, err := rulebasednotification.New(context.Background(), providerSettings, nfmanager.Config{}, store)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
orgId := "test-org"
ctx := context.Background()
@@ -799,9 +759,7 @@ route:
require.NoError(t, err)
}
err = alerts.Put(ctx, inputAlerts...)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
for i := 0; len(recorder.Alerts()) != 9; i++ {
time.Sleep(400 * time.Millisecond)
@@ -890,17 +848,13 @@ route:
group_interval: 10ms
receiver: 'slack'`
conf, err := config.Load(confData)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer alerts.Close()
timeout := func(d time.Duration) time.Duration { return time.Duration(0) }
@@ -909,9 +863,7 @@ route:
store := nfroutingstoretest.NewMockSQLRouteStore()
store.MatchExpectationsInOrder(false)
nfManager, err := rulebasednotification.New(context.Background(), providerSettings, nfmanager.Config{}, store)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
orgId := "test-org"
ctx := context.Background()
@@ -1029,9 +981,7 @@ route:
require.NoError(t, err)
}
err = alerts.Put(ctx, inputAlerts...)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
for i := 0; len(recorder.Alerts()) != 3 && i < 15; i++ {
time.Sleep(400 * time.Millisecond)
@@ -1160,9 +1110,7 @@ func TestDispatcherRace(t *testing.T) {
logger := promslog.NewNopLogger()
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer alerts.Close()
timeout := func(d time.Duration) time.Duration { return time.Duration(0) }
@@ -1188,17 +1136,13 @@ route:
group_interval: 5m
receiver: 'slack'`
conf, err := config.Load(confData)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
route := dispatch.NewRoute(conf.Route, nil)
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer alerts.Close()
timeout := func(d time.Duration) time.Duration { return d }
recorder := &recordStage{alerts: make(map[string]map[model.Fingerprint]*alertmanagertypes.Alert)}
@@ -1206,9 +1150,7 @@ route:
store := nfroutingstoretest.NewMockSQLRouteStore()
store.MatchExpectationsInOrder(false)
nfManager, err := rulebasednotification.New(context.Background(), providerSettings, nfmanager.Config{}, store)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
orgId := "test-org"
for i := 0; i < numAlerts; i++ {
@@ -1267,9 +1209,7 @@ func TestDispatcher_DoMaintenance(t *testing.T) {
marker := alertmanagertypes.NewMarker(r)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Minute, 0, nil, promslog.NewNopLogger(), prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
route := &dispatch.Route{
RouteOpts: dispatch.RouteOpts{
@@ -1363,18 +1303,14 @@ route:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conf, err := config.Load(tc.confData)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
defer alerts.Close()
timeout := func(d time.Duration) time.Duration { return time.Duration(0) }
@@ -1383,9 +1319,7 @@ route:
store := nfroutingstoretest.NewMockSQLRouteStore()
store.MatchExpectationsInOrder(false)
nfManager, err := rulebasednotification.New(context.Background(), providerSettings, nfmanager.Config{}, store)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
d := NewDispatcher(alerts, route, recorder, marker, timeout, nil, logger, metrics, nfManager, "test-org")
// setup the dispatcher for tests
d.receiverRoutes = map[string]*dispatch.Route{}

View File

@@ -101,12 +101,12 @@ func PrepareFilterExpression(labels map[string]string, whereClause string, group
}
// Visit implements the visitor for the query rule.
func (r *WhereClauseRewriter) Visit(tree antlr.ParseTree) any {
func (r *WhereClauseRewriter) Visit(tree antlr.ParseTree) interface{} {
return tree.Accept(r)
}
// VisitQuery visits the query node.
func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) any {
func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) interface{} {
if ctx.Expression() != nil {
ctx.Expression().Accept(r)
}
@@ -114,7 +114,7 @@ func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) any {
}
// VisitExpression visits the expression node.
func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) any {
func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) interface{} {
if ctx.OrExpression() != nil {
ctx.OrExpression().Accept(r)
}
@@ -122,7 +122,7 @@ func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) any
}
// VisitOrExpression visits OR expressions.
func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext) any {
func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext) interface{} {
for i, andExpr := range ctx.AllAndExpression() {
if i > 0 {
r.rewritten.WriteString(" OR ")
@@ -133,7 +133,7 @@ func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext)
}
// VisitAndExpression visits AND expressions.
func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContext) any {
func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContext) interface{} {
unaryExprs := ctx.AllUnaryExpression()
for i, unaryExpr := range unaryExprs {
if i > 0 {
@@ -151,7 +151,7 @@ func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContex
}
// VisitUnaryExpression visits unary expressions (with optional NOT).
func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionContext) any {
func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionContext) interface{} {
if ctx.NOT() != nil {
r.rewritten.WriteString("NOT ")
}
@@ -162,7 +162,7 @@ func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionCo
}
// VisitPrimary visits primary expressions.
func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) interface{} {
if ctx.LPAREN() != nil && ctx.RPAREN() != nil {
r.rewritten.WriteString("(")
if ctx.OrExpression() != nil {
@@ -173,8 +173,6 @@ func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
ctx.Comparison().Accept(r)
} else if ctx.FunctionCall() != nil {
ctx.FunctionCall().Accept(r)
} else if ctx.SearchCall() != nil {
ctx.SearchCall().Accept(r)
} else if ctx.FullText() != nil {
ctx.FullText().Accept(r)
} else if ctx.Key() != nil {
@@ -186,7 +184,7 @@ func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) any {
}
// VisitComparison visits comparison expressions.
func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any {
func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) interface{} {
if ctx.Key() == nil {
return nil
}
@@ -199,7 +197,7 @@ func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any
if _, partOfGroup := r.groupBySet[key]; partOfGroup {
// Case 1: Replace with actual value
escapedValue := escapeValueIfNeeded(value)
fmt.Fprintf(&r.rewritten, "%s=%s", key, escapedValue)
r.rewritten.WriteString(fmt.Sprintf("%s=%s", key, escapedValue))
return nil
}
}
@@ -307,7 +305,7 @@ func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) any
}
// VisitInClause visits IN clauses.
func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) any {
func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) interface{} {
r.rewritten.WriteString("IN ")
if ctx.LPAREN() != nil {
r.rewritten.WriteString("(")
@@ -328,7 +326,7 @@ func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) any {
}
// VisitNotInClause visits NOT IN clauses.
func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) any {
func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) interface{} {
r.rewritten.WriteString("NOT IN ")
if ctx.LPAREN() != nil {
r.rewritten.WriteString("(")
@@ -349,7 +347,7 @@ func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) a
}
// VisitValueList visits value lists.
func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) any {
func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) interface{} {
values := ctx.AllValue()
for i, val := range values {
if i > 0 {
@@ -361,20 +359,13 @@ func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) any {
}
// VisitFullText visits full text expressions.
func (r *WhereClauseRewriter) VisitFullText(ctx *parser.FullTextContext) any {
r.rewritten.WriteString(ctx.GetText())
return nil
}
// VisitSearchCall visits search() calls. It has no keys to rewrite, so it is
// preserved verbatim.
func (r *WhereClauseRewriter) VisitSearchCall(ctx *parser.SearchCallContext) any {
func (r *WhereClauseRewriter) VisitFullText(ctx *parser.FullTextContext) interface{} {
r.rewritten.WriteString(ctx.GetText())
return nil
}
// VisitFunctionCall visits function calls.
func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext) any {
func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext) interface{} {
// Write function name
if ctx.HAS() != nil {
r.rewritten.WriteString("has")
@@ -395,7 +386,7 @@ func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext)
}
// VisitFunctionParamList visits function parameter lists.
func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamListContext) any {
func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamListContext) interface{} {
params := ctx.AllFunctionParam()
for i, param := range params {
if i > 0 {
@@ -407,7 +398,7 @@ func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamLi
}
// VisitFunctionParam visits function parameters.
func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContext) any {
func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContext) interface{} {
if ctx.Key() != nil {
ctx.Key().Accept(r)
} else if ctx.Value() != nil {
@@ -419,7 +410,7 @@ func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContex
}
// VisitArray visits array expressions.
func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) any {
func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) interface{} {
r.rewritten.WriteString("[")
if ctx.ValueList() != nil {
ctx.ValueList().Accept(r)
@@ -429,13 +420,13 @@ func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) any {
}
// VisitValue visits value expressions.
func (r *WhereClauseRewriter) VisitValue(ctx *parser.ValueContext) any {
func (r *WhereClauseRewriter) VisitValue(ctx *parser.ValueContext) interface{} {
r.rewritten.WriteString(ctx.GetText())
return nil
}
// VisitKey visits key expressions.
func (r *WhereClauseRewriter) VisitKey(ctx *parser.KeyContext) any {
func (r *WhereClauseRewriter) VisitKey(ctx *parser.KeyContext) interface{} {
r.keysSeen[ctx.GetText()] = struct{}{}
r.rewritten.WriteString(ctx.GetText())
return nil

View File

@@ -29,19 +29,13 @@ func New(t *testing.T) flagger.Flagger {
// WithUseJSONBody returns a Flagger with use_json_body set to the given value.
func WithUseJSONBody(t *testing.T, enabled bool) flagger.Flagger {
return WithBooleanFlags(t, map[string]bool{
flagger.FeatureUseJSONBody.String(): enabled,
})
}
// WithBooleanFlags returns a Flagger with the given boolean feature flags set to
// the provided values (keyed by feature name, e.g. flagger.FeatureX.String()).
func WithBooleanFlags(t *testing.T, flags map[string]bool) flagger.Flagger {
t.Helper()
registry := flagger.MustNewRegistry()
cfg := flagger.Config{}
if len(flags) > 0 {
cfg.Config.Boolean = flags
if enabled {
cfg.Config.Boolean = map[string]bool{
flagger.FeatureUseJSONBody.String(): true,
}
}
fl, err := flagger.New(
context.Background(),

View File

@@ -15,7 +15,6 @@ var (
FeatureEnableAIObservability = featuretypes.MustNewName("enable_ai_observability")
FeatureEnableMetricsReduction = featuretypes.MustNewName("enable_metrics_reduction")
FeatureUseInfraMonitoringV2 = featuretypes.MustNewName("use_infra_monitoring_v2")
FeatureAllowLogsSearch = featuretypes.MustNewName("allow_logs_search")
)
func MustNewRegistry() featuretypes.Registry {
@@ -116,14 +115,6 @@ func MustNewRegistry() featuretypes.Registry {
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
&featuretypes.Feature{
Name: FeatureAllowLogsSearch,
Kind: featuretypes.KindBoolean,
Stage: featuretypes.StageExperimental,
Description: "Controls whether the search() function is enabled for log queries",
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
)
if err != nil {
panic(err)

View File

@@ -33,7 +33,13 @@ func (handler *handler) GetFieldsKeys(rw http.ResponseWriter, req *http.Request)
fieldKeySelector := telemetrytypes.NewFieldKeySelectorFromPostableFieldKeysParams(params)
keys, complete, err := handler.telemetryMetadataStore.GetKeys(ctx, fieldKeySelector)
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
keys, complete, err := handler.telemetryMetadataStore.GetKeys(ctx, valuer.MustNewUUID(claims.OrgID), fieldKeySelector)
if err != nil {
render.Error(rw, err)
return
@@ -68,7 +74,7 @@ func (handler *handler) GetFieldsValues(rw http.ResponseWriter, req *http.Reques
return
}
relatedValues, relatedComplete, err := handler.telemetryMetadataStore.GetRelatedValues(ctx, fieldValueSelector)
relatedValues, relatedComplete, err := handler.telemetryMetadataStore.GetRelatedValues(ctx, valuer.MustNewUUID(claims.OrgID), fieldValueSelector)
if err != nil {
// we don't want to return error if we fail to get related values for some reason
relatedValues = []string{}

View File

@@ -208,6 +208,7 @@ func (m *module) getContainersTableMetadata(ctx context.Context, orgID valuer.UU
// runs the query. The returned counts map is empty (never nil) when gated off.
func (m *module) getPerGroupContainerStatusCountsWithReqMetricChecks(
ctx context.Context,
orgID valuer.UUID,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
@@ -237,7 +238,7 @@ func (m *module) getPerGroupContainerStatusCountsWithReqMetricChecks(
return map[string]containerStatusCounts{}, warning, nil
}
counts, err := m.getPerGroupContainerStatusCounts(ctx, start, end, filter, groupBy, pageGroups)
counts, err := m.getPerGroupContainerStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups)
if err != nil {
return nil, nil, err
}
@@ -263,6 +264,7 @@ func (m *module) getPerGroupContainerStatusCountsWithReqMetricChecks(
// Groups absent from the result map have implicit zero counts (caller default).
func (m *module) getPerGroupContainerStatusCounts(
ctx context.Context,
orgID valuer.UUID,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
@@ -288,7 +290,7 @@ func (m *module) getPerGroupContainerStatusCounts(
err error
)
if mergedFilterExpr != "" {
filterClause, err = m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
filterClause, err = m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}
@@ -527,6 +529,7 @@ func (m *module) getPerGroupContainerStatusCounts(
// Groups absent from the result map have no data (caller default).
func (m *module) getPerGroupContainerRestartCounts(
ctx context.Context,
orgID valuer.UUID,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
@@ -550,7 +553,7 @@ func (m *module) getPerGroupContainerRestartCounts(
err error
)
if mergedFilterExpr != "" {
filterClause, err = m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
filterClause, err = m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}
@@ -670,6 +673,7 @@ func (m *module) getPerGroupContainerRestartCounts(
// Groups absent from the result map have no data (caller default).
func (m *module) getPerGroupContainerReadyCounts(
ctx context.Context,
orgID valuer.UUID,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
@@ -693,7 +697,7 @@ func (m *module) getPerGroupContainerReadyCounts(
err error
)
if mergedFilterExpr != "" {
filterClause, err = m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
filterClause, err = m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}

View File

@@ -398,7 +398,7 @@ func (m *module) buildReducedSamplesTblFingerprintSubQuery(metricNames []string,
return fpSB
}
func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter, startMillis, endMillis int64) (*sqlbuilder.WhereClause, error) {
func (m *module) buildFilterClause(ctx context.Context, orgID valuer.UUID, filter *qbtypes.Filter, startMillis, endMillis int64) (*sqlbuilder.WhereClause, error) {
expression := ""
if filter != nil {
expression = strings.TrimSpace(filter.Expression)
@@ -413,7 +413,7 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
whereClauseSelectors[idx].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
}
keys, _, err := m.telemetryMetadataStore.GetKeysMulti(ctx, whereClauseSelectors)
keys, _, err := m.telemetryMetadataStore.GetKeysMulti(ctx, orgID, whereClauseSelectors)
if err != nil {
return nil, err
}
@@ -646,7 +646,7 @@ func (m *module) getMetadata(
var filterClause *sqlbuilder.WhereClause
if filter != nil && strings.TrimSpace(filter.Expression) != "" {
var err error
filterClause, err = m.buildFilterClause(ctx, filter, startMs, endMs)
filterClause, err = m.buildFilterClause(ctx, orgID, filter, startMs, endMs)
if err != nil {
return nil, err
}
@@ -692,7 +692,7 @@ func (m *module) getMetadata(
)
if filter != nil && strings.TrimSpace(filter.Expression) != "" {
filterClause, err := m.buildFilterClause(ctx, filter, startMs, endMs)
filterClause, err := m.buildFilterClause(ctx, orgID, filter, startMs, endMs)
if err != nil {
return nil, err
}
@@ -860,7 +860,7 @@ func (m *module) getPerGroupDistinctCounts(
var filterClause *sqlbuilder.WhereClause
if mergedFilterExpr != "" {
var err error
filterClause, err = m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
filterClause, err = m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}
@@ -904,7 +904,7 @@ func (m *module) getPerGroupDistinctCounts(
fmt.Sprintf("fingerprint IN (%s)", sb.Var(fpSB)),
)
if mergedFilterExpr != "" {
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
filterClause, err := m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}

View File

@@ -64,7 +64,7 @@ func (m *module) getPerGroupHostStatusCounts(
var filterClause *sqlbuilder.WhereClause
if filterExpr != "" {
var err error
filterClause, err = m.buildFilterClause(ctx, &qbtypes.Filter{Expression: filterExpr}, req.Start, req.End)
filterClause, err = m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: filterExpr}, req.Start, req.End)
if err != nil {
return nil, err
}
@@ -110,7 +110,7 @@ func (m *module) getPerGroupHostStatusCounts(
)
if filterExpr != "" {
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: filterExpr}, req.Start, req.End)
filterClause, err := m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: filterExpr}, req.Start, req.End)
if err != nil {
return nil, err
}

View File

@@ -332,17 +332,17 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
})
g.Go(func() error {
var err error
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
restartCounts, err = m.getPerGroupPodRestartCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
restartCounts, err = m.getPerGroupPodRestartCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
@@ -436,17 +436,17 @@ func (m *module) ListContainers(ctx context.Context, orgID valuer.UUID, req *inf
})
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupContainerStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
statusCounts, statusWarning, err = m.getPerGroupContainerStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
restartCounts, err = m.getPerGroupContainerRestartCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
restartCounts, err = m.getPerGroupContainerRestartCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
readyCounts, err = m.getPerGroupContainerReadyCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
readyCounts, err = m.getPerGroupContainerReadyCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
@@ -540,17 +540,17 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
})
g.Go(func() error {
var err error
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podPhaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
podPhaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
@@ -644,12 +644,12 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
})
g.Go(func() error {
var err error
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
@@ -750,17 +750,17 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
})
g.Go(func() error {
var err error
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podPhaseCountsMap, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
podPhaseCountsMap, err = m.getPerGroupPodPhaseCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
@@ -941,12 +941,12 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
})
g.Go(func() error {
var err error
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
@@ -1046,12 +1046,12 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
})
g.Go(func() error {
var err error
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
@@ -1151,12 +1151,12 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
})
g.Go(func() error {
var err error
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
@@ -1256,12 +1256,12 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
})
g.Go(func() error {
var err error
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
phaseCounts, err = m.getPerGroupPodPhaseCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})

View File

@@ -177,6 +177,7 @@ func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, r
// Groups absent from the result map have implicit zero counts (caller default).
func (m *module) getPerGroupNodeConditionCounts(
ctx context.Context,
orgID valuer.UUID,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
@@ -217,7 +218,7 @@ func (m *module) getPerGroupNodeConditionCounts(
timeSeriesFPs.LE("unix_milli", flooredEndMs),
)
if mergedFilterExpr != "" {
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
filterClause, err := m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}

View File

@@ -248,6 +248,7 @@ func (m *module) getPodsTableMetadata(ctx context.Context, orgID valuer.UUID, re
// Groups absent from the result map have implicit zero counts (caller default).
func (m *module) getPerGroupPodPhaseCounts(
ctx context.Context,
orgID valuer.UUID,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
@@ -288,7 +289,7 @@ func (m *module) getPerGroupPodPhaseCounts(
timeSeriesFPs.LE("unix_milli", flooredEndMs),
)
if mergedFilterExpr != "" {
filterClause, err := m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
filterClause, err := m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}
@@ -397,6 +398,7 @@ func (m *module) getPerGroupPodPhaseCounts(
// runs the query. The returned counts map is empty (never nil) when gated off.
func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
ctx context.Context,
orgID valuer.UUID,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
@@ -426,7 +428,7 @@ func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
return map[string]podStatusCounts{}, warning, nil
}
counts, err := m.getPerGroupPodStatusCounts(ctx, start, end, filter, groupBy, pageGroups)
counts, err := m.getPerGroupPodStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups)
if err != nil {
return nil, nil, err
}
@@ -448,6 +450,7 @@ func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
// Groups absent from the result map have implicit zero counts (caller default).
func (m *module) getPerGroupPodStatusCounts(
ctx context.Context,
orgID valuer.UUID,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
@@ -476,7 +479,7 @@ func (m *module) getPerGroupPodStatusCounts(
err error
)
if mergedFilterExpr != "" {
filterClause, err = m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
filterClause, err = m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}
@@ -774,6 +777,7 @@ func (m *module) getPerGroupPodStatusCounts(
// Groups absent from the result map have no data (caller default).
func (m *module) getPerGroupPodRestartCounts(
ctx context.Context,
orgID valuer.UUID,
start, end int64,
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
@@ -798,7 +802,7 @@ func (m *module) getPerGroupPodRestartCounts(
err error
)
if mergedFilterExpr != "" {
filterClause, err = m.buildFilterClause(ctx, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
filterClause, err = m.buildFilterClause(ctx, orgID, &qbtypes.Filter{Expression: mergedFilterExpr}, start, end)
if err != nil {
return nil, err
}

View File

@@ -565,7 +565,7 @@ func (m *module) InspectMetrics(
start := uint64(req.Start)
end := uint64(req.End)
filterWhereClause, err := m.buildFilterClause(ctx, req.Filter, req.Start, req.End)
filterWhereClause, err := m.buildFilterClause(ctx, orgID, req.Filter, req.Start, req.End)
if err != nil {
return nil, err
}
@@ -950,7 +950,7 @@ func (m *module) insertMetricsMetadata(ctx context.Context, orgID valuer.UUID, r
return nil
}
func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter, startMillis, endMillis int64) (*sqlbuilder.WhereClause, error) {
func (m *module) buildFilterClause(ctx context.Context, orgID valuer.UUID, filter *qbtypes.Filter, startMillis, endMillis int64) (*sqlbuilder.WhereClause, error) {
expression := ""
if filter != nil {
expression = strings.TrimSpace(filter.Expression)
@@ -969,7 +969,7 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
// whereClauseSelectors[idx].Source = query.Source
}
keys, _, err := m.telemetryMetadataStore.GetKeysMulti(ctx, whereClauseSelectors)
keys, _, err := m.telemetryMetadataStore.GetKeysMulti(ctx, orgID, whereClauseSelectors)
if err != nil {
return nil, err
}
@@ -1010,7 +1010,7 @@ func (m *module) fetchMetricsStatsWithSamples(
var filterWhereClause *sqlbuilder.WhereClause
if hasFilter {
var err error
filterWhereClause, err = m.buildFilterClause(ctx, req.Filter, req.Start, req.End)
filterWhereClause, err = m.buildFilterClause(ctx, orgID, req.Filter, req.Start, req.End)
if err != nil {
return nil, 0, err
}
@@ -1214,7 +1214,7 @@ func (m *module) computeTimeseriesTreemap(ctx context.Context, orgID valuer.UUID
var filterWhereClause *sqlbuilder.WhereClause
if hasFilter {
var err error
filterWhereClause, err = m.buildFilterClause(ctx, req.Filter, req.Start, req.End)
filterWhereClause, err = m.buildFilterClause(ctx, orgID, req.Filter, req.Start, req.End)
if err != nil {
return nil, err
}
@@ -1332,7 +1332,7 @@ func (m *module) computeSamplesTreemap(ctx context.Context, orgID valuer.UUID, r
var filterWhereClause *sqlbuilder.WhereClause
if hasFilter {
var err error
filterWhereClause, err = m.buildFilterClause(ctx, req.Filter, req.Start, req.End)
filterWhereClause, err = m.buildFilterClause(ctx, orgID, req.Filter, req.Start, req.End)
if err != nil {
return nil, err
}

View File

@@ -33,7 +33,7 @@ func (c *conditionBuilder) ConditionFor(
sb *sqlbuilder.SelectBuilder,
) ([]string, []string, error) {
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for rule state history.
// has/hasAny/hasAll/hasToken are logs-body-only; reject for rule state history.
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
return nil, nil, err
}
@@ -49,7 +49,7 @@ func (c *conditionBuilder) ConditionFor(
conds := make([]string, 0, len(keys))
for _, k := range keys {
cond, err := c.conditionForKey(ctx, startNs, endNs, k, operator, value, sb)
cond, err := c.conditionForKey(ctx, orgID, startNs, endNs, k, operator, value, sb)
if err != nil {
return nil, nil, err
}
@@ -60,6 +60,7 @@ func (c *conditionBuilder) ConditionFor(
func (c *conditionBuilder) conditionForKey(
ctx context.Context,
orgID valuer.UUID,
startNs uint64,
endNs uint64,
key *telemetrytypes.TelemetryFieldKey,
@@ -71,7 +72,7 @@ func (c *conditionBuilder) conditionForKey(
value = querybuilder.FormatValueForContains(value)
}
fieldName, err := c.fm.FieldFor(ctx, valuer.UUID{}, startNs, endNs, key)
fieldName, err := c.fm.FieldFor(ctx, orgID, startNs, endNs, key)
if err != nil {
return "", err
}

View File

@@ -10,9 +10,11 @@ import (
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/types/authtypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -85,7 +87,13 @@ func (h *handler) GetRuleHistoryTimeline(w http.ResponseWriter, r *http.Request)
req.Query.Limit = 50
}
timelineItems, timelineTotal, err := h.module.GetHistoryTimeline(r.Context(), ruleID, req.Query)
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
timelineItems, timelineTotal, err := h.module.GetHistoryTimeline(r.Context(), valuer.MustNewUUID(claims.OrgID), ruleID, req.Query)
if err != nil {
render.Error(w, err)
return
@@ -127,7 +135,13 @@ func (h *handler) GetRuleHistoryContributors(w http.ResponseWriter, r *http.Requ
return
}
res, err := h.module.GetHistoryContributors(r.Context(), ruleID, query)
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
res, err := h.module.GetHistoryContributors(r.Context(), valuer.MustNewUUID(claims.OrgID), ruleID, query)
if err != nil {
render.Error(w, err)
return
@@ -152,7 +166,13 @@ func (h *handler) GetRuleHistoryFilterKeys(w http.ResponseWriter, r *http.Reques
return
}
res, err := h.module.GetHistoryFilterKeys(r.Context(), ruleID, query, search, limit)
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
res, err := h.module.GetHistoryFilterKeys(r.Context(), valuer.MustNewUUID(claims.OrgID), ruleID, query, search, limit)
if err != nil {
render.Error(w, err)
return
@@ -167,7 +187,13 @@ func (h *handler) GetRuleHistoryFilterValues(w http.ResponseWriter, r *http.Requ
return
}
res, err := h.module.GetHistoryFilterValues(r.Context(), ruleID, key, query, search, limit)
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
res, err := h.module.GetHistoryFilterValues(r.Context(), valuer.MustNewUUID(claims.OrgID), ruleID, key, query, search, limit)
if err != nil {
render.Error(w, err)
return

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct {
@@ -23,20 +24,20 @@ func (m *module) GetLastSavedRuleStateHistory(ctx context.Context, ruleID string
return m.store.GetLastSavedRuleStateHistory(ctx, ruleID)
}
func (m *module) GetHistoryTimeline(ctx context.Context, ruleID string, query rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error) {
return m.store.ReadRuleStateHistoryByRuleID(ctx, ruleID, &query)
func (m *module) GetHistoryTimeline(ctx context.Context, orgID valuer.UUID, ruleID string, query rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error) {
return m.store.ReadRuleStateHistoryByRuleID(ctx, orgID, ruleID, &query)
}
func (m *module) GetHistoryFilterKeys(ctx context.Context, ruleID string, query rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldKeys, error) {
return m.store.ReadRuleStateHistoryFilterKeysByRuleID(ctx, ruleID, &query, search, limit)
func (m *module) GetHistoryFilterKeys(ctx context.Context, orgID valuer.UUID, ruleID string, query rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldKeys, error) {
return m.store.ReadRuleStateHistoryFilterKeysByRuleID(ctx, orgID, ruleID, &query, search, limit)
}
func (m *module) GetHistoryFilterValues(ctx context.Context, ruleID string, key string, query rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldValues, error) {
return m.store.ReadRuleStateHistoryFilterValuesByRuleID(ctx, ruleID, key, &query, search, limit)
func (m *module) GetHistoryFilterValues(ctx context.Context, orgID valuer.UUID, ruleID string, key string, query rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldValues, error) {
return m.store.ReadRuleStateHistoryFilterValuesByRuleID(ctx, orgID, ruleID, key, &query, search, limit)
}
func (m *module) GetHistoryContributors(ctx context.Context, ruleID string, query rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error) {
return m.store.ReadRuleStateHistoryTopContributorsByRuleID(ctx, ruleID, &query)
func (m *module) GetHistoryContributors(ctx context.Context, orgID valuer.UUID, ruleID string, query rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error) {
return m.store.ReadRuleStateHistoryTopContributorsByRuleID(ctx, orgID, ruleID, &query)
}
func (m *module) GetHistoryOverallStatus(ctx context.Context, ruleID string, query rulestatehistorytypes.Query) ([]rulestatehistorytypes.GettableRuleStateWindow, error) {

View File

@@ -13,6 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
sqlbuilder "github.com/huandu/go-sqlbuilder"
)
@@ -102,7 +103,7 @@ func (s *store) GetLastSavedRuleStateHistory(ctx context.Context, ruleID string)
return history, nil
}
func (s *store) ReadRuleStateHistoryByRuleID(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error) {
func (s *store) ReadRuleStateHistoryByRuleID(ctx context.Context, orgID valuer.UUID, ruleID string, query *rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"rule_id",
@@ -119,7 +120,7 @@ func (s *store) ReadRuleStateHistoryByRuleID(ctx context.Context, ruleID string,
sb.From(historyTable())
s.applyBaseHistoryFilters(sb, ruleID, query)
whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End)
whereClause, err := s.buildFilterClause(ctx, orgID, query.FilterExpression, query.Start, query.End)
if err != nil {
return nil, 0, err
}
@@ -155,7 +156,7 @@ func (s *store) ReadRuleStateHistoryByRuleID(ctx context.Context, ruleID string,
return history, total, nil
}
func (s *store) ReadRuleStateHistoryFilterKeysByRuleID(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldKeys, error) {
func (s *store) ReadRuleStateHistoryFilterKeysByRuleID(ctx context.Context, orgID valuer.UUID, ruleID string, query *rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldKeys, error) {
if limit <= 0 {
limit = 50
}
@@ -172,7 +173,7 @@ func (s *store) ReadRuleStateHistoryFilterKeysByRuleID(ctx context.Context, rule
sb.Where(fmt.Sprintf("positionCaseInsensitiveUTF8(%s, %s) > 0", keyExpr, sb.Var(search)))
}
whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End)
whereClause, err := s.buildFilterClause(ctx, orgID, query.FilterExpression, query.Start, query.End)
if err != nil {
return nil, err
}
@@ -226,7 +227,7 @@ func (s *store) ReadRuleStateHistoryFilterKeysByRuleID(ctx context.Context, rule
}, nil
}
func (s *store) ReadRuleStateHistoryFilterValuesByRuleID(ctx context.Context, ruleID string, key string, query *rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldValues, error) {
func (s *store) ReadRuleStateHistoryFilterValuesByRuleID(ctx context.Context, orgID valuer.UUID, ruleID string, key string, query *rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldValues, error) {
if limit <= 0 {
limit = 50
}
@@ -244,7 +245,7 @@ func (s *store) ReadRuleStateHistoryFilterValuesByRuleID(ctx context.Context, ru
sb.Where(fmt.Sprintf("positionCaseInsensitiveUTF8(%s, %s) > 0", valExpr, sb.Var(search)))
}
whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End)
whereClause, err := s.buildFilterClause(ctx, orgID, query.FilterExpression, query.Start, query.End)
if err != nil {
return nil, err
}
@@ -291,7 +292,7 @@ func (s *store) ReadRuleStateHistoryFilterValuesByRuleID(ctx context.Context, ru
}, nil
}
func (s *store) ReadRuleStateHistoryTopContributorsByRuleID(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error) {
func (s *store) ReadRuleStateHistoryTopContributorsByRuleID(ctx context.Context, orgID valuer.UUID, ruleID string, query *rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
"fingerprint",
@@ -305,7 +306,7 @@ func (s *store) ReadRuleStateHistoryTopContributorsByRuleID(ctx context.Context,
sb.Where(sb.GE("unix_milli", query.Start))
sb.Where(sb.LT("unix_milli", query.End))
whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End)
whereClause, err := s.buildFilterClause(ctx, orgID, query.FilterExpression, query.Start, query.End)
if err != nil {
return nil, err
}
@@ -472,7 +473,7 @@ func (s *store) querySeries(ctx context.Context, selectQuery string, args ...any
return series, nil
}
func (s *store) buildFilterClause(ctx context.Context, filter qbtypes.Filter, startMillis, endMillis int64) (*sqlbuilder.WhereClause, error) {
func (s *store) buildFilterClause(ctx context.Context, orgID valuer.UUID, filter qbtypes.Filter, startMillis, endMillis int64) (*sqlbuilder.WhereClause, error) {
expression := strings.TrimSpace(filter.Expression)
if expression == "" {
return nil, nil //nolint:nilnil
@@ -483,7 +484,7 @@ func (s *store) buildFilterClause(ctx context.Context, filter qbtypes.Filter, st
selectors[i].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
}
fieldKeys, _, err := s.telemetryMetadataStore.GetKeysMulti(ctx, selectors)
fieldKeys, _, err := s.telemetryMetadataStore.GetKeysMulti(ctx, orgID, selectors)
if err != nil || len(fieldKeys) == 0 {
fieldKeys = map[string][]*telemetrytypes.TelemetryFieldKey{}
for _, sel := range selectors {

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// Module defines the core operations for managing rule state history.
@@ -23,16 +24,16 @@ type Module interface {
// GetHistoryTimeline returns a time-ordered list of rule state history entries and a total count
// for the given query, suitable for paginated timeline views.
GetHistoryTimeline(context.Context, string, rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error)
GetHistoryTimeline(context.Context, valuer.UUID, string, rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error)
// GetHistoryFilterKeys returns the available filter keys for rule state history queries.
GetHistoryFilterKeys(context.Context, string, rulestatehistorytypes.Query, string, int64) (*telemetrytypes.GettableFieldKeys, error)
GetHistoryFilterKeys(context.Context, valuer.UUID, string, rulestatehistorytypes.Query, string, int64) (*telemetrytypes.GettableFieldKeys, error)
// GetHistoryFilterValues returns the available values for a specific filter key in rule state history.
GetHistoryFilterValues(context.Context, string, string, rulestatehistorytypes.Query, string, int64) (*telemetrytypes.GettableFieldValues, error)
GetHistoryFilterValues(context.Context, valuer.UUID, string, string, rulestatehistorytypes.Query, string, int64) (*telemetrytypes.GettableFieldValues, error)
// GetHistoryContributors returns the top contributors to trigger alert, for the given query.
GetHistoryContributors(context.Context, string, rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error)
GetHistoryContributors(context.Context, valuer.UUID, string, rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error)
// GetHistoryOverallStatus returns the overall status windows for rule state history,
// providing an aggregated view of rule health over time.

File diff suppressed because one or more lines are too long

View File

@@ -24,13 +24,12 @@ HASTOKEN=23
HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
BOOL=27
NUMBER=28
QUOTED_TEXT=29
KEY=30
WS=31
FREETEXT=32
'('=1
')'=2
'['=3

File diff suppressed because one or more lines are too long

View File

@@ -24,13 +24,12 @@ HASTOKEN=23
HAS=24
HASANY=25
HASALL=26
SEARCH=27
BOOL=28
NUMBER=29
QUOTED_TEXT=30
KEY=31
WS=32
FREETEXT=33
BOOL=27
NUMBER=28
QUOTED_TEXT=29
KEY=30
WS=31
FREETEXT=32
'('=1
')'=2
'['=3

View File

@@ -1,4 +1,4 @@
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser // FilterQuery
@@ -93,12 +93,6 @@ func (s *BaseFilterQueryListener) EnterFunctionCall(ctx *FunctionCallContext) {}
// ExitFunctionCall is called when production functionCall is exited.
func (s *BaseFilterQueryListener) ExitFunctionCall(ctx *FunctionCallContext) {}
// EnterSearchCall is called when production searchCall is entered.
func (s *BaseFilterQueryListener) EnterSearchCall(ctx *SearchCallContext) {}
// ExitSearchCall is called when production searchCall is exited.
func (s *BaseFilterQueryListener) ExitSearchCall(ctx *SearchCallContext) {}
// EnterFunctionParamList is called when production functionParamList is entered.
func (s *BaseFilterQueryListener) EnterFunctionParamList(ctx *FunctionParamListContext) {}

View File

@@ -1,4 +1,4 @@
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser // FilterQuery
@@ -56,10 +56,6 @@ func (v *BaseFilterQueryVisitor) VisitFunctionCall(ctx *FunctionCallContext) int
return v.VisitChildren(ctx)
}
func (v *BaseFilterQueryVisitor) VisitSearchCall(ctx *SearchCallContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseFilterQueryVisitor) VisitFunctionParamList(ctx *FunctionParamListContext) interface{} {
return v.VisitChildren(ctx)
}

View File

@@ -1,12 +1,13 @@
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser
import (
"fmt"
"github.com/antlr4-go/antlr/v4"
"sync"
"unicode"
"github.com/antlr4-go/antlr/v4"
)
// Suppress unused import error
@@ -50,174 +51,170 @@ func filterquerylexerLexerInit() {
"", "LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
"HASALL", "SEARCH", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
"HASALL", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
}
staticData.RuleNames = []string{
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "ILIKE", "BETWEEN", "EXISTS",
"REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR", "HASTOKEN", "HAS", "HASANY",
"HASALL", "SEARCH", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT",
"EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
"HASALL", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS",
"OLD_JSON_BRACKS", "KEY", "WS", "DIGIT", "FREETEXT",
}
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
staticData.serializedATN = []int32{
4, 0, 33, 329, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 0, 32, 320, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2,
10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15,
7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7,
20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25,
2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2,
31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36,
7, 36, 2, 37, 7, 37, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1,
4, 1, 4, 1, 5, 1, 5, 1, 5, 3, 5, 91, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7,
1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11,
1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1,
13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15,
1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 134, 8, 15, 1, 16, 1, 16, 1, 16, 1,
16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17,
1, 17, 3, 17, 151, 8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1,
19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22,
1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1,
24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25,
1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1,
27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 210,
8, 27, 1, 28, 1, 28, 1, 29, 3, 29, 215, 8, 29, 1, 29, 4, 29, 218, 8, 29,
11, 29, 12, 29, 219, 1, 29, 1, 29, 5, 29, 224, 8, 29, 10, 29, 12, 29, 227,
9, 29, 3, 29, 229, 8, 29, 1, 29, 1, 29, 3, 29, 233, 8, 29, 1, 29, 4, 29,
236, 8, 29, 11, 29, 12, 29, 237, 3, 29, 240, 8, 29, 1, 29, 3, 29, 243,
8, 29, 1, 29, 1, 29, 4, 29, 247, 8, 29, 11, 29, 12, 29, 248, 1, 29, 1,
29, 3, 29, 253, 8, 29, 1, 29, 4, 29, 256, 8, 29, 11, 29, 12, 29, 257, 3,
29, 260, 8, 29, 3, 29, 262, 8, 29, 1, 30, 1, 30, 1, 30, 1, 30, 5, 30, 268,
8, 30, 10, 30, 12, 30, 271, 9, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 5,
30, 278, 8, 30, 10, 30, 12, 30, 281, 9, 30, 1, 30, 3, 30, 284, 8, 30, 1,
31, 1, 31, 5, 31, 288, 8, 31, 10, 31, 12, 31, 291, 9, 31, 1, 32, 1, 32,
1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1,
34, 1, 34, 4, 34, 307, 8, 34, 11, 34, 12, 34, 308, 5, 34, 311, 8, 34, 10,
34, 12, 34, 314, 9, 34, 1, 35, 4, 35, 317, 8, 35, 11, 35, 12, 35, 318,
1, 35, 1, 35, 1, 36, 1, 36, 1, 37, 4, 37, 326, 8, 37, 11, 37, 12, 37, 327,
0, 0, 38, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19,
7, 36, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5,
1, 5, 1, 5, 3, 5, 89, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1,
8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1,
12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14,
1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1,
15, 1, 15, 3, 15, 132, 8, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16,
1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17, 149,
8, 17, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1,
20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22,
1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1,
24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25,
1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 3, 26, 201,
8, 26, 1, 27, 1, 27, 1, 28, 3, 28, 206, 8, 28, 1, 28, 4, 28, 209, 8, 28,
11, 28, 12, 28, 210, 1, 28, 1, 28, 5, 28, 215, 8, 28, 10, 28, 12, 28, 218,
9, 28, 3, 28, 220, 8, 28, 1, 28, 1, 28, 3, 28, 224, 8, 28, 1, 28, 4, 28,
227, 8, 28, 11, 28, 12, 28, 228, 3, 28, 231, 8, 28, 1, 28, 3, 28, 234,
8, 28, 1, 28, 1, 28, 4, 28, 238, 8, 28, 11, 28, 12, 28, 239, 1, 28, 1,
28, 3, 28, 244, 8, 28, 1, 28, 4, 28, 247, 8, 28, 11, 28, 12, 28, 248, 3,
28, 251, 8, 28, 3, 28, 253, 8, 28, 1, 29, 1, 29, 1, 29, 1, 29, 5, 29, 259,
8, 29, 10, 29, 12, 29, 262, 9, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 5,
29, 269, 8, 29, 10, 29, 12, 29, 272, 9, 29, 1, 29, 3, 29, 275, 8, 29, 1,
30, 1, 30, 5, 30, 279, 8, 30, 10, 30, 12, 30, 282, 9, 30, 1, 31, 1, 31,
1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1,
33, 1, 33, 4, 33, 298, 8, 33, 11, 33, 12, 33, 299, 5, 33, 302, 8, 33, 10,
33, 12, 33, 305, 9, 33, 1, 34, 4, 34, 308, 8, 34, 11, 34, 12, 34, 309,
1, 34, 1, 34, 1, 35, 1, 35, 1, 36, 4, 36, 317, 8, 36, 11, 36, 12, 36, 318,
0, 0, 37, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19,
10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37,
19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55,
28, 57, 0, 59, 29, 61, 30, 63, 0, 65, 0, 67, 0, 69, 31, 71, 32, 73, 0,
75, 33, 1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0,
75, 75, 107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84,
84, 116, 116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88,
88, 120, 120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71,
71, 103, 103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79,
111, 111, 2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104,
104, 2, 0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102,
102, 2, 0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92,
4, 0, 35, 36, 64, 90, 95, 95, 97, 123, 7, 0, 35, 36, 45, 45, 47, 58, 64,
90, 95, 95, 97, 123, 125, 125, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57,
8, 0, 9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 353,
0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0,
0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0,
0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0,
0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1,
0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39,
1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0,
47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0,
0, 55, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 69, 1, 0, 0,
0, 0, 71, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 1, 77, 1, 0, 0, 0, 3, 79, 1, 0,
0, 0, 5, 81, 1, 0, 0, 0, 7, 83, 1, 0, 0, 0, 9, 85, 1, 0, 0, 0, 11, 90,
1, 0, 0, 0, 13, 92, 1, 0, 0, 0, 15, 95, 1, 0, 0, 0, 17, 98, 1, 0, 0, 0,
19, 100, 1, 0, 0, 0, 21, 103, 1, 0, 0, 0, 23, 105, 1, 0, 0, 0, 25, 108,
1, 0, 0, 0, 27, 113, 1, 0, 0, 0, 29, 119, 1, 0, 0, 0, 31, 127, 1, 0, 0,
0, 33, 135, 1, 0, 0, 0, 35, 142, 1, 0, 0, 0, 37, 152, 1, 0, 0, 0, 39, 155,
1, 0, 0, 0, 41, 159, 1, 0, 0, 0, 43, 163, 1, 0, 0, 0, 45, 166, 1, 0, 0,
0, 47, 175, 1, 0, 0, 0, 49, 179, 1, 0, 0, 0, 51, 186, 1, 0, 0, 0, 53, 193,
1, 0, 0, 0, 55, 209, 1, 0, 0, 0, 57, 211, 1, 0, 0, 0, 59, 261, 1, 0, 0,
0, 61, 283, 1, 0, 0, 0, 63, 285, 1, 0, 0, 0, 65, 292, 1, 0, 0, 0, 67, 295,
1, 0, 0, 0, 69, 299, 1, 0, 0, 0, 71, 316, 1, 0, 0, 0, 73, 322, 1, 0, 0,
0, 75, 325, 1, 0, 0, 0, 77, 78, 5, 40, 0, 0, 78, 2, 1, 0, 0, 0, 79, 80,
5, 41, 0, 0, 80, 4, 1, 0, 0, 0, 81, 82, 5, 91, 0, 0, 82, 6, 1, 0, 0, 0,
83, 84, 5, 93, 0, 0, 84, 8, 1, 0, 0, 0, 85, 86, 5, 44, 0, 0, 86, 10, 1,
0, 0, 0, 87, 91, 5, 61, 0, 0, 88, 89, 5, 61, 0, 0, 89, 91, 5, 61, 0, 0,
90, 87, 1, 0, 0, 0, 90, 88, 1, 0, 0, 0, 91, 12, 1, 0, 0, 0, 92, 93, 5,
33, 0, 0, 93, 94, 5, 61, 0, 0, 94, 14, 1, 0, 0, 0, 95, 96, 5, 60, 0, 0,
96, 97, 5, 62, 0, 0, 97, 16, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 18, 1,
0, 0, 0, 100, 101, 5, 60, 0, 0, 101, 102, 5, 61, 0, 0, 102, 20, 1, 0, 0,
0, 103, 104, 5, 62, 0, 0, 104, 22, 1, 0, 0, 0, 105, 106, 5, 62, 0, 0, 106,
107, 5, 61, 0, 0, 107, 24, 1, 0, 0, 0, 108, 109, 7, 0, 0, 0, 109, 110,
7, 1, 0, 0, 110, 111, 7, 2, 0, 0, 111, 112, 7, 3, 0, 0, 112, 26, 1, 0,
0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 0, 0, 0, 115, 116, 7, 1, 0, 0,
116, 117, 7, 2, 0, 0, 117, 118, 7, 3, 0, 0, 118, 28, 1, 0, 0, 0, 119, 120,
7, 4, 0, 0, 120, 121, 7, 3, 0, 0, 121, 122, 7, 5, 0, 0, 122, 123, 7, 6,
0, 0, 123, 124, 7, 3, 0, 0, 124, 125, 7, 3, 0, 0, 125, 126, 7, 7, 0, 0,
126, 30, 1, 0, 0, 0, 127, 128, 7, 3, 0, 0, 128, 129, 7, 8, 0, 0, 129, 130,
7, 1, 0, 0, 130, 131, 7, 9, 0, 0, 131, 133, 7, 5, 0, 0, 132, 134, 7, 9,
0, 0, 133, 132, 1, 0, 0, 0, 133, 134, 1, 0, 0, 0, 134, 32, 1, 0, 0, 0,
135, 136, 7, 10, 0, 0, 136, 137, 7, 3, 0, 0, 137, 138, 7, 11, 0, 0, 138,
139, 7, 3, 0, 0, 139, 140, 7, 8, 0, 0, 140, 141, 7, 12, 0, 0, 141, 34,
1, 0, 0, 0, 142, 143, 7, 13, 0, 0, 143, 144, 7, 14, 0, 0, 144, 145, 7,
7, 0, 0, 145, 146, 7, 5, 0, 0, 146, 147, 7, 15, 0, 0, 147, 148, 7, 1, 0,
0, 148, 150, 7, 7, 0, 0, 149, 151, 7, 9, 0, 0, 150, 149, 1, 0, 0, 0, 150,
151, 1, 0, 0, 0, 151, 36, 1, 0, 0, 0, 152, 153, 7, 1, 0, 0, 153, 154, 7,
7, 0, 0, 154, 38, 1, 0, 0, 0, 155, 156, 7, 7, 0, 0, 156, 157, 7, 14, 0,
0, 157, 158, 7, 5, 0, 0, 158, 40, 1, 0, 0, 0, 159, 160, 7, 15, 0, 0, 160,
161, 7, 7, 0, 0, 161, 162, 7, 16, 0, 0, 162, 42, 1, 0, 0, 0, 163, 164,
7, 14, 0, 0, 164, 165, 7, 10, 0, 0, 165, 44, 1, 0, 0, 0, 166, 167, 7, 17,
0, 0, 167, 168, 7, 15, 0, 0, 168, 169, 7, 9, 0, 0, 169, 170, 7, 5, 0, 0,
170, 171, 7, 14, 0, 0, 171, 172, 7, 2, 0, 0, 172, 173, 7, 3, 0, 0, 173,
174, 7, 7, 0, 0, 174, 46, 1, 0, 0, 0, 175, 176, 7, 17, 0, 0, 176, 177,
7, 15, 0, 0, 177, 178, 7, 9, 0, 0, 178, 48, 1, 0, 0, 0, 179, 180, 7, 17,
0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 9, 0, 0, 182, 183, 7, 15, 0,
0, 183, 184, 7, 7, 0, 0, 184, 185, 7, 18, 0, 0, 185, 50, 1, 0, 0, 0, 186,
187, 7, 17, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189, 7, 9, 0, 0, 189, 190,
7, 15, 0, 0, 190, 191, 7, 0, 0, 0, 191, 192, 7, 0, 0, 0, 192, 52, 1, 0,
0, 0, 193, 194, 7, 9, 0, 0, 194, 195, 7, 3, 0, 0, 195, 196, 7, 15, 0, 0,
196, 197, 7, 10, 0, 0, 197, 198, 7, 13, 0, 0, 198, 199, 7, 17, 0, 0, 199,
54, 1, 0, 0, 0, 200, 201, 7, 5, 0, 0, 201, 202, 7, 10, 0, 0, 202, 203,
7, 19, 0, 0, 203, 210, 7, 3, 0, 0, 204, 205, 7, 20, 0, 0, 205, 206, 7,
15, 0, 0, 206, 207, 7, 0, 0, 0, 207, 208, 7, 9, 0, 0, 208, 210, 7, 3, 0,
0, 209, 200, 1, 0, 0, 0, 209, 204, 1, 0, 0, 0, 210, 56, 1, 0, 0, 0, 211,
212, 7, 21, 0, 0, 212, 58, 1, 0, 0, 0, 213, 215, 3, 57, 28, 0, 214, 213,
1, 0, 0, 0, 214, 215, 1, 0, 0, 0, 215, 217, 1, 0, 0, 0, 216, 218, 3, 73,
36, 0, 217, 216, 1, 0, 0, 0, 218, 219, 1, 0, 0, 0, 219, 217, 1, 0, 0, 0,
219, 220, 1, 0, 0, 0, 220, 228, 1, 0, 0, 0, 221, 225, 5, 46, 0, 0, 222,
224, 3, 73, 36, 0, 223, 222, 1, 0, 0, 0, 224, 227, 1, 0, 0, 0, 225, 223,
1, 0, 0, 0, 225, 226, 1, 0, 0, 0, 226, 229, 1, 0, 0, 0, 227, 225, 1, 0,
0, 0, 228, 221, 1, 0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 239, 1, 0, 0, 0,
230, 232, 7, 3, 0, 0, 231, 233, 3, 57, 28, 0, 232, 231, 1, 0, 0, 0, 232,
233, 1, 0, 0, 0, 233, 235, 1, 0, 0, 0, 234, 236, 3, 73, 36, 0, 235, 234,
1, 0, 0, 0, 236, 237, 1, 0, 0, 0, 237, 235, 1, 0, 0, 0, 237, 238, 1, 0,
0, 0, 238, 240, 1, 0, 0, 0, 239, 230, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0,
240, 262, 1, 0, 0, 0, 241, 243, 3, 57, 28, 0, 242, 241, 1, 0, 0, 0, 242,
243, 1, 0, 0, 0, 243, 244, 1, 0, 0, 0, 244, 246, 5, 46, 0, 0, 245, 247,
3, 73, 36, 0, 246, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 246, 1,
0, 0, 0, 248, 249, 1, 0, 0, 0, 249, 259, 1, 0, 0, 0, 250, 252, 7, 3, 0,
0, 251, 253, 3, 57, 28, 0, 252, 251, 1, 0, 0, 0, 252, 253, 1, 0, 0, 0,
253, 255, 1, 0, 0, 0, 254, 256, 3, 73, 36, 0, 255, 254, 1, 0, 0, 0, 256,
257, 1, 0, 0, 0, 257, 255, 1, 0, 0, 0, 257, 258, 1, 0, 0, 0, 258, 260,
1, 0, 0, 0, 259, 250, 1, 0, 0, 0, 259, 260, 1, 0, 0, 0, 260, 262, 1, 0,
0, 0, 261, 214, 1, 0, 0, 0, 261, 242, 1, 0, 0, 0, 262, 60, 1, 0, 0, 0,
263, 269, 5, 34, 0, 0, 264, 268, 8, 22, 0, 0, 265, 266, 5, 92, 0, 0, 266,
268, 9, 0, 0, 0, 267, 264, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 271,
1, 0, 0, 0, 269, 267, 1, 0, 0, 0, 269, 270, 1, 0, 0, 0, 270, 272, 1, 0,
0, 0, 271, 269, 1, 0, 0, 0, 272, 284, 5, 34, 0, 0, 273, 279, 5, 39, 0,
0, 274, 278, 8, 23, 0, 0, 275, 276, 5, 92, 0, 0, 276, 278, 9, 0, 0, 0,
277, 274, 1, 0, 0, 0, 277, 275, 1, 0, 0, 0, 278, 281, 1, 0, 0, 0, 279,
277, 1, 0, 0, 0, 279, 280, 1, 0, 0, 0, 280, 282, 1, 0, 0, 0, 281, 279,
1, 0, 0, 0, 282, 284, 5, 39, 0, 0, 283, 263, 1, 0, 0, 0, 283, 273, 1, 0,
0, 0, 284, 62, 1, 0, 0, 0, 285, 289, 7, 24, 0, 0, 286, 288, 7, 25, 0, 0,
287, 286, 1, 0, 0, 0, 288, 291, 1, 0, 0, 0, 289, 287, 1, 0, 0, 0, 289,
290, 1, 0, 0, 0, 290, 64, 1, 0, 0, 0, 291, 289, 1, 0, 0, 0, 292, 293, 5,
91, 0, 0, 293, 294, 5, 93, 0, 0, 294, 66, 1, 0, 0, 0, 295, 296, 5, 91,
0, 0, 296, 297, 5, 42, 0, 0, 297, 298, 5, 93, 0, 0, 298, 68, 1, 0, 0, 0,
299, 312, 3, 63, 31, 0, 300, 301, 5, 46, 0, 0, 301, 311, 3, 63, 31, 0,
302, 311, 3, 65, 32, 0, 303, 311, 3, 67, 33, 0, 304, 306, 5, 46, 0, 0,
305, 307, 3, 73, 36, 0, 306, 305, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308,
306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 311, 1, 0, 0, 0, 310, 300,
1, 0, 0, 0, 310, 302, 1, 0, 0, 0, 310, 303, 1, 0, 0, 0, 310, 304, 1, 0,
0, 0, 311, 314, 1, 0, 0, 0, 312, 310, 1, 0, 0, 0, 312, 313, 1, 0, 0, 0,
313, 70, 1, 0, 0, 0, 314, 312, 1, 0, 0, 0, 315, 317, 7, 26, 0, 0, 316,
315, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 316, 1, 0, 0, 0, 318, 319,
1, 0, 0, 0, 319, 320, 1, 0, 0, 0, 320, 321, 6, 35, 0, 0, 321, 72, 1, 0,
0, 0, 322, 323, 7, 27, 0, 0, 323, 74, 1, 0, 0, 0, 324, 326, 8, 28, 0, 0,
325, 324, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 325, 1, 0, 0, 0, 327,
328, 1, 0, 0, 0, 328, 76, 1, 0, 0, 0, 29, 0, 90, 133, 150, 209, 214, 219,
225, 228, 232, 237, 239, 242, 248, 252, 257, 259, 261, 267, 269, 277, 279,
283, 289, 308, 310, 312, 318, 327, 1, 6, 0, 0,
0, 57, 28, 59, 29, 61, 0, 63, 0, 65, 0, 67, 30, 69, 31, 71, 0, 73, 32,
1, 0, 29, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0, 75, 75,
107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 66, 66, 98, 98, 2, 0, 84, 84, 116,
116, 2, 0, 87, 87, 119, 119, 2, 0, 78, 78, 110, 110, 2, 0, 88, 88, 120,
120, 2, 0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71, 71, 103,
103, 2, 0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 79, 79, 111, 111,
2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72, 104, 104, 2,
0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70, 102, 102, 2,
0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92, 4, 0, 35,
36, 64, 90, 95, 95, 97, 123, 7, 0, 35, 36, 45, 45, 47, 58, 64, 90, 95,
95, 97, 123, 125, 125, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 8, 0,
9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 344, 0,
1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0,
9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0,
0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0,
0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0,
0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1,
0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47,
1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0,
57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0,
0, 73, 1, 0, 0, 0, 1, 75, 1, 0, 0, 0, 3, 77, 1, 0, 0, 0, 5, 79, 1, 0, 0,
0, 7, 81, 1, 0, 0, 0, 9, 83, 1, 0, 0, 0, 11, 88, 1, 0, 0, 0, 13, 90, 1,
0, 0, 0, 15, 93, 1, 0, 0, 0, 17, 96, 1, 0, 0, 0, 19, 98, 1, 0, 0, 0, 21,
101, 1, 0, 0, 0, 23, 103, 1, 0, 0, 0, 25, 106, 1, 0, 0, 0, 27, 111, 1,
0, 0, 0, 29, 117, 1, 0, 0, 0, 31, 125, 1, 0, 0, 0, 33, 133, 1, 0, 0, 0,
35, 140, 1, 0, 0, 0, 37, 150, 1, 0, 0, 0, 39, 153, 1, 0, 0, 0, 41, 157,
1, 0, 0, 0, 43, 161, 1, 0, 0, 0, 45, 164, 1, 0, 0, 0, 47, 173, 1, 0, 0,
0, 49, 177, 1, 0, 0, 0, 51, 184, 1, 0, 0, 0, 53, 200, 1, 0, 0, 0, 55, 202,
1, 0, 0, 0, 57, 252, 1, 0, 0, 0, 59, 274, 1, 0, 0, 0, 61, 276, 1, 0, 0,
0, 63, 283, 1, 0, 0, 0, 65, 286, 1, 0, 0, 0, 67, 290, 1, 0, 0, 0, 69, 307,
1, 0, 0, 0, 71, 313, 1, 0, 0, 0, 73, 316, 1, 0, 0, 0, 75, 76, 5, 40, 0,
0, 76, 2, 1, 0, 0, 0, 77, 78, 5, 41, 0, 0, 78, 4, 1, 0, 0, 0, 79, 80, 5,
91, 0, 0, 80, 6, 1, 0, 0, 0, 81, 82, 5, 93, 0, 0, 82, 8, 1, 0, 0, 0, 83,
84, 5, 44, 0, 0, 84, 10, 1, 0, 0, 0, 85, 89, 5, 61, 0, 0, 86, 87, 5, 61,
0, 0, 87, 89, 5, 61, 0, 0, 88, 85, 1, 0, 0, 0, 88, 86, 1, 0, 0, 0, 89,
12, 1, 0, 0, 0, 90, 91, 5, 33, 0, 0, 91, 92, 5, 61, 0, 0, 92, 14, 1, 0,
0, 0, 93, 94, 5, 60, 0, 0, 94, 95, 5, 62, 0, 0, 95, 16, 1, 0, 0, 0, 96,
97, 5, 60, 0, 0, 97, 18, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 100, 5, 61,
0, 0, 100, 20, 1, 0, 0, 0, 101, 102, 5, 62, 0, 0, 102, 22, 1, 0, 0, 0,
103, 104, 5, 62, 0, 0, 104, 105, 5, 61, 0, 0, 105, 24, 1, 0, 0, 0, 106,
107, 7, 0, 0, 0, 107, 108, 7, 1, 0, 0, 108, 109, 7, 2, 0, 0, 109, 110,
7, 3, 0, 0, 110, 26, 1, 0, 0, 0, 111, 112, 7, 1, 0, 0, 112, 113, 7, 0,
0, 0, 113, 114, 7, 1, 0, 0, 114, 115, 7, 2, 0, 0, 115, 116, 7, 3, 0, 0,
116, 28, 1, 0, 0, 0, 117, 118, 7, 4, 0, 0, 118, 119, 7, 3, 0, 0, 119, 120,
7, 5, 0, 0, 120, 121, 7, 6, 0, 0, 121, 122, 7, 3, 0, 0, 122, 123, 7, 3,
0, 0, 123, 124, 7, 7, 0, 0, 124, 30, 1, 0, 0, 0, 125, 126, 7, 3, 0, 0,
126, 127, 7, 8, 0, 0, 127, 128, 7, 1, 0, 0, 128, 129, 7, 9, 0, 0, 129,
131, 7, 5, 0, 0, 130, 132, 7, 9, 0, 0, 131, 130, 1, 0, 0, 0, 131, 132,
1, 0, 0, 0, 132, 32, 1, 0, 0, 0, 133, 134, 7, 10, 0, 0, 134, 135, 7, 3,
0, 0, 135, 136, 7, 11, 0, 0, 136, 137, 7, 3, 0, 0, 137, 138, 7, 8, 0, 0,
138, 139, 7, 12, 0, 0, 139, 34, 1, 0, 0, 0, 140, 141, 7, 13, 0, 0, 141,
142, 7, 14, 0, 0, 142, 143, 7, 7, 0, 0, 143, 144, 7, 5, 0, 0, 144, 145,
7, 15, 0, 0, 145, 146, 7, 1, 0, 0, 146, 148, 7, 7, 0, 0, 147, 149, 7, 9,
0, 0, 148, 147, 1, 0, 0, 0, 148, 149, 1, 0, 0, 0, 149, 36, 1, 0, 0, 0,
150, 151, 7, 1, 0, 0, 151, 152, 7, 7, 0, 0, 152, 38, 1, 0, 0, 0, 153, 154,
7, 7, 0, 0, 154, 155, 7, 14, 0, 0, 155, 156, 7, 5, 0, 0, 156, 40, 1, 0,
0, 0, 157, 158, 7, 15, 0, 0, 158, 159, 7, 7, 0, 0, 159, 160, 7, 16, 0,
0, 160, 42, 1, 0, 0, 0, 161, 162, 7, 14, 0, 0, 162, 163, 7, 10, 0, 0, 163,
44, 1, 0, 0, 0, 164, 165, 7, 17, 0, 0, 165, 166, 7, 15, 0, 0, 166, 167,
7, 9, 0, 0, 167, 168, 7, 5, 0, 0, 168, 169, 7, 14, 0, 0, 169, 170, 7, 2,
0, 0, 170, 171, 7, 3, 0, 0, 171, 172, 7, 7, 0, 0, 172, 46, 1, 0, 0, 0,
173, 174, 7, 17, 0, 0, 174, 175, 7, 15, 0, 0, 175, 176, 7, 9, 0, 0, 176,
48, 1, 0, 0, 0, 177, 178, 7, 17, 0, 0, 178, 179, 7, 15, 0, 0, 179, 180,
7, 9, 0, 0, 180, 181, 7, 15, 0, 0, 181, 182, 7, 7, 0, 0, 182, 183, 7, 18,
0, 0, 183, 50, 1, 0, 0, 0, 184, 185, 7, 17, 0, 0, 185, 186, 7, 15, 0, 0,
186, 187, 7, 9, 0, 0, 187, 188, 7, 15, 0, 0, 188, 189, 7, 0, 0, 0, 189,
190, 7, 0, 0, 0, 190, 52, 1, 0, 0, 0, 191, 192, 7, 5, 0, 0, 192, 193, 7,
10, 0, 0, 193, 194, 7, 19, 0, 0, 194, 201, 7, 3, 0, 0, 195, 196, 7, 20,
0, 0, 196, 197, 7, 15, 0, 0, 197, 198, 7, 0, 0, 0, 198, 199, 7, 9, 0, 0,
199, 201, 7, 3, 0, 0, 200, 191, 1, 0, 0, 0, 200, 195, 1, 0, 0, 0, 201,
54, 1, 0, 0, 0, 202, 203, 7, 21, 0, 0, 203, 56, 1, 0, 0, 0, 204, 206, 3,
55, 27, 0, 205, 204, 1, 0, 0, 0, 205, 206, 1, 0, 0, 0, 206, 208, 1, 0,
0, 0, 207, 209, 3, 71, 35, 0, 208, 207, 1, 0, 0, 0, 209, 210, 1, 0, 0,
0, 210, 208, 1, 0, 0, 0, 210, 211, 1, 0, 0, 0, 211, 219, 1, 0, 0, 0, 212,
216, 5, 46, 0, 0, 213, 215, 3, 71, 35, 0, 214, 213, 1, 0, 0, 0, 215, 218,
1, 0, 0, 0, 216, 214, 1, 0, 0, 0, 216, 217, 1, 0, 0, 0, 217, 220, 1, 0,
0, 0, 218, 216, 1, 0, 0, 0, 219, 212, 1, 0, 0, 0, 219, 220, 1, 0, 0, 0,
220, 230, 1, 0, 0, 0, 221, 223, 7, 3, 0, 0, 222, 224, 3, 55, 27, 0, 223,
222, 1, 0, 0, 0, 223, 224, 1, 0, 0, 0, 224, 226, 1, 0, 0, 0, 225, 227,
3, 71, 35, 0, 226, 225, 1, 0, 0, 0, 227, 228, 1, 0, 0, 0, 228, 226, 1,
0, 0, 0, 228, 229, 1, 0, 0, 0, 229, 231, 1, 0, 0, 0, 230, 221, 1, 0, 0,
0, 230, 231, 1, 0, 0, 0, 231, 253, 1, 0, 0, 0, 232, 234, 3, 55, 27, 0,
233, 232, 1, 0, 0, 0, 233, 234, 1, 0, 0, 0, 234, 235, 1, 0, 0, 0, 235,
237, 5, 46, 0, 0, 236, 238, 3, 71, 35, 0, 237, 236, 1, 0, 0, 0, 238, 239,
1, 0, 0, 0, 239, 237, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0, 240, 250, 1, 0,
0, 0, 241, 243, 7, 3, 0, 0, 242, 244, 3, 55, 27, 0, 243, 242, 1, 0, 0,
0, 243, 244, 1, 0, 0, 0, 244, 246, 1, 0, 0, 0, 245, 247, 3, 71, 35, 0,
246, 245, 1, 0, 0, 0, 247, 248, 1, 0, 0, 0, 248, 246, 1, 0, 0, 0, 248,
249, 1, 0, 0, 0, 249, 251, 1, 0, 0, 0, 250, 241, 1, 0, 0, 0, 250, 251,
1, 0, 0, 0, 251, 253, 1, 0, 0, 0, 252, 205, 1, 0, 0, 0, 252, 233, 1, 0,
0, 0, 253, 58, 1, 0, 0, 0, 254, 260, 5, 34, 0, 0, 255, 259, 8, 22, 0, 0,
256, 257, 5, 92, 0, 0, 257, 259, 9, 0, 0, 0, 258, 255, 1, 0, 0, 0, 258,
256, 1, 0, 0, 0, 259, 262, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 260, 261,
1, 0, 0, 0, 261, 263, 1, 0, 0, 0, 262, 260, 1, 0, 0, 0, 263, 275, 5, 34,
0, 0, 264, 270, 5, 39, 0, 0, 265, 269, 8, 23, 0, 0, 266, 267, 5, 92, 0,
0, 267, 269, 9, 0, 0, 0, 268, 265, 1, 0, 0, 0, 268, 266, 1, 0, 0, 0, 269,
272, 1, 0, 0, 0, 270, 268, 1, 0, 0, 0, 270, 271, 1, 0, 0, 0, 271, 273,
1, 0, 0, 0, 272, 270, 1, 0, 0, 0, 273, 275, 5, 39, 0, 0, 274, 254, 1, 0,
0, 0, 274, 264, 1, 0, 0, 0, 275, 60, 1, 0, 0, 0, 276, 280, 7, 24, 0, 0,
277, 279, 7, 25, 0, 0, 278, 277, 1, 0, 0, 0, 279, 282, 1, 0, 0, 0, 280,
278, 1, 0, 0, 0, 280, 281, 1, 0, 0, 0, 281, 62, 1, 0, 0, 0, 282, 280, 1,
0, 0, 0, 283, 284, 5, 91, 0, 0, 284, 285, 5, 93, 0, 0, 285, 64, 1, 0, 0,
0, 286, 287, 5, 91, 0, 0, 287, 288, 5, 42, 0, 0, 288, 289, 5, 93, 0, 0,
289, 66, 1, 0, 0, 0, 290, 303, 3, 61, 30, 0, 291, 292, 5, 46, 0, 0, 292,
302, 3, 61, 30, 0, 293, 302, 3, 63, 31, 0, 294, 302, 3, 65, 32, 0, 295,
297, 5, 46, 0, 0, 296, 298, 3, 71, 35, 0, 297, 296, 1, 0, 0, 0, 298, 299,
1, 0, 0, 0, 299, 297, 1, 0, 0, 0, 299, 300, 1, 0, 0, 0, 300, 302, 1, 0,
0, 0, 301, 291, 1, 0, 0, 0, 301, 293, 1, 0, 0, 0, 301, 294, 1, 0, 0, 0,
301, 295, 1, 0, 0, 0, 302, 305, 1, 0, 0, 0, 303, 301, 1, 0, 0, 0, 303,
304, 1, 0, 0, 0, 304, 68, 1, 0, 0, 0, 305, 303, 1, 0, 0, 0, 306, 308, 7,
26, 0, 0, 307, 306, 1, 0, 0, 0, 308, 309, 1, 0, 0, 0, 309, 307, 1, 0, 0,
0, 309, 310, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 312, 6, 34, 0, 0, 312,
70, 1, 0, 0, 0, 313, 314, 7, 27, 0, 0, 314, 72, 1, 0, 0, 0, 315, 317, 8,
28, 0, 0, 316, 315, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 316, 1, 0, 0,
0, 318, 319, 1, 0, 0, 0, 319, 74, 1, 0, 0, 0, 29, 0, 88, 131, 148, 200,
205, 210, 216, 219, 223, 228, 230, 233, 239, 243, 248, 250, 252, 258, 260,
268, 270, 274, 280, 299, 301, 303, 309, 318, 1, 6, 0, 0,
}
deserializer := antlr.NewATNDeserializer(nil)
staticData.atn = deserializer.Deserialize(staticData.serializedATN)
@@ -284,11 +281,10 @@ const (
FilterQueryLexerHAS = 24
FilterQueryLexerHASANY = 25
FilterQueryLexerHASALL = 26
FilterQueryLexerSEARCH = 27
FilterQueryLexerBOOL = 28
FilterQueryLexerNUMBER = 29
FilterQueryLexerQUOTED_TEXT = 30
FilterQueryLexerKEY = 31
FilterQueryLexerWS = 32
FilterQueryLexerFREETEXT = 33
FilterQueryLexerBOOL = 27
FilterQueryLexerNUMBER = 28
FilterQueryLexerQUOTED_TEXT = 29
FilterQueryLexerKEY = 30
FilterQueryLexerWS = 31
FilterQueryLexerFREETEXT = 32
)

View File

@@ -1,4 +1,4 @@
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser // FilterQuery
@@ -44,9 +44,6 @@ type FilterQueryListener interface {
// EnterFunctionCall is called when entering the functionCall production.
EnterFunctionCall(c *FunctionCallContext)
// EnterSearchCall is called when entering the searchCall production.
EnterSearchCall(c *SearchCallContext)
// EnterFunctionParamList is called when entering the functionParamList production.
EnterFunctionParamList(c *FunctionParamListContext)
@@ -98,9 +95,6 @@ type FilterQueryListener interface {
// ExitFunctionCall is called when exiting the functionCall production.
ExitFunctionCall(c *FunctionCallContext)
// ExitSearchCall is called when exiting the searchCall production.
ExitSearchCall(c *SearchCallContext)
// ExitFunctionParamList is called when exiting the functionParamList production.
ExitFunctionParamList(c *FunctionParamListContext)

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Code generated from FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
// Code generated from grammar/FilterQuery.g4 by ANTLR 4.13.2. DO NOT EDIT.
package parser // FilterQuery
@@ -44,9 +44,6 @@ type FilterQueryVisitor interface {
// Visit a parse tree produced by FilterQueryParser#functionCall.
VisitFunctionCall(ctx *FunctionCallContext) interface{}
// Visit a parse tree produced by FilterQueryParser#searchCall.
VisitSearchCall(ctx *SearchCallContext) interface{}
// Visit a parse tree produced by FilterQueryParser#functionParamList.
VisitFunctionParamList(ctx *FunctionParamListContext) interface{}

View File

@@ -20,8 +20,6 @@ import (
"github.com/SigNoz/signoz/pkg/valuer"
)
const estimateTimeout = 5 * time.Second
const traceOutsideRangeWarn = "Query %s references a trace_id that exists between %s and %s (UTC) but lies outside the selected time range; adjust the time range to see results"
type builderQuery[T any] struct {
@@ -249,10 +247,6 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
return nil, err
}
if err := q.enforceEstimate(ctx, stmt); err != nil {
return nil, err
}
// Execute the query with proper context for partial value detection
result, err := q.executeWithContext(ctx, stmt.Query, stmt.Args)
if err != nil {
@@ -264,70 +258,6 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
return result, nil
}
// estimateRows runs EXPLAIN ESTIMATE for a cost-guarded statement and returns its
// estimated total scan rows. guarded=false means there is nothing to enforce (no
// CostGuard or a non-positive budget). A non-nil error means the query must be
// rejected: either the estimate itself failed (fail closed — we cannot honor the
// budget without it) or the parent context was cancelled (surfaced as-is). Callers
// enforce the budget so it can be applied per-statement or cumulatively.
func (q *builderQuery[T]) estimateRows(ctx context.Context, stmt *qbtypes.Statement) (int64, bool, error) {
if stmt.CostGuard == nil || stmt.CostGuard.MaxScanRows <= 0 {
return 0, false, nil
}
estCtx, cancel := context.WithTimeout(ctx, estimateTimeout)
defer cancel()
entries, err := q.telemetryStore.Estimate(estCtx, stmt.Query, stmt.Args...)
if err != nil {
// Parent cancellation isn't the budget's concern — surface it unchanged.
if ctx.Err() != nil {
return 0, true, ctx.Err()
}
// Fail closed: this is a scan-heavy statement and without an estimate we
// cannot bound its cost, so running it unbounded is exactly what the guard
// exists to prevent.
reason := fmt.Sprintf("This query is too broad to plan within %s; narrow the time range or add a more selective filter.", estimateTimeout)
if estCtx.Err() != context.DeadlineExceeded {
reason = "Could not estimate this query's scan cost; narrow the time range or add a more selective filter."
q.logger.WarnContext(ctx, "EXPLAIN ESTIMATE failed; rejecting scan-heavy query (fail-closed)", errors.Attr(err))
}
return 0, true, errors.NewInvalidInputf(errors.CodeInvalidInput, "%s", withAdvisory(stmt.CostGuard.Warning, reason))
}
var rows int64
for _, e := range entries {
rows += e.Rows
}
return rows, true, nil
}
// enforceEstimate rejects a single scan-heavy statement (Statement.CostGuard) whose
// EXPLAIN ESTIMATE rows exceed its budget, before executing. Budget 0 disables.
func (q *builderQuery[T]) enforceEstimate(ctx context.Context, stmt *qbtypes.Statement) error {
rows, guarded, err := q.estimateRows(ctx, stmt)
if err != nil {
return err
}
if !guarded {
return nil
}
if budget := stmt.CostGuard.MaxScanRows; rows > budget {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "%s",
withAdvisory(stmt.CostGuard.Warning, fmt.Sprintf("This query would scan about %d rows in this range, over the limit of %d; narrow the time range or add a more selective filter.", rows, budget)))
}
return nil
}
// withAdvisory prefixes reason with the requirement's advisory (e.g. the search()
// warning) when present, so the rejection leads with why the query is expensive.
func withAdvisory(advisory, reason string) string {
if advisory == "" {
return reason
}
return strings.TrimRight(advisory, ". ") + ". " + reason
}
// narrowWindowByTraceID inspects the filter for trace_id predicates and clamps
// [fromMS,toMS] to the time range stored in signoz_traces.distributed_trace_summary.
// Returns the (possibly narrowed) window, overlap=false when the trace lies
@@ -561,11 +491,6 @@ func (q *builderQuery[T]) executeWindowList(ctx context.Context) (*qbtypes.Resul
var warnings []string
var warningsDocURL string
// Cost guard is enforced against the cumulative estimate across the buckets we
// actually visit — a broad search() that fans every bucket must be bounded by
// the per-query budget, not let each bucket pass its slice independently.
var estimatedScan int64
for _, r := range buckets {
q.spec.Offset = 0
q.spec.Limit = need
@@ -576,17 +501,6 @@ func (q *builderQuery[T]) executeWindowList(ctx context.Context) (*qbtypes.Resul
}
warnings = stmt.Warnings
warningsDocURL = stmt.WarningsDocURL
rowsEst, guarded, err := q.estimateRows(ctx, stmt)
if err != nil {
return nil, err
}
if guarded {
estimatedScan += rowsEst
if budget := stmt.CostGuard.MaxScanRows; estimatedScan > budget {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "%s",
withAdvisory(stmt.CostGuard.Warning, fmt.Sprintf("This query would scan about %d rows across the time range, over the limit of %d; narrow the time range or add a more selective filter.", estimatedScan, budget)))
}
}
// Execute with proper context for partial value detection
res, err := q.executeWithContext(ctx, stmt.Query, stmt.Args)
if err != nil {

View File

@@ -27,8 +27,6 @@ type Config struct {
SkipResourceFingerprint SkipResourceFingerprint `yaml:"skip_resource_fingerprint" mapstructure:"skip_resource_fingerprint"`
// LogTraceIDWindowPadding is the padding added to narrowed down timerange from trace summary to logs with trace_id filter.
LogTraceIDWindowPadding time.Duration `yaml:"log_trace_id_window_padding" mapstructure:"log_trace_id_window_padding"`
// SearchMaxScanRows caps the rows a search() query may scan, enforced via
SearchMaxScanRows int64 `yaml:"search_max_scan_rows" mapstructure:"search_max_scan_rows"`
}
// NewConfigFactory creates a new config factory for querier.
@@ -47,7 +45,6 @@ func newConfig() factory.Config {
Threshold: 100000,
},
LogTraceIDWindowPadding: 5 * time.Minute,
SearchMaxScanRows: 100_000_000,
}
}
@@ -68,9 +65,6 @@ func (c Config) Validate() error {
if c.LogTraceIDWindowPadding < 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "log_trace_id_window_padding must not be negative, got %v", c.LogTraceIDWindowPadding)
}
if c.SearchMaxScanRows < 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "search_max_scan_rows must not be negative, got %v", c.SearchMaxScanRows)
}
return nil
}

View File

@@ -843,7 +843,7 @@ func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query
}
// createRangedQuery creates a copy of the query with a different time range.
func (q *querier) createRangedQuery(orgID valuer.UUID, originalQuery qbtypes.Query, timeRange qbtypes.TimeRange) qbtypes.Query {
func (q *querier) createRangedQuery(_ valuer.UUID, originalQuery qbtypes.Query, timeRange qbtypes.TimeRange) qbtypes.Query {
// this is called in a goroutine, so we create a copy of the query to avoid race conditions
switch qt := originalQuery.(type) {
case *promqlQuery:

View File

@@ -126,7 +126,6 @@ func newProvider(
telemetryStore,
cfg.SkipResourceFingerprint.Enabled,
cfg.SkipResourceFingerprint.Threshold,
telemetrylogs.WithSearchMaxScanRows(cfg.SearchMaxScanRows),
)
// Create audit statement builder

View File

@@ -402,7 +402,6 @@ func (pc *LogParsingPipelineController) RecommendAgentConfig(
return nil, "", err
}
// TODO(Tushar): thread orgID here to evaluate correctly
if pc.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgId)) {
// add default normalize pipeline at the beginning, only for sending to collector
enrichedPipelines = append([]pipelinetypes.GettablePipeline{pc.getNormalizePipeline()}, enrichedPipelines...)

View File

@@ -8,10 +8,6 @@ const (
// BodyFullTextSearchDefaultWarning is emitted when a full-text search or "body" searches are hit
// with New JSON Body enhancements.
BodyFullTextSearchDefaultWarning = "Full text searches default to `body.message:string`. Use `body.<key>` to search a different field inside body"
// SearchWarning is emitted on every search() call. search() scans all fields,
// so it is slow and expensive; a specific field is cheaper.
SearchWarning = "search() runs across all fields and can be slow and expensive. Prefer a specific field, e.g. `<context>.<field_key>:<type>`"
)
var (

View File

@@ -32,7 +32,7 @@ var friendly = map[string]string{
"BETWEEN": "BETWEEN", "IN": "IN", "EXISTS": "EXISTS",
"REGEXP": "REGEXP", "CONTAINS": "CONTAINS",
"HAS": "has()", "HASANY": "hasAny()", "HASALL": "hasAll()",
"HASTOKEN": "hasToken()", "SEARCH": "search()",
"HASTOKEN": "hasToken()",
// literals / identifiers
"NUMBER": "number",

View File

@@ -163,16 +163,14 @@ func inferDataTypesFromList(values []any) []telemetrytypes.FieldDataType {
return out
}
// NewFunctionUnsupportedError returns the error for a has/hasAny/hasAll/hasToken/search
// operator on a builder that doesn't support it (logs only), or nil for other operators.
// NewFunctionUnsupportedError returns the error for a has/hasAny/hasAll/hasToken operator
// on a builder that doesn't support it (logs body only), or nil for other operators.
func NewFunctionUnsupportedError(operator qbtypes.FilterOperator) error {
switch operator {
case qbtypes.FilterOperatorHasToken:
return errors.NewInvalidInputf(errors.CodeInvalidInput, "function `hasToken` only supports body field as first parameter").WithUrl(hasTokenFunctionDocURL)
case qbtypes.FilterOperatorHas, qbtypes.FilterOperatorHasAny, qbtypes.FilterOperatorHasAll:
return errors.NewInvalidInputf(errors.CodeInvalidInput, "function `%s` supports only body JSON search", operator.FunctionName()).WithUrl(functionBodyJSONSearchDocURL)
case qbtypes.FilterOperatorSearch:
return errors.NewInvalidInputf(errors.CodeInvalidInput, "function `search` is only supported for logs")
default:
return nil
}

View File

@@ -15,8 +15,8 @@ import (
type FieldConstraint struct {
Field string
Operator qbtypes.FilterOperator
Value any
Values []any // For IN, NOT IN operations
Value interface{}
Values []interface{} // For IN, NOT IN operations
}
// ConstraintSet represents a set of constraints that must all be true (AND).
@@ -103,7 +103,7 @@ func (d *LogicalContradictionDetector) popNotContext() {
}
// Visit dispatches to the appropriate visit method.
func (d *LogicalContradictionDetector) Visit(tree antlr.ParseTree) any {
func (d *LogicalContradictionDetector) Visit(tree antlr.ParseTree) interface{} {
if tree == nil {
return nil
}
@@ -111,7 +111,7 @@ func (d *LogicalContradictionDetector) Visit(tree antlr.ParseTree) any {
}
// VisitQuery is the entry point.
func (d *LogicalContradictionDetector) VisitQuery(ctx *grammar.QueryContext) any {
func (d *LogicalContradictionDetector) VisitQuery(ctx *grammar.QueryContext) interface{} {
d.Visit(ctx.Expression())
// Check final constraints
d.checkContradictions(d.currentConstraints())
@@ -119,12 +119,12 @@ func (d *LogicalContradictionDetector) VisitQuery(ctx *grammar.QueryContext) any
}
// VisitExpression just passes through to OrExpression.
func (d *LogicalContradictionDetector) VisitExpression(ctx *grammar.ExpressionContext) any {
func (d *LogicalContradictionDetector) VisitExpression(ctx *grammar.ExpressionContext) interface{} {
return d.Visit(ctx.OrExpression())
}
// VisitOrExpression handles OR logic.
func (d *LogicalContradictionDetector) VisitOrExpression(ctx *grammar.OrExpressionContext) any {
func (d *LogicalContradictionDetector) VisitOrExpression(ctx *grammar.OrExpressionContext) interface{} {
andExpressions := ctx.AllAndExpression()
if len(andExpressions) == 1 {
@@ -149,7 +149,7 @@ func (d *LogicalContradictionDetector) VisitOrExpression(ctx *grammar.OrExpressi
}
// VisitAndExpression handles AND logic (including implicit AND).
func (d *LogicalContradictionDetector) VisitAndExpression(ctx *grammar.AndExpressionContext) any {
func (d *LogicalContradictionDetector) VisitAndExpression(ctx *grammar.AndExpressionContext) interface{} {
unaryExpressions := ctx.AllUnaryExpression()
// Visit each unary expression, accumulating constraints
@@ -161,7 +161,7 @@ func (d *LogicalContradictionDetector) VisitAndExpression(ctx *grammar.AndExpres
}
// VisitUnaryExpression handles NOT operator.
func (d *LogicalContradictionDetector) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) any {
func (d *LogicalContradictionDetector) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) interface{} {
hasNot := ctx.NOT() != nil
if hasNot {
@@ -180,7 +180,7 @@ func (d *LogicalContradictionDetector) VisitUnaryExpression(ctx *grammar.UnaryEx
}
// VisitPrimary handles different primary expressions.
func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext) any {
func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext) interface{} {
if ctx.OrExpression() != nil {
// Parenthesized expression
// If we're in an AND context, we continue with the same constraint set
@@ -191,9 +191,6 @@ func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext)
} else if ctx.FunctionCall() != nil {
// Handle function calls if needed
return nil
} else if ctx.SearchCall() != nil {
// search() spans all fields; it can never be a logical contradiction
return nil
} else if ctx.FullText() != nil {
// Handle full text search if needed
return nil
@@ -203,7 +200,7 @@ func (d *LogicalContradictionDetector) VisitPrimary(ctx *grammar.PrimaryContext)
}
// VisitComparison extracts constraints from comparisons.
func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonContext) any {
func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonContext) interface{} {
if ctx.Key() == nil {
return nil
}
@@ -277,7 +274,7 @@ func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonCo
constraint := FieldConstraint{
Field: field,
Operator: operator,
Values: []any{val1, val2},
Values: []interface{}{val1, val2},
}
d.addConstraint(constraint)
}
@@ -346,7 +343,7 @@ func (d *LogicalContradictionDetector) VisitComparison(ctx *grammar.ComparisonCo
}
// extractValue extracts the actual value from a ValueContext.
func (d *LogicalContradictionDetector) extractValue(ctx grammar.IValueContext) any {
func (d *LogicalContradictionDetector) extractValue(ctx grammar.IValueContext) interface{} {
if ctx.QUOTED_TEXT() != nil {
text := ctx.QUOTED_TEXT().GetText()
// Remove quotes
@@ -365,12 +362,12 @@ func (d *LogicalContradictionDetector) extractValue(ctx grammar.IValueContext) a
}
// extractValueList extracts values from a ValueListContext.
func (d *LogicalContradictionDetector) extractValueList(ctx grammar.IValueListContext) []any {
func (d *LogicalContradictionDetector) extractValueList(ctx grammar.IValueListContext) []interface{} {
if ctx == nil {
return nil
}
values := []any{}
values := []interface{}{}
for _, val := range ctx.AllValue() {
values = append(values, d.extractValue(val))
}
@@ -766,7 +763,7 @@ func (d *LogicalContradictionDetector) checkRangeContradictions(constraints []Fi
}
// valuesSatisfyRanges checks if a value satisfies all range constraints.
func (d *LogicalContradictionDetector) valuesSatisfyRanges(value any, constraints []FieldConstraint) bool {
func (d *LogicalContradictionDetector) valuesSatisfyRanges(value interface{}, constraints []FieldConstraint) bool {
val, err := parseNumericValue(value)
if err != nil {
return true // If not numeric, we can't check
@@ -802,7 +799,7 @@ func (d *LogicalContradictionDetector) valuesSatisfyRanges(value any, constraint
}
// valueSatisfiesBetween checks if a value is within a BETWEEN range.
func (d *LogicalContradictionDetector) valueSatisfiesBetween(value any, between FieldConstraint) bool {
func (d *LogicalContradictionDetector) valueSatisfiesBetween(value interface{}, between FieldConstraint) bool {
if len(between.Values) != 2 {
return false
}
@@ -851,7 +848,7 @@ func (d *LogicalContradictionDetector) cloneConstraintSet(set *ConstraintSet) *C
}
// parseNumericValue attempts to parse a value as a number.
func parseNumericValue(value any) (float64, error) {
func parseNumericValue(value interface{}) (float64, error) {
switch v := value.(type) {
case float64:
return v, nil

View File

@@ -43,8 +43,6 @@ type filterExpressionVisitor struct {
keysWithWarnings map[string]bool
startNs uint64
endNs uint64
requiresCostGuard bool
}
type FilterExprVisitorOpts struct {
@@ -83,10 +81,9 @@ func newFilterExpressionVisitor(opts FilterExprVisitorOpts) *filterExpressionVis
}
type PreparedWhereClause struct {
WhereClause *sqlbuilder.WhereClause
Warnings []string
WarningsDocURL string
RequiresCostGuard bool
WhereClause *sqlbuilder.WhereClause
Warnings []string
WarningsDocURL string
}
func (p PreparedWhereClause) IsEmpty() bool {
@@ -168,12 +165,12 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (PreparedWhere
// Return empty where clause so callers can skip the WHERE clause
if cond == "" || cond == SkipConditionLiteral {
return PreparedWhereClause{WhereClause: nil, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL, RequiresCostGuard: visitor.requiresCostGuard}, nil
return PreparedWhereClause{WhereClause: nil, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL}, nil
}
whereClause := sqlbuilder.NewWhereClause().AddWhereExpr(visitor.builder.Args, cond)
return PreparedWhereClause{WhereClause: whereClause, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL, RequiresCostGuard: visitor.requiresCostGuard}, nil
return PreparedWhereClause{WhereClause: whereClause, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL}, nil
}
// Visit dispatches to the specific visit method based on node type.
@@ -206,8 +203,6 @@ func (v *filterExpressionVisitor) Visit(tree antlr.ParseTree) any {
return v.VisitValueList(t)
case *grammar.FullTextContext:
return v.VisitFullText(t)
case *grammar.SearchCallContext:
return v.VisitSearchCall(t)
case *grammar.FunctionCallContext:
return v.VisitFunctionCall(t)
case *grammar.FunctionParamListContext:
@@ -322,8 +317,6 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
return v.Visit(ctx.Comparison())
} else if ctx.FunctionCall() != nil {
return v.Visit(ctx.FunctionCall())
} else if ctx.SearchCall() != nil {
return v.Visit(ctx.SearchCall())
} else if ctx.FullText() != nil {
return v.Visit(ctx.FullText())
}
@@ -755,62 +748,6 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon
return v.builder.Or(conds...)
}
// VisitSearchCall handles search('needle'): a keyless full-text search. The
// visitor emits FilterOperatorSearch; the signal's condition builder owns the
// behavior (logs fan out across all columns, other signals reject it).
func (v *filterExpressionVisitor) VisitSearchCall(ctx *grammar.SearchCallContext) any {
// Flag scan-heavy so the statement builder attaches the cost guard.
v.requiresCostGuard = true
paramList := ctx.FunctionParamList()
if paramList == nil {
v.errors = append(v.errors, "function `search` expects a single value parameter, e.g. search('error')")
return ErrorConditionLiteral
}
// Single needle today; functionParamList leaves room for scoped forms
// (search(body, 'abc')) without a grammar change.
params := paramList.AllFunctionParam()
if len(params) != 1 {
v.errors = append(v.errors, fmt.Sprintf("function `search` currently supports a single argument, e.g. search('error'), but got %d", len(params)))
return ErrorConditionLiteral
}
// Use the raw needle: a bare word parses as a key but we take its literal text
// (not the normalized key, which would strip a `context.` prefix or `:type`).
param := params[0]
var searchText string
switch {
case param.Value() != nil:
// The search needle is always a literal string: take the token text rather
// than visiting (which parses NUMBER to float64 and would render large/round
// integers as scientific notation, e.g. search(1000000) -> "1e+06").
valCtx := param.Value()
if valCtx.QUOTED_TEXT() != nil {
searchText = trimQuotes(valCtx.QUOTED_TEXT().GetText())
} else {
searchText = valCtx.GetText()
}
case param.Key() != nil:
searchText = param.Key().GetText()
default:
v.errors = append(v.errors, "function `search` expects a quoted string, e.g. search('error')")
return ErrorConditionLiteral
}
// Keyless: pass an empty key as a placeholder for the ConditionFor signature.
conds, ok := v.buildConditions(&telemetrytypes.TelemetryFieldKey{}, nil, qbtypes.FilterOperatorSearch, searchText)
if !ok {
return ErrorConditionLiteral
}
if len(conds) == 0 {
return SkipConditionLiteral
}
if len(conds) == 1 {
return conds[0]
}
return v.builder.Or(conds...)
}
// VisitFunctionParamList handles the parameter list for function calls.
func (v *filterExpressionVisitor) VisitFunctionParamList(ctx *grammar.FunctionParamListContext) any {
params := ctx.AllFunctionParam()

View File

@@ -23,13 +23,14 @@ func NewConditionBuilder(fm qbtypes.FieldMapper) *conditionBuilder {
func (c *conditionBuilder) conditionFor(
ctx context.Context,
orgID valuer.UUID,
startNs, endNs uint64,
key *telemetrytypes.TelemetryFieldKey,
operator qbtypes.FilterOperator,
value any,
sb *sqlbuilder.SelectBuilder,
) (string, error) {
columns, err := c.fm.ColumnFor(ctx, valuer.UUID{}, startNs, endNs, key)
columns, err := c.fm.ColumnFor(ctx, orgID, startNs, endNs, key)
if err != nil {
return "", err
}
@@ -38,7 +39,7 @@ func (c *conditionBuilder) conditionFor(
value = querybuilder.FormatValueForContains(value)
}
fieldExpression, err := c.fm.FieldFor(ctx, valuer.UUID{}, startNs, endNs, key)
fieldExpression, err := c.fm.FieldFor(ctx, orgID, startNs, endNs, key)
if err != nil {
return "", err
}
@@ -183,7 +184,7 @@ func (c *conditionBuilder) ConditionFor(
sb *sqlbuilder.SelectBuilder,
) ([]string, []string, error) {
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for audit.
// has/hasAny/hasAll/hasToken are logs-body-only; reject for audit.
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
return nil, nil, err
}
@@ -199,7 +200,7 @@ func (c *conditionBuilder) ConditionFor(
conds := make([]string, 0, len(keys))
for _, k := range keys {
cond, err := c.conditionForKey(ctx, startNs, endNs, k, operator, value, sb)
cond, err := c.conditionForKey(ctx, orgID, startNs, endNs, k, operator, value, sb)
if err != nil {
return nil, nil, err
}
@@ -210,6 +211,7 @@ func (c *conditionBuilder) ConditionFor(
func (c *conditionBuilder) conditionForKey(
ctx context.Context,
orgID valuer.UUID,
startNs uint64,
endNs uint64,
key *telemetrytypes.TelemetryFieldKey,
@@ -217,7 +219,7 @@ func (c *conditionBuilder) conditionForKey(
value any,
sb *sqlbuilder.SelectBuilder,
) (string, error) {
condition, err := c.conditionFor(ctx, startNs, endNs, key, operator, value, sb)
condition, err := c.conditionFor(ctx, orgID, startNs, endNs, key, operator, value, sb)
if err != nil {
return "", err
}
@@ -227,7 +229,7 @@ func (c *conditionBuilder) conditionForKey(
}
if operator.AddDefaultExistsFilter() {
existsCondition, err := c.conditionFor(ctx, startNs, endNs, key, qbtypes.FilterOperatorExists, nil, sb)
existsCondition, err := c.conditionFor(ctx, orgID, startNs, endNs, key, qbtypes.FilterOperatorExists, nil, sb)
if err != nil {
return "", err
}

View File

@@ -78,7 +78,7 @@ func (b *auditQueryStatementBuilder) Build(
end = querybuilder.ToNanoSecs(end)
keySelectors := getKeySelectors(query)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, keySelectors)
if err != nil {
return nil, err
}

View File

@@ -3,7 +3,6 @@ package telemetrylogs
import (
"context"
"fmt"
"regexp"
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
"github.com/SigNoz/signoz/pkg/errors"
@@ -26,70 +25,6 @@ func NewConditionBuilder(fm qbtypes.FieldMapper, fl flagger.Flagger) *conditionB
return &conditionBuilder{fm: fm, fl: fl}
}
// conditionForSearch ORs a case-insensitive match of the needle across every
// searchable column: log columns, the body (body_v2 JSON when use_json_body is
// on, else the body string), and the attribute/resource maps (keys + values,
// non-string values cast to string; JSON columns matched on their serialized
// form). Cost is bounded by the querier's EXPLAIN ESTIMATE gate, not a window cap.
func (c *conditionBuilder) conditionForSearch(
ctx context.Context,
orgID valuer.UUID,
value any,
sb *sqlbuilder.SelectBuilder,
) ([]string, []string, error) {
// search() is gated by its own feature flag; reject before building the fan-out.
if !c.fl.BooleanOrEmpty(ctx, flagger.FeatureAllowLogsSearch, featuretypes.NewFlaggerEvaluationContext(orgID)) {
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "function `search` is not enabled")
}
// use_json_body picks the body column below; the scan budget rides on CostGuard.
// search is a literal, case-insensitive substring match: QuoteMeta so regex
// metacharacters in the needle match literally, and LOWER both sides on every
// column. Keeping the body path as LOWER(toString(body_v2)) lets it use the
// LOWER(toString(body_v2)) skip index (a (?i) regex could use neither the index
// nor be ngram-extractable).
needle := regexp.QuoteMeta(fmt.Sprintf("%v", value))
contexts := []telemetrytypes.FieldContext{
telemetrytypes.FieldContextLog,
telemetrytypes.FieldContextBody,
telemetrytypes.FieldContextAttribute,
telemetrytypes.FieldContextResource,
}
useJSONBody := c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID))
var conditions []string
for _, fieldContext := range contexts {
for _, col := range searchColumns(fieldContext, useJSONBody) {
switch col.Type.GetType() {
case schema.ColumnTypeEnumMap:
keysExpr := fmt.Sprintf("mapKeys(%s)", col.Name)
valsExpr := fmt.Sprintf("mapValues(%s)", col.Name)
// match() needs a String array; cast non-string map values first.
if mc, ok := col.Type.(schema.MapColumnType); ok && mc.ValueType.GetType() != schema.ColumnTypeEnumString {
valsExpr = fmt.Sprintf("arrayMap(x -> toString(x), mapValues(%s))", col.Name)
}
conditions = append(conditions, sb.Or(
fmt.Sprintf("arrayExists(x -> match(LOWER(x), LOWER(%s)), %s)", sb.Var(needle), keysExpr),
fmt.Sprintf("arrayExists(x -> match(LOWER(x), LOWER(%s)), %s)", sb.Var(needle), valsExpr),
))
case schema.ColumnTypeEnumJSON:
conditions = append(conditions, fmt.Sprintf("match(LOWER(toString(%s)), LOWER(%s))", col.Name, sb.Var(needle)))
case schema.ColumnTypeEnumString, schema.ColumnTypeEnumLowCardinality:
conditions = append(conditions, fmt.Sprintf("match(LOWER(%s), LOWER(%s))", col.Name, sb.Var(needle)))
default:
return nil, nil, errors.NewInternalf(errors.CodeInternal, "search does not support the column type of %q", col.Name)
}
}
}
if len(conditions) == 0 {
return nil, nil, nil
}
// The advisory rides on CostGuard (set by the visitor), not warnings.
return []string{sb.Or(conditions...)}, nil, nil
}
// isBodyJSONSearch reports whether a key addresses a path within the body JSON. Only
// an explicit Body context qualifies; a bare, context-less `body` (e.g. full-text
// `count_distinct(body)` or `body EXISTS`) is a full-text match, not a `$.body` path.
@@ -446,11 +381,6 @@ func (c *conditionBuilder) ConditionFor(
sb *sqlbuilder.SelectBuilder,
) ([]string, []string, error) {
// search() is keyless; handle it before key resolution.
if operator == qbtypes.FilterOperatorSearch {
return c.conditionForSearch(ctx, orgID, value, sb)
}
keys, warning := querybuilder.ResolveKeys(key, fieldKeysForName)
var warnings []string
if warning != "" {

View File

@@ -96,7 +96,6 @@ func (m *fieldMapper) getColumn(ctx context.Context, orgID valuer.UUID, key *tel
}
case telemetrytypes.FieldContextBody:
// Body context is for JSON body fields. Use body_v2 if feature flag is enabled.
// TODO(Tushar): thread orgID here to evaluate correctly
if m.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID)) {
if key.Name == messageSubField {
return []*schema.Column{logsV2Columns[messageSubColumn]}, nil
@@ -106,7 +105,6 @@ func (m *fieldMapper) getColumn(ctx context.Context, orgID valuer.UUID, key *tel
// Fall back to legacy body column
return []*schema.Column{logsV2Columns["body"]}, nil
case telemetrytypes.FieldContextLog, telemetrytypes.FieldContextUnspecified:
// TODO(Tushar): thread orgID here to evaluate correctly
if key.Name == LogsV2BodyColumn && m.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID)) {
return []*schema.Column{logsV2Columns[messageSubColumn]}, nil
}
@@ -115,7 +113,6 @@ func (m *fieldMapper) getColumn(ctx context.Context, orgID valuer.UUID, key *tel
// check if the key has body JSON search
if strings.HasPrefix(key.Name, telemetrytypes.BodyJSONStringSearchPrefix) {
// Use body_v2 if feature flag is enabled and we have a body condition builder
// TODO(Tushar): thread orgID here to evaluate correctly
if m.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID)) {
// TODO(Piyush): Update this to support multiple JSON columns based on evolutions
// i.e return both the body json and body json promoted and let the evolutions decide which one to use
@@ -433,33 +430,3 @@ func (m *fieldMapper) buildArrayMap(currentNode *telemetrytypes.JSONAccessNode,
return fmt.Sprintf("arrayMap(%s->%s, %s)", currentNode.Alias(), nestedExpr, arrayExpr), nil
}
// searchColumns is the single source of truth for the columns search() fans out
// across, by field context. Body is body_v2 JSON when useJSONBody, else body string.
func searchColumns(fieldContext telemetrytypes.FieldContext, useJSONBody bool) []*schema.Column {
switch fieldContext {
case telemetrytypes.FieldContextLog:
return []*schema.Column{
logsV2Columns[LogsV2SeverityTextColumn],
logsV2Columns[LogsV2TraceIDColumn],
logsV2Columns[LogsV2SpanIDColumn],
}
case telemetrytypes.FieldContextBody:
if useJSONBody {
return []*schema.Column{logsV2Columns[LogsV2BodyV2Column]}
}
return []*schema.Column{logsV2Columns[LogsV2BodyColumn]}
case telemetrytypes.FieldContextAttribute:
return []*schema.Column{
logsV2Columns[LogsV2AttributesStringColumn],
logsV2Columns[LogsV2AttributesNumberColumn],
logsV2Columns[LogsV2AttributesBoolColumn],
}
case telemetrytypes.FieldContextResource:
return []*schema.Column{
logsV2Columns[LogsV2ResourcesStringColumn],
}
default:
return nil
}
}

View File

@@ -115,7 +115,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Single word",
query: "<script>alert('xss')</script>",
shouldPass: false,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '<'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got '<'",
},
// Single word searches with spaces
@@ -181,7 +181,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "[tracing]",
shouldPass: false,
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '['",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got '['",
},
{
category: "Special characters",
@@ -211,7 +211,7 @@ func TestFilterExprLogs(t *testing.T) {
category: "Special characters",
query: "ERROR: cannot execute update() in a read-only context",
shouldPass: false,
expectedErrorContains: "expecting one of {(, AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got ')'",
expectedErrorContains: "expecting one of {(, AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got ')'",
},
{
category: "Special characters",
@@ -633,7 +633,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'and'",
expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
},
{
category: "Keyword conflict",
@@ -641,7 +641,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'or'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'or'",
},
{
category: "Keyword conflict",
@@ -649,7 +649,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got EOF",
expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got EOF",
},
{
category: "Keyword conflict",
@@ -657,7 +657,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'like'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'like'",
},
{
category: "Keyword conflict",
@@ -665,7 +665,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'between'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'between'",
},
{
category: "Keyword conflict",
@@ -673,7 +673,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'in'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'in'",
},
{
category: "Keyword conflict",
@@ -681,7 +681,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'exists'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'exists'",
},
{
category: "Keyword conflict",
@@ -689,7 +689,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'regexp'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'regexp'",
},
{
category: "Keyword conflict",
@@ -697,7 +697,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: []any{},
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'contains'",
expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'contains'",
},
{
category: "Keyword conflict",
@@ -2033,9 +2033,9 @@ func TestFilterExprLogs(t *testing.T) {
expectedErrorContains: "",
},
{category: "Only keywords", query: "AND", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'AND'"},
{category: "Only keywords", query: "OR", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'OR'"},
{category: "Only keywords", query: "NOT", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got EOF"},
{category: "Only keywords", query: "AND", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'AND'"},
{category: "Only keywords", query: "OR", shouldPass: false, expectedErrorContains: "expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'OR'"},
{category: "Only keywords", query: "NOT", shouldPass: false, expectedErrorContains: "expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got EOF"},
{category: "Only functions", query: "has", shouldPass: false, expectedErrorContains: "expecting one of {(, )} but got EOF"},
{category: "Only functions", query: "hasAny", shouldPass: false, expectedErrorContains: "expecting one of {(, )} but got EOF"},
@@ -2177,7 +2177,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "line 1:0 expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'and'",
expectedErrorContains: "line 1:0 expecting one of {(, ), FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'and'",
},
{
category: "Operator keywords as keys",
@@ -2185,7 +2185,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'or'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'or'",
},
{
category: "Operator keywords as keys",
@@ -2193,7 +2193,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "line 1:3 expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got '='",
expectedErrorContains: "line 1:3 expecting one of {(, ), FREETEXT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got '='",
},
{
category: "Operator keywords as keys",
@@ -2201,7 +2201,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'between'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'between'",
},
{
category: "Operator keywords as keys",
@@ -2209,7 +2209,7 @@ func TestFilterExprLogs(t *testing.T) {
shouldPass: false,
expectedQuery: "",
expectedArgs: nil,
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text, search()} but got 'in'",
expectedErrorContains: "line 1:0 expecting one of {(, ), AND, FREETEXT, NOT, boolean, has(), hasAll(), hasAny(), hasToken(), number, quoted text} but got 'in'",
},
// Using function keywords as keys

View File

@@ -1,291 +0,0 @@
package telemetrylogs
import (
"context"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/huandu/go-sqlbuilder"
"github.com/stretchr/testify/require"
)
// searchFanOut returns the WHERE fragment search() fans out to. bodyExpr is the
// body match expression, which differs between the legacy string body and the
// body_v2 JSON column.
func searchFanOut(bodyExpr string) string {
return "(match(LOWER(severity_text), LOWER(?)) OR match(LOWER(trace_id), LOWER(?)) OR match(LOWER(span_id), LOWER(?)) OR " +
bodyExpr + " OR " +
"(arrayExists(x -> match(LOWER(x), LOWER(?)), mapKeys(attributes_string)) OR arrayExists(x -> match(LOWER(x), LOWER(?)), mapValues(attributes_string))) OR " +
"(arrayExists(x -> match(LOWER(x), LOWER(?)), mapKeys(attributes_number)) OR arrayExists(x -> match(LOWER(x), LOWER(?)), arrayMap(x -> toString(x), mapValues(attributes_number)))) OR " +
"(arrayExists(x -> match(LOWER(x), LOWER(?)), mapKeys(attributes_bool)) OR arrayExists(x -> match(LOWER(x), LOWER(?)), arrayMap(x -> toString(x), mapValues(attributes_bool)))) OR " +
"(arrayExists(x -> match(LOWER(x), LOWER(?)), mapKeys(resources_string)) OR arrayExists(x -> match(LOWER(x), LOWER(?)), mapValues(resources_string))))"
}
// searchArgs returns v repeated once per bound parameter search() emits — one per
// searchable column expression (currently 12).
func searchArgs(v any) []any {
const searchColumnParams = 12
args := make([]any, searchColumnParams)
for i := range args {
args[i] = v
}
return args
}
// TestFilterExprSearch covers the search('needle') function, which fans out
// across every searchable column via FilterOperatorSearch.
func TestFilterExprSearch(t *testing.T) {
releaseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
inWindowStart := uint64(releaseTime.Add(-5 * time.Minute).UnixNano())
inWindowEnd := uint64(releaseTime.Add(5 * time.Minute).UnixNano())
legacyBody := "match(LOWER(body), LOWER(?))"
jsonBody := "match(LOWER(toString(body_v2)), LOWER(?))"
serviceNameEq := "(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? " +
"AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)"
testCases := []struct {
name string
query string
searchDisabled bool
jsonBodyEnabled bool
fullTextColumn *telemetrytypes.TelemetryFieldKey
startNs uint64
endNs uint64
shouldPass bool
expectedQuery string
expectedArgs []any
expectWarning bool
expectedErrorContains string
}{
{
name: "quoted, legacy body",
query: "search('error')",
fullTextColumn: DefaultFullTextColumn,
startNs: inWindowStart,
endNs: inWindowEnd,
shouldPass: true,
expectedQuery: "WHERE " + searchFanOut(legacyBody),
expectedArgs: searchArgs("error"),
expectWarning: true,
},
{
name: "quoted, json body",
query: "search('error')",
jsonBodyEnabled: true,
fullTextColumn: DefaultFullTextColumn,
startNs: inWindowStart,
endNs: inWindowEnd,
shouldPass: true,
expectedQuery: "WHERE " + searchFanOut(jsonBody),
expectedArgs: searchArgs("error"),
expectWarning: true,
},
{
name: "bare word",
query: "search(timeout)",
fullTextColumn: DefaultFullTextColumn,
startNs: inWindowStart,
endNs: inWindowEnd,
shouldPass: true,
expectedQuery: "WHERE " + searchFanOut(legacyBody),
expectedArgs: searchArgs("timeout"),
expectWarning: true,
},
{
name: "negated",
query: "NOT search('error')",
fullTextColumn: DefaultFullTextColumn,
startNs: inWindowStart,
endNs: inWindowEnd,
shouldPass: true,
expectedQuery: "WHERE NOT (" + searchFanOut(legacyBody) + ")",
expectedArgs: searchArgs("error"),
expectWarning: true,
},
{
name: "combined with field filter",
query: "search('error') AND service.name=\"api\"",
fullTextColumn: DefaultFullTextColumn,
startNs: inWindowStart,
endNs: inWindowEnd,
shouldPass: true,
expectedQuery: "WHERE (" + searchFanOut(legacyBody) + " AND " + serviceNameEq + ")",
expectedArgs: append(searchArgs("error"), "api"),
expectWarning: true,
},
{
// A wide window is allowed at build time; scan cost is bounded by the
// querier's EXPLAIN ESTIMATE gate, not a window cap in the builder.
name: "wide window builds (estimate gate lives in querier)",
query: "search('error')",
fullTextColumn: DefaultFullTextColumn,
startNs: uint64(releaseTime.Add(-10 * time.Hour).UnixNano()),
endNs: inWindowEnd,
shouldPass: true,
expectedQuery: "WHERE " + searchFanOut(legacyBody),
expectedArgs: searchArgs("error"),
expectWarning: true,
},
{
// search() is keyless and independent of fullTextColumn (which only
// governs bare/quoted free text). It must work even when unset.
name: "independent of full text column",
query: "search('error')",
fullTextColumn: nil,
startNs: inWindowStart,
endNs: inWindowEnd,
shouldPass: true,
expectedQuery: "WHERE " + searchFanOut(legacyBody),
expectedArgs: searchArgs("error"),
expectWarning: true,
},
{
// A context-prefixed bare word must be used as the literal needle, not
// normalized into a field key (Normalize would strip "resource.").
name: "bare word with context prefix is not normalized",
query: "search(resource.deployment)",
fullTextColumn: DefaultFullTextColumn,
startNs: inWindowStart,
endNs: inWindowEnd,
shouldPass: true,
expectedQuery: "WHERE " + searchFanOut(legacyBody),
expectedArgs: searchArgs("resource\\.deployment"),
expectWarning: true,
},
{
// A numeric needle must be the literal digits, not the %v rendering of a
// parsed float64 (which would make search(1000000) scan for "1e+06").
name: "numeric needle is not scientific notation",
query: "search(1000000)",
fullTextColumn: DefaultFullTextColumn,
startNs: inWindowStart,
endNs: inWindowEnd,
shouldPass: true,
expectedQuery: "WHERE " + searchFanOut(legacyBody),
expectedArgs: searchArgs("1000000"),
expectWarning: true,
},
{
name: "too many parameters",
query: "search('error', 'timeout')",
fullTextColumn: DefaultFullTextColumn,
startNs: inWindowStart,
endNs: inWindowEnd,
shouldPass: false,
expectedErrorContains: "currently supports a single argument",
},
{
// The condition builder gates search() on its feature flag and rejects
// while building the fan-out (i.e. during where-clause preparation).
name: "search disabled by flag",
query: "search('error')",
searchDisabled: true,
fullTextColumn: DefaultFullTextColumn,
startNs: inWindowStart,
endNs: inWindowEnd,
shouldPass: false,
expectedErrorContains: "is not enabled",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fl := flaggertest.WithBooleanFlags(t, map[string]bool{
flagger.FeatureAllowLogsSearch.String(): !tc.searchDisabled,
flagger.FeatureUseJSONBody.String(): tc.jsonBodyEnabled,
})
fm := NewFieldMapper(fl)
cb := NewConditionBuilder(fm, fl)
keys := buildCompleteFieldKeyMap(releaseTime)
opts := querybuilder.FilterExprVisitorOpts{
Context: context.Background(),
Logger: instrumentationtest.New().Logger(),
FieldMapper: fm,
ConditionBuilder: cb,
FieldKeys: keys,
FullTextColumn: tc.fullTextColumn,
StartNs: tc.startNs,
EndNs: tc.endNs,
}
clause, err := querybuilder.PrepareWhereClause(tc.query, opts)
if !tc.shouldPass {
require.Error(t, err)
require.True(t, detailContains(err, tc.expectedErrorContains),
"error %v should contain %q", err, tc.expectedErrorContains)
return
}
require.NoError(t, err)
require.False(t, clause.IsEmpty())
sql, args := clause.WhereClause.BuildWithFlavor(sqlbuilder.ClickHouse)
require.Equal(t, tc.expectedQuery, sql)
require.Equal(t, tc.expectedArgs, args)
if tc.expectWarning {
// The visitor only flags the cost guard; the statement builder
// materializes the advisory + budget from config downstream.
require.True(t, clause.RequiresCostGuard)
}
})
}
}
// TestSearchFeatureFlagGate covers the search() flag check end-to-end: the
// condition builder rejects search() when the flag is off, surfacing through Build.
func TestSearchFeatureFlagGate(t *testing.T) {
releaseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
ctx := context.Background()
start := uint64(releaseTime.Add(-5 * time.Minute).UnixMilli())
end := uint64(releaseTime.UnixMilli())
buildSearch := func(t *testing.T, searchEnabled bool) (*qbtypes.Statement, error) {
fl := flaggertest.WithBooleanFlags(t, map[string]bool{
flagger.FeatureAllowLogsSearch.String(): searchEnabled,
})
fm := NewFieldMapper(fl)
cb := NewConditionBuilder(fm, fl)
store := telemetrytypestest.NewMockMetadataStore()
store.KeysMap = buildCompleteFieldKeyMap(releaseTime)
rewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl)
sb := NewLogQueryStatementBuilder(
instrumentationtest.New().ToProviderSettings(),
store, fm, cb, rewriter, DefaultFullTextColumn, GetBodyJSONKey, fl, nil, false, 100000,
)
query := qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "search('error')"},
Limit: 1,
}
return sb.Build(ctx, valuer.UUID{}, start, end, qbtypes.RequestTypeRaw, query, nil)
}
t.Run("disabled", func(t *testing.T) {
_, err := buildSearch(t, false)
require.Error(t, err)
// The condition builder throws; the where-clause visitor wraps it, so the
// "is not enabled" detail rides in the structured errors, not err.Error().
require.True(t, detailContains(err, "is not enabled"),
"error %v should contain %q", err, "is not enabled")
})
t.Run("enabled", func(t *testing.T) {
stmt, err := buildSearch(t, true)
require.NoError(t, err)
require.NotNil(t, stmt.CostGuard)
require.Contains(t, stmt.Warnings, querybuilder.SearchWarning)
})
}

View File

@@ -29,21 +29,12 @@ type logQueryStatementBuilder struct {
fl flagger.Flagger
skipResourceFingerprintEnabled bool
fullTextColumn *telemetrytypes.TelemetryFieldKey
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
searchMaxScanRows int64
fullTextColumn *telemetrytypes.TelemetryFieldKey
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
}
var _ qbtypes.StatementBuilder[qbtypes.LogAggregation] = (*logQueryStatementBuilder)(nil)
type LogQueryStatementBuilderOption func(*logQueryStatementBuilder)
// WithSearchMaxScanRows sets the estimated-rows budget the querier enforces for
// search() statements (0 disables the gate).
func WithSearchMaxScanRows(n int64) LogQueryStatementBuilderOption {
return func(b *logQueryStatementBuilder) { b.searchMaxScanRows = n }
}
func NewLogQueryStatementBuilder(
settings factory.ProviderSettings,
metadataStore telemetrytypes.MetadataStore,
@@ -56,7 +47,6 @@ func NewLogQueryStatementBuilder(
telemetryStore telemetrystore.TelemetryStore,
skipResourceFingerprintEnable bool,
skipResourceFingerprintThreshold uint64,
opts ...LogQueryStatementBuilderOption,
) *logQueryStatementBuilder {
logsSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrylogs")
@@ -73,7 +63,7 @@ func NewLogQueryStatementBuilder(
skipResourceFingerprintThreshold,
)
b := &logQueryStatementBuilder{
return &logQueryStatementBuilder{
logger: logsSettings.Logger(),
metadataStore: metadataStore,
fm: fieldMapper,
@@ -85,10 +75,6 @@ func NewLogQueryStatementBuilder(
fullTextColumn: fullTextColumn,
jsonKeyToKey: jsonKeyToKey,
}
for _, opt := range opts {
opt(b)
}
return b
}
// Build builds a SQL query for logs based on the given parameters.
@@ -107,7 +93,7 @@ func (b *logQueryStatementBuilder) Build(
bodyJSONEnabled := b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID))
keySelectors, warnings := getKeySelectors(query, bodyJSONEnabled)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, keySelectors)
if err != nil {
return nil, err
}
@@ -134,23 +120,9 @@ func (b *logQueryStatementBuilder) Build(
}
stmt.Warnings = append(stmt.Warnings, warnings...)
// The search flag is gated in the condition builder; here the advisory rides on
// the statement so the querier can enforce the scan budget from the CostGuard.
if stmt.CostGuard != nil && stmt.CostGuard.Warning != "" {
stmt.Warnings = append(stmt.Warnings, stmt.CostGuard.Warning)
}
return stmt, nil
}
// costGuardFor builds the cost guard for a scan-heavy (search()) statement,
// pairing the advisory with the configured scan budget. Returns nil otherwise.
func (b *logQueryStatementBuilder) costGuardFor(required bool) *qbtypes.CostGuard {
if !required {
return nil
}
return &qbtypes.CostGuard{Warning: querybuilder.SearchWarning, MaxScanRows: b.searchMaxScanRows}
}
func getKeySelectors(query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation], bodyJSONEnabled bool) ([]*telemetrytypes.FieldKeySelector, []string) {
var keySelectors []*telemetrytypes.FieldKeySelector
var warnings []string
@@ -389,7 +361,6 @@ func (b *logQueryStatementBuilder) buildListQuery(
Args: finalArgs,
Warnings: preparedWhereClause.Warnings,
WarningsDocURL: preparedWhereClause.WarningsDocURL,
CostGuard: b.costGuardFor(preparedWhereClause.RequiresCostGuard),
}
return stmt, nil
@@ -548,7 +519,6 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
Args: finalArgs,
Warnings: preparedWhereClause.Warnings,
WarningsDocURL: preparedWhereClause.WarningsDocURL,
CostGuard: b.costGuardFor(preparedWhereClause.RequiresCostGuard),
}
return stmt, nil
@@ -670,7 +640,6 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
Args: finalArgs,
Warnings: preparedWhereClause.Warnings,
WarningsDocURL: preparedWhereClause.WarningsDocURL,
CostGuard: b.costGuardFor(preparedWhereClause.RequiresCostGuard),
}
return stmt, nil

View File

@@ -46,7 +46,7 @@ func (c *conditionBuilder) ConditionFor(
conds := make([]string, 0, len(keys))
for _, k := range keys {
cond, err := c.conditionForKey(ctx, tsStart, tsEnd, k, operator, value, sb)
cond, err := c.conditionForKey(ctx, orgID, tsStart, tsEnd, k, operator, value, sb)
if err != nil {
return nil, nil, err
}
@@ -57,6 +57,7 @@ func (c *conditionBuilder) ConditionFor(
func (c *conditionBuilder) conditionForKey(
ctx context.Context,
orgID valuer.UUID,
tsStart, tsEnd uint64,
key *telemetrytypes.TelemetryFieldKey,
operator qbtypes.FilterOperator,
@@ -74,13 +75,13 @@ func (c *conditionBuilder) conditionForKey(
value = querybuilder.FormatValueForContains(value)
}
columns, err := c.fm.ColumnFor(ctx, valuer.UUID{}, tsStart, tsEnd, key)
columns, err := c.fm.ColumnFor(ctx, orgID, tsStart, tsEnd, key)
if err != nil {
// if we don't have a column, we can't build a condition for related values
return "", nil
}
fieldExpression, err := c.fm.FieldFor(ctx, valuer.UUID{}, tsStart, tsEnd, key)
fieldExpression, err := c.fm.FieldFor(ctx, orgID, tsStart, tsEnd, key)
if err != nil {
// if we don't have a table field name, we can't build a condition for related values
return "", nil

View File

@@ -379,7 +379,7 @@ func (t *telemetryMetaStore) logsTblStatementToFieldKeys(ctx context.Context) ([
}
// getLogsKeys returns the keys from the spans that match the field selection criteria.
func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors []*telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, bool, error) {
func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, orgID valuer.UUID, fieldKeySelectors []*telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, bool, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalLogs.StringValue(),
instrumentationtypes.CodeNamespace: "metadata",
@@ -426,8 +426,7 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
}
// body keys are gated behind the feature flag
// TODO(Tushar): thread orgID here to evaluate correctly
queryBodyTable = queryBodyTable && t.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
queryBodyTable = queryBodyTable && t.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID))
// requestedFieldKeySelectors is the set of names the user explicitly asked for.
// Used to ensure a name that is both a parent path AND a directly requested field still surfaces
@@ -687,8 +686,7 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
}
// enrich body keys with promoted paths, indexes, and JSON access plans
// TODO(Tushar): thread orgID here to evaluate correctly
if t.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) {
if t.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID)) {
if err := t.enrichJSONKeys(ctx, fieldKeySelectors, keys, parentTypes); err != nil {
return nil, false, err
}
@@ -1215,7 +1213,7 @@ func matchesSelectorName(selectorName, target string, matchType telemetrytypes.F
}
}
func (t *telemetryMetaStore) GetKeys(ctx context.Context, fieldKeySelector *telemetrytypes.FieldKeySelector) (map[string][]*telemetrytypes.TelemetryFieldKey, bool, error) {
func (t *telemetryMetaStore) GetKeys(ctx context.Context, orgID valuer.UUID, fieldKeySelector *telemetrytypes.FieldKeySelector) (map[string][]*telemetrytypes.TelemetryFieldKey, bool, error) {
var keys []*telemetrytypes.TelemetryFieldKey
var complete = true
var err error
@@ -1232,7 +1230,7 @@ func (t *telemetryMetaStore) GetKeys(ctx context.Context, fieldKeySelector *tele
if fieldKeySelector.Source == telemetrytypes.SourceAudit {
keys, complete, err = t.getAuditKeys(ctx, selectors)
} else {
keys, complete, err = t.getLogsKeys(ctx, selectors)
keys, complete, err = t.getLogsKeys(ctx, orgID, selectors)
}
case telemetrytypes.SignalMetrics:
if fieldKeySelector.Source == telemetrytypes.SourceMeter {
@@ -1249,7 +1247,7 @@ func (t *telemetryMetaStore) GetKeys(ctx context.Context, fieldKeySelector *tele
keys = append(keys, tracesKeys...)
// get logs keys
logsKeys, logsComplete, err := t.getLogsKeys(ctx, selectors)
logsKeys, logsComplete, err := t.getLogsKeys(ctx, orgID, selectors)
if err != nil {
return nil, false, err
}
@@ -1279,7 +1277,7 @@ func (t *telemetryMetaStore) GetKeys(ctx context.Context, fieldKeySelector *tele
return mapOfKeys, complete, nil
}
func (t *telemetryMetaStore) GetKeysMulti(ctx context.Context, fieldKeySelectors []*telemetrytypes.FieldKeySelector) (map[string][]*telemetrytypes.TelemetryFieldKey, bool, error) {
func (t *telemetryMetaStore) GetKeysMulti(ctx context.Context, orgID valuer.UUID, fieldKeySelectors []*telemetrytypes.FieldKeySelector) (map[string][]*telemetrytypes.TelemetryFieldKey, bool, error) {
logsSelectors := []*telemetrytypes.FieldKeySelector{}
auditSelectors := []*telemetrytypes.FieldKeySelector{}
@@ -1310,7 +1308,7 @@ func (t *telemetryMetaStore) GetKeysMulti(ctx context.Context, fieldKeySelectors
}
}
logsKeys, logsComplete, err := t.getLogsKeys(ctx, logsSelectors)
logsKeys, logsComplete, err := t.getLogsKeys(ctx, orgID, logsSelectors)
if err != nil {
return nil, false, err
}
@@ -1357,15 +1355,15 @@ func (t *telemetryMetaStore) GetKeysMulti(ctx context.Context, fieldKeySelectors
return mapOfKeys, complete, nil
}
func (t *telemetryMetaStore) GetKey(ctx context.Context, fieldKeySelector *telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, error) {
keys, _, err := t.GetKeys(ctx, fieldKeySelector)
func (t *telemetryMetaStore) GetKey(ctx context.Context, orgID valuer.UUID, fieldKeySelector *telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, error) {
keys, _, err := t.GetKeys(ctx, orgID, fieldKeySelector)
if err != nil {
return nil, err
}
return keys[fieldKeySelector.Name], nil
}
func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSelector *telemetrytypes.FieldValueSelector) ([]string, bool, error) {
func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, orgID valuer.UUID, fieldValueSelector *telemetrytypes.FieldValueSelector) ([]string, bool, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: fieldValueSelector.Signal.StringValue(),
instrumentationtypes.CodeNamespace: "metadata",
@@ -1384,18 +1382,18 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSel
FieldDataType: fieldValueSelector.FieldDataType,
}
selectColumn, err := t.fm.FieldFor(ctx, valuer.UUID{}, 0, 0, key)
selectColumn, err := t.fm.FieldFor(ctx, orgID, 0, 0, key)
if err != nil {
// we don't have a explicit column to select from the related metadata table
// so we will select either from resource_attributes or attributes table
// in that order
resourceColumn, _ := t.fm.FieldFor(ctx, valuer.UUID{}, 0, 0, &telemetrytypes.TelemetryFieldKey{
resourceColumn, _ := t.fm.FieldFor(ctx, orgID, 0, 0, &telemetrytypes.TelemetryFieldKey{
Name: key.Name,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
})
attributeColumn, _ := t.fm.FieldFor(ctx, valuer.UUID{}, 0, 0, &telemetrytypes.TelemetryFieldKey{
attributeColumn, _ := t.fm.FieldFor(ctx, orgID, 0, 0, &telemetrytypes.TelemetryFieldKey{
Name: key.Name,
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
@@ -1410,7 +1408,7 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSel
for _, keySelector := range keySelectors {
keySelector.Signal = fieldValueSelector.Signal
}
keys, _, err := t.GetKeysMulti(ctx, keySelectors)
keys, _, err := t.GetKeysMulti(ctx, orgID, keySelectors)
if err != nil {
return nil, false, err
}
@@ -1446,20 +1444,20 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSel
// search on attributes
key.FieldContext = telemetrytypes.FieldContextAttribute
attrConds, _, err := t.conditionBuilder.ConditionFor(ctx, valuer.UUID{}, 0, 0, key, []*telemetrytypes.TelemetryFieldKey{key}, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
attrConds, _, err := t.conditionBuilder.ConditionFor(ctx, orgID, 0, 0, key, []*telemetrytypes.TelemetryFieldKey{key}, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
if err == nil {
conds = append(conds, attrConds...)
}
// search on resource
key.FieldContext = telemetrytypes.FieldContextResource
resourceConds, _, err := t.conditionBuilder.ConditionFor(ctx, valuer.UUID{}, 0, 0, key, []*telemetrytypes.TelemetryFieldKey{key}, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
resourceConds, _, err := t.conditionBuilder.ConditionFor(ctx, orgID, 0, 0, key, []*telemetrytypes.TelemetryFieldKey{key}, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
if err == nil {
conds = append(conds, resourceConds...)
}
key.FieldContext = origContext
} else {
keyConds, _, err := t.conditionBuilder.ConditionFor(ctx, valuer.UUID{}, 0, 0, key, []*telemetrytypes.TelemetryFieldKey{key}, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
keyConds, _, err := t.conditionBuilder.ConditionFor(ctx, orgID, 0, 0, key, []*telemetrytypes.TelemetryFieldKey{key}, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
if err == nil {
conds = append(conds, keyConds...)
}
@@ -1513,8 +1511,8 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSel
return attributeValues, complete, nil
}
func (t *telemetryMetaStore) GetRelatedValues(ctx context.Context, fieldValueSelector *telemetrytypes.FieldValueSelector) ([]string, bool, error) {
return t.getRelatedValues(ctx, fieldValueSelector)
func (t *telemetryMetaStore) GetRelatedValues(ctx context.Context, orgID valuer.UUID, fieldValueSelector *telemetrytypes.FieldValueSelector) ([]string, bool, error) {
return t.getRelatedValues(ctx, orgID, fieldValueSelector)
}
func (t *telemetryMetaStore) getSpanFieldValues(ctx context.Context, fieldValueSelector *telemetrytypes.FieldValueSelector) (*telemetrytypes.TelemetryFieldValues, bool, error) {

View File

@@ -54,7 +54,7 @@ func (b *meterQueryStatementBuilder) Build(
variables map[string]qbtypes.VariableItem,
) (*qbtypes.Statement, error) {
keySelectors := telemetrymetrics.GetKeySelectors(query)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, keySelectors)
if err != nil {
return nil, err
}

View File

@@ -24,6 +24,7 @@ func NewConditionBuilder(fm qbtypes.FieldMapper) *conditionBuilder {
func (c *conditionBuilder) conditionFor(
ctx context.Context,
orgID valuer.UUID,
startNs uint64,
endNs uint64,
key *telemetrytypes.TelemetryFieldKey,
@@ -36,7 +37,7 @@ func (c *conditionBuilder) conditionFor(
value = querybuilder.FormatValueForContains(value)
}
fieldExpression, err := c.fm.FieldFor(ctx, valuer.UUID{}, startNs, endNs, key)
fieldExpression, err := c.fm.FieldFor(ctx, orgID, startNs, endNs, key)
if err != nil {
return "", err
}
@@ -154,7 +155,7 @@ func (c *conditionBuilder) ConditionFor(
sb *sqlbuilder.SelectBuilder,
) ([]string, []string, error) {
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for metrics.
// has/hasAny/hasAll/hasToken are logs-body-only; reject for metrics.
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
return nil, nil, err
}
@@ -170,7 +171,7 @@ func (c *conditionBuilder) ConditionFor(
conds := make([]string, 0, len(keys))
for _, k := range keys {
cond, err := c.conditionForKey(ctx, startNs, endNs, k, operator, value, sb)
cond, err := c.conditionForKey(ctx, orgID, startNs, endNs, k, operator, value, sb)
if err != nil {
return nil, nil, err
}
@@ -181,6 +182,7 @@ func (c *conditionBuilder) ConditionFor(
func (c *conditionBuilder) conditionForKey(
ctx context.Context,
orgID valuer.UUID,
startNs uint64,
endNs uint64,
key *telemetrytypes.TelemetryFieldKey,
@@ -188,7 +190,7 @@ func (c *conditionBuilder) conditionForKey(
value any,
sb *sqlbuilder.SelectBuilder,
) (string, error) {
condition, err := c.conditionFor(ctx, startNs, endNs, key, operator, value, sb)
condition, err := c.conditionFor(ctx, orgID, startNs, endNs, key, operator, value, sb)
if err != nil {
return "", err
}

View File

@@ -99,7 +99,7 @@ func (b *MetricQueryStatementBuilder) Build(
variables map[string]qbtypes.VariableItem,
) (*qbtypes.Statement, error) {
keySelectors := GetKeySelectors(query)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, keySelectors)
if err != nil {
return nil, err
}

View File

@@ -100,7 +100,7 @@ func (b *resourceFilterStatementBuilder[T]) Build(
q.From(fmt.Sprintf("%s.%s", b.dbName, b.tableName))
keySelectors := b.getKeySelectors(query)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, keySelectors)
if err != nil {
return nil, err
}

View File

@@ -33,6 +33,7 @@ func NewConditionBuilder(fm qbtypes.FieldMapper) *conditionBuilder {
func (c *conditionBuilder) conditionFor(
ctx context.Context,
orgID valuer.UUID,
startNs uint64,
endNs uint64,
key *telemetrytypes.TelemetryFieldKey,
@@ -46,13 +47,13 @@ func (c *conditionBuilder) conditionFor(
}
// first, locate the raw column type (so we can choose the right EXISTS logic)
columns, err := c.fm.ColumnFor(ctx, valuer.UUID{}, startNs, endNs, key)
columns, err := c.fm.ColumnFor(ctx, orgID, startNs, endNs, key)
if err != nil {
return "", err
}
// then ask the mapper for the actual SQL reference
fieldExpression, err := c.fm.FieldFor(ctx, valuer.UUID{}, startNs, endNs, key)
fieldExpression, err := c.fm.FieldFor(ctx, orgID, startNs, endNs, key)
if err != nil {
return "", err
}
@@ -306,7 +307,7 @@ func (c *conditionBuilder) conditionsForKeys(
sb *sqlbuilder.SelectBuilder,
) ([]string, []string, error) {
// has/hasAny/hasAll/hasToken/search are logs-only functions; reject for traces.
// has/hasAny/hasAll/hasToken are logs-body-only; reject for traces.
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
return nil, nil, err
}
@@ -345,7 +346,7 @@ func (c *conditionBuilder) conditionsForKeys(
conds := make([]string, 0, len(keys))
for _, k := range keys {
cond, err := c.conditionForKey(ctx, startNs, endNs, k, operator, value, sb)
cond, err := c.conditionForKey(ctx, orgID, startNs, endNs, k, operator, value, sb)
if err != nil {
return nil, nil, err
}
@@ -356,6 +357,7 @@ func (c *conditionBuilder) conditionsForKeys(
func (c *conditionBuilder) conditionForKey(
ctx context.Context,
orgID valuer.UUID,
startNs uint64,
endNs uint64,
key *telemetrytypes.TelemetryFieldKey,
@@ -367,14 +369,14 @@ func (c *conditionBuilder) conditionForKey(
return c.buildSpanScopeCondition(key, operator, value, startNs)
}
condition, err := c.conditionFor(ctx, startNs, endNs, key, operator, value, sb)
condition, err := c.conditionFor(ctx, orgID, startNs, endNs, key, operator, value, sb)
if err != nil {
return "", err
}
if operator.AddDefaultExistsFilter() {
// skip adding exists filter for intrinsic fields
field, _ := c.fm.FieldFor(ctx, valuer.UUID{}, startNs, endNs, key)
field, _ := c.fm.FieldFor(ctx, orgID, startNs, endNs, key)
if slices.Contains(maps.Keys(IntrinsicFields), field) ||
slices.Contains(maps.Keys(IntrinsicFieldsDeprecated), field) ||
slices.Contains(maps.Keys(CalculatedFields), field) ||
@@ -382,7 +384,7 @@ func (c *conditionBuilder) conditionForKey(
return condition, nil
}
existsCondition, err := c.conditionFor(ctx, startNs, endNs, key, qbtypes.FilterOperatorExists, nil, sb)
existsCondition, err := c.conditionFor(ctx, orgID, startNs, endNs, key, qbtypes.FilterOperatorExists, nil, sb)
if err != nil {
return "", err
}

View File

@@ -97,7 +97,7 @@ func (b *traceQueryStatementBuilder) Build(
// fields can carry keys that need evolutions, so fetch keys after that.
keySelectors := getKeySelectors(query)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
keys, _, err := b.metadataStore.GetKeysMulti(ctx, orgID, keySelectors)
if err != nil {
return nil, err
}

View File

@@ -213,7 +213,7 @@ func (b *traceOperatorCTEBuilder) buildQueryCTE(ctx context.Context, queryName s
keySelectors := getKeySelectors(*query)
b.stmtBuilder.logger.DebugContext(ctx, "Key selectors for query", slog.String("query_name", queryName), slog.Any("key_selectors", keySelectors))
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, keySelectors)
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, b.orgID, keySelectors)
if err != nil {
return "", err
}
@@ -443,7 +443,7 @@ func (b *traceOperatorCTEBuilder) buildFinalQuery(ctx context.Context, selectFro
}
keySelectors := b.getKeySelectors()
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, keySelectors)
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, b.orgID, keySelectors)
if err != nil {
return nil, err
}

Some files were not shown because too many files have changed in this diff Show More