mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-10 03:32:04 +00:00
Compare commits
14 Commits
feat/sessi
...
not-fix-ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c8ce2a749 | ||
|
|
87ce197631 | ||
|
|
3cc5a24a4b | ||
|
|
9b8a892079 | ||
|
|
396e0cdc2d | ||
|
|
c838d7e2d4 | ||
|
|
1a193fb1a9 | ||
|
|
88dff3f552 | ||
|
|
5bb6d78c42 | ||
|
|
369f77977d | ||
|
|
836605def5 | ||
|
|
cc80923265 | ||
|
|
92e5986af2 | ||
|
|
912a34da8d |
@@ -26,7 +26,7 @@ const config: Config.InitialOptions = {
|
||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
|
||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
"@signozhq/design-tokens": "1.1.4",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/sonner": "0.1.0",
|
||||
"@signozhq/table": "0.3.7",
|
||||
"@signozhq/tooltip": "0.0.2",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
25
frontend/src/api/settings/getRetentionV2.ts
Normal file
25
frontend/src/api/settings/getRetentionV2.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ApiV2Instance } from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/settings/getRetention';
|
||||
|
||||
// Only works for logs
|
||||
const getRetentionV2 = async (): Promise<
|
||||
SuccessResponseV2<PayloadProps<'logs'>>
|
||||
> => {
|
||||
try {
|
||||
const response = await ApiV2Instance.get<PayloadProps<'logs'>>(
|
||||
`/settings/ttl`,
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getRetentionV2;
|
||||
@@ -1,14 +1,14 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/settings/setRetention';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadPropsV2, Props } from 'types/api/settings/setRetention';
|
||||
|
||||
const setRetention = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
): Promise<SuccessResponseV2<PayloadPropsV2>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>(
|
||||
const response = await axios.post<PayloadPropsV2>(
|
||||
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
|
||||
props.coldStorage
|
||||
? `&coldStorage=${props.coldStorage}&toColdDuration=${props.toColdDuration}`
|
||||
@@ -17,13 +17,11 @@ const setRetention = async (
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data,
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
32
frontend/src/api/settings/setRetentionV2.ts
Normal file
32
frontend/src/api/settings/setRetentionV2.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ApiV2Instance } from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadPropsV2, PropsV2 } from 'types/api/settings/setRetention';
|
||||
|
||||
const setRetentionV2 = async ({
|
||||
type,
|
||||
defaultTTLDays,
|
||||
coldStorageVolume,
|
||||
coldStorageDuration,
|
||||
ttlConditions,
|
||||
}: PropsV2): Promise<SuccessResponseV2<PayloadPropsV2>> => {
|
||||
try {
|
||||
const response = await ApiV2Instance.post<PayloadPropsV2>(`/settings/ttl`, {
|
||||
type,
|
||||
defaultTTLDays,
|
||||
coldStorageVolume,
|
||||
coldStorageDuration,
|
||||
ttlConditions,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default setRetentionV2;
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -39,7 +40,7 @@ import {
|
||||
TextSelect,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCopyToClipboard, useLocation } from 'react-use';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -94,6 +95,8 @@ function LogDetailInner({
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { onLogCopy } = useCopyLogLink(log?.id);
|
||||
|
||||
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
|
||||
|
||||
const handleModeChange = (e: RadioChangeEvent): void => {
|
||||
@@ -146,6 +149,34 @@ function LogDetailInner({
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
||||
};
|
||||
|
||||
const handleQueryExpressionChange = useCallback(
|
||||
(value: string, queryIndex: number) => {
|
||||
// update the query at the given index
|
||||
setContextQuery((prev) => {
|
||||
if (!prev) return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
builder: {
|
||||
...prev.builder,
|
||||
queryData: prev.builder.queryData.map((query, idx) =>
|
||||
idx === queryIndex
|
||||
? {
|
||||
...query,
|
||||
filter: {
|
||||
...query.filter,
|
||||
expression: value,
|
||||
},
|
||||
}
|
||||
: query,
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRunQuery = (expression: string): void => {
|
||||
let updatedContextQuery = cloneDeep(contextQuery);
|
||||
|
||||
@@ -305,11 +336,19 @@ function LogDetailInner({
|
||||
onClick={handleFilterVisible}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tooltip title="Copy Log Link" placement="left" aria-label="Copy Log Link">
|
||||
<Button
|
||||
className="action-btn"
|
||||
icon={<Copy size={16} />}
|
||||
onClick={onLogCopy}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{isFilterVisible && contextQuery?.builder.queryData[0] && (
|
||||
<div className="log-detail-drawer-query-container">
|
||||
<QuerySearch
|
||||
onChange={(): void => {}}
|
||||
onChange={(value): void => handleQueryExpressionChange(value, 0)}
|
||||
dataSource={DataSource.LOGS}
|
||||
queryData={contextQuery?.builder.queryData[0]}
|
||||
onRun={handleRunQuery}
|
||||
|
||||
@@ -56,6 +56,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
.map(({ name }) => ({
|
||||
title: name,
|
||||
dataIndex: name,
|
||||
accessorKey: name,
|
||||
id: name.toLowerCase().replace(/\./g, '_'),
|
||||
key: name,
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
@@ -83,7 +85,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
// We do not need any title and data index for the log state indicator
|
||||
title: '',
|
||||
dataIndex: '',
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
key: 'state-indicator',
|
||||
accessorKey: 'state-indicator',
|
||||
id: 'state-indicator',
|
||||
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
children: (
|
||||
<div className={cx('state-indicator', fontSize)}>
|
||||
@@ -101,6 +106,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
title: 'timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
accessorKey: 'timestamp',
|
||||
id: 'timestamp',
|
||||
// https://github.com/ant-design/ant-design/discussions/36886
|
||||
render: (
|
||||
field: string | number,
|
||||
@@ -135,6 +142,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
title: 'body',
|
||||
dataIndex: 'body',
|
||||
key: 'body',
|
||||
accessorKey: 'body',
|
||||
id: 'body',
|
||||
render: (
|
||||
field: string | number,
|
||||
): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
|
||||
@@ -33,4 +33,5 @@ export enum LOCALSTORAGE {
|
||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
|
||||
SHOW_FREQUENCY_CHART = 'SHOW_FREQUENCY_CHART',
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import './AppLayout.styles.scss';
|
||||
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Toaster } from '@signozhq/sonner';
|
||||
import { Flex } from 'antd';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
@@ -852,6 +853,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
{showChangelogModal && changelog && (
|
||||
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
|
||||
)}
|
||||
|
||||
<Toaster />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.explorer-options-container {
|
||||
position: fixed;
|
||||
bottom: 8px;
|
||||
bottom: 0px;
|
||||
left: calc(50% + 240px);
|
||||
transform: translate(calc(-50% - 120px), 0);
|
||||
transition: left 0.2s linear;
|
||||
@@ -74,6 +74,7 @@
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
z-index: 1;
|
||||
|
||||
.ant-select-selector {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
.explorer-show-btn {
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
border-radius: 6px 6px 0px 0px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: rgba(22, 24, 29, 0.4);
|
||||
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
|
||||
|
||||
@@ -2,22 +2,30 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Col, Divider, Modal, Row, Spin, Typography } from 'antd';
|
||||
import setRetentionApi from 'api/settings/setRetention';
|
||||
import setRetentionApiV2 from 'api/settings/setRetentionV2';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import find from 'lodash-es/find';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useInterval } from 'react-use';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
ErrorResponse,
|
||||
ErrorResponseV2,
|
||||
SuccessResponse,
|
||||
SuccessResponseV2,
|
||||
} from 'types/api';
|
||||
import {
|
||||
IDiskType,
|
||||
PayloadProps as GetDisksPayload,
|
||||
} from 'types/api/disks/getDisks';
|
||||
import APIError from 'types/api/error';
|
||||
import { TTTLType } from 'types/api/settings/common';
|
||||
import {
|
||||
PayloadPropsLogs as GetRetentionPeriodLogsPayload,
|
||||
@@ -127,7 +135,7 @@ function GeneralSettings({
|
||||
|
||||
useEffect(() => {
|
||||
if (logsCurrentTTLValues) {
|
||||
setLogsTotalRetentionPeriod(logsCurrentTTLValues.logs_ttl_duration_hrs);
|
||||
setLogsTotalRetentionPeriod(logsCurrentTTLValues.default_ttl_days * 24);
|
||||
setLogsS3RetentionPeriod(
|
||||
logsCurrentTTLValues.logs_move_ttl_duration_hrs
|
||||
? logsCurrentTTLValues.logs_move_ttl_duration_hrs
|
||||
@@ -336,20 +344,40 @@ function GeneralSettings({
|
||||
}
|
||||
try {
|
||||
onPostApiLoadingHandler(type);
|
||||
const setTTLResponse = await setRetentionApi({
|
||||
type,
|
||||
totalDuration: `${apiCallTotalRetention || -1}h`,
|
||||
coldStorage: s3Enabled ? 's3' : null,
|
||||
toColdDuration: `${apiCallS3Retention || -1}h`,
|
||||
});
|
||||
let hasSetTTLFailed = false;
|
||||
if (setTTLResponse.statusCode === 409) {
|
||||
|
||||
try {
|
||||
if (type === 'logs') {
|
||||
await setRetentionApiV2({
|
||||
type,
|
||||
defaultTTLDays: apiCallTotalRetention ? apiCallTotalRetention / 24 : -1, // convert Hours to days
|
||||
coldStorageVolume: '',
|
||||
coldStorageDuration: 0,
|
||||
ttlConditions: [],
|
||||
});
|
||||
} else {
|
||||
await setRetentionApi({
|
||||
type,
|
||||
totalDuration: `${apiCallTotalRetention || -1}h`,
|
||||
coldStorage: s3Enabled ? 's3' : null,
|
||||
toColdDuration: `${apiCallS3Retention || -1}h`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
hasSetTTLFailed = true;
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('retention_request_race_condition'),
|
||||
placement: 'topRight',
|
||||
});
|
||||
if ((error as APIError).getHttpStatusCode() === StatusCodes.CONFLICT) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('retention_request_race_condition'),
|
||||
placement: 'topRight',
|
||||
});
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: (error as APIError).getErrorMessage(),
|
||||
placement: 'topRight',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'metrics') {
|
||||
@@ -376,11 +404,14 @@ function GeneralSettings({
|
||||
logsTtlValuesRefetch();
|
||||
if (!hasSetTTLFailed)
|
||||
// Updates the currentTTL Values in order to avoid pushing the same values.
|
||||
setLogsCurrentTTLValues({
|
||||
setLogsCurrentTTLValues((prev) => ({
|
||||
...prev,
|
||||
logs_ttl_duration_hrs: logsTotalRetentionPeriod || -1,
|
||||
logs_move_ttl_duration_hrs: logsS3RetentionPeriod || -1,
|
||||
status: '',
|
||||
});
|
||||
default_ttl_days: logsTotalRetentionPeriod
|
||||
? logsTotalRetentionPeriod / 24 // convert Hours to days
|
||||
: -1,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
@@ -399,6 +430,7 @@ function GeneralSettings({
|
||||
const renderConfig = [
|
||||
{
|
||||
name: 'Metrics',
|
||||
type: 'metrics',
|
||||
retentionFields: [
|
||||
{
|
||||
name: t('total_retention_period'),
|
||||
@@ -440,6 +472,7 @@ function GeneralSettings({
|
||||
},
|
||||
{
|
||||
name: 'Traces',
|
||||
type: 'traces',
|
||||
retentionFields: [
|
||||
{
|
||||
name: t('total_retention_period'),
|
||||
@@ -479,6 +512,7 @@ function GeneralSettings({
|
||||
},
|
||||
{
|
||||
name: 'Logs',
|
||||
type: 'logs',
|
||||
retentionFields: [
|
||||
{
|
||||
name: t('total_retention_period'),
|
||||
@@ -537,6 +571,7 @@ function GeneralSettings({
|
||||
/>
|
||||
{category.retentionFields.map((retentionField) => (
|
||||
<Retention
|
||||
type={category.type as TTTLType}
|
||||
key={retentionField.name}
|
||||
text={retentionField.name}
|
||||
retentionValue={retentionField.value}
|
||||
@@ -625,7 +660,7 @@ interface GeneralSettingsProps {
|
||||
ErrorResponse | SuccessResponse<GetRetentionPeriodTracesPayload>
|
||||
>['refetch'];
|
||||
logsTtlValuesRefetch: UseQueryResult<
|
||||
ErrorResponse | SuccessResponse<GetRetentionPeriodLogsPayload>
|
||||
ErrorResponseV2 | SuccessResponseV2<GetRetentionPeriodLogsPayload>
|
||||
>['refetch'];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { TTTLType } from 'types/api/settings/common';
|
||||
|
||||
import {
|
||||
Input,
|
||||
@@ -20,11 +21,13 @@ import {
|
||||
convertHoursValueToRelevantUnit,
|
||||
SettingPeriod,
|
||||
TimeUnits,
|
||||
TimeUnitsValues,
|
||||
} from './utils';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
function Retention({
|
||||
type,
|
||||
retentionValue,
|
||||
setRetentionValue,
|
||||
text,
|
||||
@@ -50,7 +53,9 @@ function Retention({
|
||||
if (!interacted.current) setSelectTimeUnit(initialTimeUnitValue);
|
||||
}, [initialTimeUnitValue]);
|
||||
|
||||
const menuItems = TimeUnits.map((option) => (
|
||||
const menuItems = TimeUnits.filter((option) =>
|
||||
type === 'logs' ? option.value !== TimeUnitsValues.hr : true,
|
||||
).map((option) => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.key}
|
||||
</Option>
|
||||
@@ -124,6 +129,7 @@ function Retention({
|
||||
}
|
||||
|
||||
interface RetentionProps {
|
||||
type: TTTLType;
|
||||
retentionValue: number | null;
|
||||
text: string;
|
||||
setRetentionValue: Dispatch<SetStateAction<number | null>>;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Typography } from 'antd';
|
||||
import getDisks from 'api/disks/getDisks';
|
||||
import getRetentionPeriodApi from 'api/settings/getRetention';
|
||||
import getRetentionPeriodApiV2 from 'api/settings/getRetentionV2';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { TTTLType } from 'types/api/settings/common';
|
||||
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
|
||||
|
||||
@@ -15,6 +17,10 @@ type TRetentionAPIReturn<T extends TTTLType> = Promise<
|
||||
SuccessResponse<GetRetentionPeriodAPIPayloadProps<T>> | ErrorResponse
|
||||
>;
|
||||
|
||||
type TRetentionAPIReturnV2<T extends TTTLType> = Promise<
|
||||
SuccessResponseV2<GetRetentionPeriodAPIPayloadProps<T>>
|
||||
>;
|
||||
|
||||
function GeneralSettings(): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
const { user } = useAppContext();
|
||||
@@ -36,7 +42,7 @@ function GeneralSettings(): JSX.Element {
|
||||
queryKey: ['getRetentionPeriodApiTraces', user?.accessJwt],
|
||||
},
|
||||
{
|
||||
queryFn: (): TRetentionAPIReturn<'logs'> => getRetentionPeriodApi('logs'),
|
||||
queryFn: (): TRetentionAPIReturnV2<'logs'> => getRetentionPeriodApiV2(), // Only works for logs
|
||||
queryKey: ['getRetentionPeriodApiLogs', user?.accessJwt],
|
||||
},
|
||||
{
|
||||
@@ -70,7 +76,7 @@ function GeneralSettings(): JSX.Element {
|
||||
if (getRetentionPeriodLogsApiResponse.isError || getDisksResponse.isError) {
|
||||
return (
|
||||
<Typography>
|
||||
{getRetentionPeriodLogsApiResponse.data?.error ||
|
||||
{(getRetentionPeriodLogsApiResponse.error as APIError).getErrorMessage() ||
|
||||
getDisksResponse.data?.error ||
|
||||
t('something_went_wrong')}
|
||||
</Typography>
|
||||
@@ -86,7 +92,7 @@ function GeneralSettings(): JSX.Element {
|
||||
getRetentionPeriodTracesApiResponse.isLoading ||
|
||||
!getRetentionPeriodTracesApiResponse.data?.payload ||
|
||||
getRetentionPeriodLogsApiResponse.isLoading ||
|
||||
!getRetentionPeriodLogsApiResponse.data?.payload
|
||||
!getRetentionPeriodLogsApiResponse.data?.data
|
||||
) {
|
||||
return <Spinner tip="Loading.." height="70vh" />;
|
||||
}
|
||||
@@ -99,7 +105,7 @@ function GeneralSettings(): JSX.Element {
|
||||
metricsTtlValuesRefetch: getRetentionPeriodMetricsApiResponse.refetch,
|
||||
tracesTtlValuesPayload: getRetentionPeriodTracesApiResponse.data?.payload,
|
||||
tracesTtlValuesRefetch: getRetentionPeriodTracesApiResponse.refetch,
|
||||
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.payload,
|
||||
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.data,
|
||||
logsTtlValuesRefetch: getRetentionPeriodLogsApiResponse.refetch,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -5,19 +5,26 @@ export interface ITimeUnit {
|
||||
key: string;
|
||||
multiplier: number;
|
||||
}
|
||||
|
||||
export enum TimeUnitsValues {
|
||||
hr = 'hr',
|
||||
day = 'day',
|
||||
month = 'month',
|
||||
}
|
||||
|
||||
export const TimeUnits: ITimeUnit[] = [
|
||||
{
|
||||
value: 'hr',
|
||||
value: TimeUnitsValues.hr,
|
||||
key: 'Hours',
|
||||
multiplier: 1,
|
||||
},
|
||||
{
|
||||
value: 'day',
|
||||
value: TimeUnitsValues.day,
|
||||
key: 'Days',
|
||||
multiplier: 1 / 24,
|
||||
},
|
||||
{
|
||||
value: 'month',
|
||||
value: TimeUnitsValues.month,
|
||||
key: 'Months',
|
||||
multiplier: 1 / (24 * 30),
|
||||
},
|
||||
|
||||
@@ -774,6 +774,13 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
),
|
||||
children: (
|
||||
<div className="ingestion-key-info-container">
|
||||
<Row>
|
||||
<Col span={6}> ID </Col>
|
||||
<Col span={12}>
|
||||
<Typography.Text>{APIKey.id}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={6}> Created on </Col>
|
||||
<Col span={12}>
|
||||
|
||||
@@ -14,4 +14,7 @@ export const ContentWrapper = styled(Row)`
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding-bottom: 4rem;
|
||||
padding-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
`;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CardStyled = styled(Card)`
|
||||
border: none !important;
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
.ant-card-body {
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
padding: 0 16px 16px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,25 @@
|
||||
.logs-frequency-chart-container {
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
border-bottom: 1px solid #262626;
|
||||
|
||||
.ant-card-body {
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
padding: 0 16px 16px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
|
||||
.logs-frequency-chart-loading {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.logs-frequency-chart-container {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import './LogsExplorerChart.styles.scss';
|
||||
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -15,7 +17,6 @@ import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
|
||||
import { CardStyled } from './LogsExplorerChart.styled';
|
||||
import { getColorsForSeverityLabels } from './utils';
|
||||
|
||||
function LogsExplorerChart({
|
||||
@@ -100,9 +101,11 @@ function LogsExplorerChart({
|
||||
);
|
||||
|
||||
return (
|
||||
<CardStyled className={className}>
|
||||
<div className={`${className} logs-frequency-chart-container`}>
|
||||
{isLoading ? (
|
||||
<Spinner size="default" height="100%" />
|
||||
<div className="logs-frequency-chart-loading">
|
||||
<Spinner size="default" height="100%" />
|
||||
</div>
|
||||
) : (
|
||||
<Graph
|
||||
name="logsExplorerChart"
|
||||
@@ -115,7 +118,7 @@ function LogsExplorerChart({
|
||||
maxTime={chartMaxTime}
|
||||
/>
|
||||
)}
|
||||
</CardStyled>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ColumnDef, DataTable, Row } from '@signozhq/table';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import LogStateIndicator from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import { getLogIndicatorTypeForTable } from 'components/Logs/LogStateIndicator/utils';
|
||||
import { useTableView } from 'components/Logs/TableView/useTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import useDragColumns from 'hooks/useDragColumns';
|
||||
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
interface ColumnViewProps {
|
||||
logs: ILog[];
|
||||
onLoadMore: () => void;
|
||||
selectedFields: any[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
|
||||
isFrequencyChartVisible: boolean;
|
||||
options: {
|
||||
maxLinesPerRow: number;
|
||||
fontSize: FontSize;
|
||||
};
|
||||
}
|
||||
|
||||
function ColumnView({
|
||||
logs,
|
||||
onLoadMore,
|
||||
selectedFields,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isFrequencyChartVisible,
|
||||
options,
|
||||
}: ColumnViewProps): JSX.Element {
|
||||
const {
|
||||
activeLog,
|
||||
onSetActiveLog: handleSetActiveLog,
|
||||
onClearActiveLog: handleClearActiveLog,
|
||||
onAddToQuery: handleAddToQuery,
|
||||
onGroupByAttribute: handleGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const { queryData: activeLogId } = useUrlQueryData<string | null>(
|
||||
QueryParams.activeLogId,
|
||||
null,
|
||||
);
|
||||
|
||||
const scrollToIndexRef = useRef<
|
||||
| ((
|
||||
rowIndex: number,
|
||||
options?: { align?: 'start' | 'center' | 'end' },
|
||||
) => void)
|
||||
| undefined
|
||||
>();
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
useEffect(() => {
|
||||
if (activeLogId) {
|
||||
const log = logs.find(({ id }) => id === activeLogId);
|
||||
|
||||
if (log) {
|
||||
handleSetActiveLog(log);
|
||||
}
|
||||
}
|
||||
}, [activeLogId, logs, handleSetActiveLog]);
|
||||
|
||||
const tableViewProps = {
|
||||
logs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLinesPerRow as number,
|
||||
fontSize: options.fontSize as FontSize,
|
||||
appendTo: 'end' as const,
|
||||
activeLogIndex: 0,
|
||||
};
|
||||
|
||||
const { dataSource, columns } = useTableView({
|
||||
...tableViewProps,
|
||||
onClickExpand: handleSetActiveLog,
|
||||
onOpenLogsContext: handleClearActiveLog,
|
||||
});
|
||||
|
||||
const { draggedColumns, onColumnOrderChange } = useDragColumns<
|
||||
Record<string, unknown>
|
||||
>(LOCALSTORAGE.LOGS_LIST_COLUMNS);
|
||||
|
||||
const tableColumns = useMemo(
|
||||
() => getDraggedColumns<Record<string, unknown>>(columns, draggedColumns),
|
||||
[columns, draggedColumns],
|
||||
);
|
||||
|
||||
const scrollToLog = useCallback(
|
||||
(logId: string): void => {
|
||||
const logIndex = logs.findIndex((log) => log.id === logId);
|
||||
|
||||
if (logIndex !== -1 && scrollToIndexRef.current) {
|
||||
scrollToIndexRef.current(logIndex, { align: 'center' });
|
||||
}
|
||||
},
|
||||
[logs],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeLogId) {
|
||||
scrollToLog(activeLogId);
|
||||
}
|
||||
}, [activeLogId]);
|
||||
|
||||
const args = {
|
||||
columns,
|
||||
tableId: 'virtualized-infinite-reorder-resize',
|
||||
enableSorting: false,
|
||||
enableFiltering: false,
|
||||
enableGlobalFilter: false,
|
||||
enableColumnReordering: true,
|
||||
enableColumnResizing: true,
|
||||
enableColumnPinning: false,
|
||||
enableRowSelection: false,
|
||||
enablePagination: false,
|
||||
showHeaders: true,
|
||||
defaultColumnWidth: 180,
|
||||
minColumnWidth: 80,
|
||||
maxColumnWidth: 480,
|
||||
// Virtualization + Infinite Scroll
|
||||
enableVirtualization: true,
|
||||
estimateRowSize: 56,
|
||||
overscan: 50,
|
||||
rowHeight: 56,
|
||||
enableInfiniteScroll: true,
|
||||
enableScrollRestoration: false,
|
||||
fixedHeight: isFrequencyChartVisible ? 560 : 760,
|
||||
enableDynamicRowHeight: true,
|
||||
};
|
||||
|
||||
const selectedColumns = useMemo(
|
||||
() =>
|
||||
tableColumns.map((field) => ({
|
||||
id: field.key?.toString().toLowerCase().replace(/\./g, '_'), // IMP - Replace dots with underscores as reordering does not work well for accessorKey with dots
|
||||
// accessorKey: field.name,
|
||||
accessorFn: (row: Record<string, string>): string =>
|
||||
row[field.key as string] as string,
|
||||
header: field.title as string,
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
size: field.key === 'state-indicator' ? 4 : 180,
|
||||
minSize: field.key === 'state-indicator' ? 4 : 120,
|
||||
maxSize: field.key === 'state-indicator' ? 4 : Number.MAX_SAFE_INTEGER,
|
||||
disableReorder: field.key === 'state-indicator',
|
||||
disableDropBefore: field.key === 'state-indicator',
|
||||
disableResizing: field.key === 'state-indicator',
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
cell: ({
|
||||
row,
|
||||
getValue,
|
||||
}: {
|
||||
row: Row<Record<string, string>>;
|
||||
getValue: () => string | JSX.Element;
|
||||
}): string | JSX.Element => {
|
||||
if (field.key === 'state-indicator') {
|
||||
const type = getLogIndicatorTypeForTable(row.original);
|
||||
const fontSize = options.fontSize as FontSize;
|
||||
|
||||
return <LogStateIndicator type={type} fontSize={fontSize} />;
|
||||
}
|
||||
|
||||
const isTimestamp = field.key === 'timestamp';
|
||||
const cellContent = getValue();
|
||||
|
||||
if (isTimestamp) {
|
||||
const formattedTimestamp = dayjs(cellContent as string).tz(
|
||||
timezone.value,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="table-cell-content">
|
||||
{formattedTimestamp.format(DATE_TIME_FORMATS.ISO_DATETIME_MS)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`table-cell-content ${
|
||||
row.original.id === activeLog?.id ? 'active-log' : ''
|
||||
}`}
|
||||
>
|
||||
{cellContent}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
})),
|
||||
[tableColumns, options.fontSize, activeLog?.id],
|
||||
);
|
||||
|
||||
const handleColumnOrderChange = (newColumns: ColumnDef<any>[]): void => {
|
||||
if (isEmpty(newColumns) || isEqual(newColumns, selectedColumns)) return;
|
||||
|
||||
const formattedColumns = newColumns.map((column) => ({
|
||||
id: column.id,
|
||||
header: column.header,
|
||||
size: column.size,
|
||||
minSize: column.minSize,
|
||||
maxSize: column.maxSize,
|
||||
key: column.id,
|
||||
title: column.header as string,
|
||||
dataIndex: column.id,
|
||||
}));
|
||||
|
||||
onColumnOrderChange(formattedColumns);
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Row<Record<string, unknown>>): void => {
|
||||
const currentLog = logs.find(({ id }) => id === row.original.id);
|
||||
|
||||
handleSetActiveLog(currentLog as ILog);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`logs-list-table-view-container ${
|
||||
options.fontSize as FontSize
|
||||
} max-lines-${options.maxLinesPerRow as number}`}
|
||||
data-max-lines-per-row={options.maxLinesPerRow}
|
||||
data-font-size={options.fontSize}
|
||||
>
|
||||
<DataTable
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...args}
|
||||
columns={selectedColumns as ColumnDef<Record<string, string>, unknown>[]}
|
||||
data={dataSource}
|
||||
hasMore
|
||||
onLoadMore={onLoadMore}
|
||||
loadingMore={isLoading || isFetching}
|
||||
onColumnOrderChange={handleColumnOrderChange}
|
||||
onRowClick={handleRowClick}
|
||||
scrollToIndexRef={scrollToIndexRef}
|
||||
/>
|
||||
|
||||
{activeLog && (
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={handleClearActiveLog}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
onClickActionItem={handleAddToQuery}
|
||||
onGroupByAttribute={handleGroupByAttribute}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColumnView;
|
||||
@@ -11,4 +11,5 @@ export type LogsExplorerListProps = {
|
||||
isError: boolean;
|
||||
error?: Error | APIError;
|
||||
isFilterApplied: boolean;
|
||||
isFrequencyChartVisible: boolean;
|
||||
};
|
||||
|
||||
@@ -9,4 +9,364 @@
|
||||
letter-spacing: -0.005em;
|
||||
text-align: left;
|
||||
min-height: 500px;
|
||||
|
||||
.logs-list-table-view-container {
|
||||
.data-table-container {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: white !important;
|
||||
|
||||
.cursor-col-resize {
|
||||
width: 3px !important;
|
||||
cursor: col-resize !important;
|
||||
opacity: 0.5 !important;
|
||||
background-color: var(--bg-ink-500) !important;
|
||||
border: 1px solid var(--bg-ink-500) !important;
|
||||
|
||||
&:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-400) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-400) !important;
|
||||
background-color: var(--bg-ink-400) !important;
|
||||
|
||||
tr {
|
||||
&:hover {
|
||||
background-color: var(--bg-ink-400) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
padding: 4px !important;
|
||||
|
||||
white-space: pre-wrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-slate-500) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-400) !important;
|
||||
}
|
||||
|
||||
tr:has(.active-log) {
|
||||
background-color: rgba(
|
||||
78,
|
||||
116,
|
||||
248,
|
||||
0.5
|
||||
) !important; // same as bg-robin-500
|
||||
color: var(--text-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
z-index: 0 !important;
|
||||
}
|
||||
|
||||
.log-state-indicator {
|
||||
padding-left: 0 !important;
|
||||
|
||||
.line {
|
||||
margin: 0 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sticky-header-table-container {
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-lines-1 {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='1'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='2'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='3'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='4'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='5'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 5;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='6'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 6;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='7'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 7;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='8'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 8;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='9'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 9;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='10'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 10;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.logs-list-view-container {
|
||||
.logs-list-table-view-container {
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
|
||||
tr {
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sticky-header-table-container {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ function LogsExplorerList({
|
||||
});
|
||||
}
|
||||
}, [isLoading, isFetching, isError, logs.length]);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, log: ILog): JSX.Element => {
|
||||
if (options.format === 'raw') {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding-bottom: 60px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.views-tabs-container {
|
||||
padding: 8px 16px;
|
||||
@@ -216,15 +216,19 @@
|
||||
background-color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.logs-histogram {
|
||||
.ant-card-body {
|
||||
height: 140px;
|
||||
min-height: 140px;
|
||||
padding: 0 16px 22px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
.logs-frequency-chart-container {
|
||||
padding: 0px 8px;
|
||||
|
||||
margin-bottom: 0px;
|
||||
.logs-frequency-chart {
|
||||
.ant-card-body {
|
||||
height: 140px;
|
||||
min-height: 140px;
|
||||
padding: 0 16px 22px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import './LogsExplorerViews.styles.scss';
|
||||
|
||||
import { Button, Switch, Typography } from 'antd';
|
||||
import getFromLocalstorage from 'api/browser/localstorage/get';
|
||||
import setToLocalstorage from 'api/browser/localstorage/set';
|
||||
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
@@ -103,7 +105,13 @@ function LogsExplorerViewsContainer({
|
||||
}): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
|
||||
|
||||
const [showFrequencyChart, setShowFrequencyChart] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const frequencyChart = getFromLocalstorage(LOCALSTORAGE.SHOW_FREQUENCY_CHART);
|
||||
setShowFrequencyChart(frequencyChart === 'true');
|
||||
}, []);
|
||||
|
||||
// this is to respect the panel type present in the URL rather than defaulting it to list always.
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
@@ -672,6 +680,18 @@ function LogsExplorerViewsContainer({
|
||||
[logs, timezone.value],
|
||||
);
|
||||
|
||||
const handleToggleFrequencyChart = useCallback(() => {
|
||||
const newShowFrequencyChart = !showFrequencyChart;
|
||||
|
||||
// store the value in local storage
|
||||
setToLocalstorage(
|
||||
LOCALSTORAGE.SHOW_FREQUENCY_CHART,
|
||||
newShowFrequencyChart?.toString() || 'false',
|
||||
);
|
||||
|
||||
setShowFrequencyChart(newShowFrequencyChart);
|
||||
}, [showFrequencyChart]);
|
||||
|
||||
return (
|
||||
<div className="logs-explorer-views-container">
|
||||
<div className="logs-explorer-views-types">
|
||||
@@ -685,7 +705,7 @@ function LogsExplorerViewsContainer({
|
||||
size="small"
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={(): void => setShowFrequencyChart(!showFrequencyChart)}
|
||||
onChange={handleToggleFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -761,12 +781,14 @@ function LogsExplorerViewsContainer({
|
||||
</div>
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.LIST && showFrequencyChart && (
|
||||
<LogsExplorerChart
|
||||
className="logs-histogram"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
<div className="logs-frequency-chart-container">
|
||||
<LogsExplorerChart
|
||||
className="logs-frequency-chart"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="logs-explorer-views-type-content">
|
||||
@@ -777,12 +799,12 @@ function LogsExplorerViewsContainer({
|
||||
currentStagedQueryData={listQuery}
|
||||
logs={logs}
|
||||
onEndReached={handleEndReached}
|
||||
isFrequencyChartVisible={showFrequencyChart}
|
||||
isError={isError}
|
||||
error={error as APIError}
|
||||
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.TIME_SERIES && (
|
||||
<TimeSeriesView
|
||||
isLoading={isLoading || isFetching}
|
||||
|
||||
@@ -65,17 +65,6 @@ const useOptionsMenu = ({
|
||||
const [isFocused, setIsFocused] = useState<boolean>(false);
|
||||
const debouncedSearchText = useDebounce(searchText, 300);
|
||||
|
||||
// const initialQueryParams = useMemo(
|
||||
// () => ({
|
||||
// searchText: '',
|
||||
// aggregateAttribute: '',
|
||||
// tagType: undefined,
|
||||
// dataSource,
|
||||
// aggregateOperator,
|
||||
// }),
|
||||
// [dataSource, aggregateOperator],
|
||||
// );
|
||||
|
||||
const initialQueryParamsV5: QueryKeyRequestProps = useMemo(
|
||||
() => ({
|
||||
signal: dataSource,
|
||||
@@ -89,22 +78,6 @@ const useOptionsMenu = ({
|
||||
redirectWithQuery: redirectWithOptionsData,
|
||||
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||
|
||||
// const initialQueries = useMemo(
|
||||
// () =>
|
||||
// initialOptions?.selectColumns?.map((column) => ({
|
||||
// queryKey: column,
|
||||
// queryFn: (): Promise<
|
||||
// SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||
// > =>
|
||||
// getAggregateKeys({
|
||||
// ...initialQueryParams,
|
||||
// searchText: column,
|
||||
// }),
|
||||
// enabled: !!column && !optionsQuery,
|
||||
// })) || [],
|
||||
// [initialOptions?.selectColumns, initialQueryParams, optionsQuery],
|
||||
// );
|
||||
|
||||
const initialQueriesV5 = useMemo(
|
||||
() =>
|
||||
initialOptions?.selectColumns?.map((column) => ({
|
||||
|
||||
@@ -9,8 +9,6 @@ import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { Container } from '../styles';
|
||||
|
||||
function AddDomain({ refetch }: Props): JSX.Element {
|
||||
const { t } = useTranslation(['common', 'organizationsettings']);
|
||||
const [isAddDomains, setIsDomain] = useState(false);
|
||||
@@ -42,7 +40,7 @@ function AddDomain({ refetch }: Props): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<div className="auth-domains-title-container">
|
||||
<Typography.Title level={3}>
|
||||
{t('authenticated_domains', {
|
||||
ns: 'organizationsettings',
|
||||
@@ -55,10 +53,11 @@ function AddDomain({ refetch }: Props): JSX.Element {
|
||||
>
|
||||
{t('add_domain', { ns: 'organizationsettings' })}
|
||||
</Button>
|
||||
</Container>
|
||||
</div>
|
||||
<Modal
|
||||
centered
|
||||
title="Add Domain"
|
||||
className="add-domain-modal"
|
||||
footer={null}
|
||||
open={isAddDomains}
|
||||
destroyOnClose
|
||||
|
||||
@@ -244,7 +244,7 @@ function AuthDomains(): JSX.Element {
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Space direction="vertical" size="middle">
|
||||
<div className="auth-domains-container">
|
||||
<AddDomain refetch={refetch} />
|
||||
|
||||
<ResizeTable
|
||||
@@ -255,7 +255,7 @@ function AuthDomains(): JSX.Element {
|
||||
rowKey={(record: AuthDomain): string => record.name + v4()}
|
||||
bordered
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
export const ColumnWithTooltip = styled(Row)`
|
||||
|
||||
@@ -152,6 +152,7 @@ function InviteUserModal(props: InviteUserModalProps): JSX.Element {
|
||||
onCancel={(): void => toggleModal(false)}
|
||||
centered
|
||||
data-testid="invite-team-members-modal"
|
||||
className="invite-user-modal"
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button key="back" onClick={(): void => toggleModal(false)} type="default">
|
||||
|
||||
@@ -157,6 +157,7 @@ function UserFunction({
|
||||
</Space>
|
||||
<Modal
|
||||
title="Edit member details"
|
||||
className="edit-member-details-modal"
|
||||
open={isModalVisible}
|
||||
onOk={(): void => onModalToggleHandler(setIsModalVisible, false)}
|
||||
onCancel={(): void => onModalToggleHandler(setIsModalVisible, false)}
|
||||
@@ -285,7 +286,7 @@ function Members(): JSX.Element {
|
||||
];
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="middle">
|
||||
<div className="members-container">
|
||||
<Typography.Title level={3}>
|
||||
Members{' '}
|
||||
{!isLoading && dataSource && (
|
||||
@@ -300,7 +301,7 @@ function Members(): JSX.Element {
|
||||
loading={status === 'loading'}
|
||||
bordered
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
.organization-settings-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin: 16px auto;
|
||||
gap: 24px;
|
||||
margin: 16px;
|
||||
|
||||
padding: 16px;
|
||||
|
||||
width: 90%;
|
||||
|
||||
border: 1px solid var(--Slate-500, #161922);
|
||||
background: var(--Ink-400, #121317);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.pending-invites-container,
|
||||
.members-container,
|
||||
.auth-domains-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-domains-title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.organization-settings-container {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
@@ -180,7 +180,7 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pending-invites-container-wrapper">
|
||||
<InviteUserModal
|
||||
form={form}
|
||||
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
|
||||
@@ -189,7 +189,7 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
shouldCallApi
|
||||
/>
|
||||
|
||||
<Space direction="vertical" size="middle">
|
||||
<div className="pending-invites-container">
|
||||
<TitleWrapper>
|
||||
<Typography.Title level={3}>
|
||||
{t('pending_invites')}
|
||||
@@ -218,7 +218,7 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
loading={getPendingInvitesResponse.status === 'loading'}
|
||||
bordered
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Divider, Space } from 'antd';
|
||||
import './OrganizationSettings.styles.scss';
|
||||
|
||||
import { Space } from 'antd';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import AuthDomains from './AuthDomains';
|
||||
@@ -24,7 +26,6 @@ function OrganizationSettings(): JSX.Element {
|
||||
<PendingInvitesContainer />
|
||||
|
||||
<Members />
|
||||
<Divider />
|
||||
<AuthDomains />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -42,11 +42,11 @@
|
||||
gap: 8px;
|
||||
|
||||
&.active-tab {
|
||||
background-color: var(--bg-ink-500);
|
||||
border-bottom: 1px solid var(--bg-ink-500);
|
||||
background-color: var(--bg-robin-500);
|
||||
border-bottom: 1px solid var(--bg-robin-500);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-ink-500) !important;
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export type UseCopyLogLink = {
|
||||
isLogsExplorerPage: boolean;
|
||||
activeLogId: string | null;
|
||||
onLogCopy: MouseEventHandler<HTMLElement>;
|
||||
onClearActiveLog: () => void;
|
||||
};
|
||||
|
||||
export type UseActiveLog = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import {
|
||||
@@ -21,9 +22,10 @@ import { UseCopyLogLink } from './types';
|
||||
|
||||
export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
const urlQuery = useUrlQuery();
|
||||
const { pathname } = useLocation();
|
||||
const { pathname, search } = useLocation();
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const { queryData: activeLogId } = useUrlQueryData<string | null>(
|
||||
QueryParams.activeLogId,
|
||||
@@ -58,13 +60,19 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
|
||||
|
||||
setCopy(link);
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
|
||||
toast.success('Copied to clipboard', { position: 'top-right' });
|
||||
},
|
||||
[logId, urlQuery, minTime, maxTime, pathname, setCopy, notifications],
|
||||
[logId, urlQuery, minTime, maxTime, pathname, setCopy],
|
||||
);
|
||||
|
||||
const onClearActiveLog = useCallback(() => {
|
||||
const currentUrlQuery = new URLSearchParams(search);
|
||||
currentUrlQuery.delete(QueryParams.activeLogId);
|
||||
const newUrl = `${pathname}?${currentUrlQuery.toString()}`;
|
||||
safeNavigate(newUrl);
|
||||
}, [pathname, search, safeNavigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActiveLog) return;
|
||||
|
||||
@@ -81,5 +89,6 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
isLogsExplorerPage,
|
||||
activeLogId,
|
||||
onLogCopy,
|
||||
onClearActiveLog,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -40,6 +40,13 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
|
||||
[handleRedirectWithDraggedColumns],
|
||||
);
|
||||
|
||||
const onColumnOrderChange = useCallback(
|
||||
(newColumns: ColumnsType<T>): void => {
|
||||
handleRedirectWithDraggedColumns(newColumns);
|
||||
},
|
||||
[handleRedirectWithDraggedColumns],
|
||||
);
|
||||
|
||||
const redirectWithNewDraggedColumns = useCallback(
|
||||
async (localStorageColumns: string) => {
|
||||
let nextDraggedColumns: ColumnsType<T> = [];
|
||||
@@ -69,6 +76,7 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
|
||||
return {
|
||||
draggedColumns,
|
||||
onDragColumns,
|
||||
onColumnOrderChange,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ export type UseDragColumns<T> = {
|
||||
fromIndex: number,
|
||||
toIndex: number,
|
||||
) => void;
|
||||
onColumnOrderChange: (newColumns: ColumnsType<T>) => void;
|
||||
};
|
||||
|
||||
@@ -105,7 +105,6 @@ const logsQueryServerRequest = (): void =>
|
||||
describe('Logs Explorer Tests', () => {
|
||||
test('Logs Explorer default view test without data', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByRole,
|
||||
queryByText,
|
||||
getByTestId,
|
||||
@@ -123,13 +122,10 @@ describe('Logs Explorer Tests', () => {
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
// check the presence of frequency chart content
|
||||
expect(getByText(frequencyChartContent)).toBeInTheDocument();
|
||||
|
||||
// toggle the chart and check it gets removed from the DOM
|
||||
// by default is hidden, toggle the chart and check it's visibility
|
||||
const histogramToggle = getByRole('switch');
|
||||
fireEvent.click(histogramToggle);
|
||||
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument();
|
||||
expect(queryByText(frequencyChartContent)).toBeInTheDocument();
|
||||
|
||||
// check the presence of search bar and query builder and absence of clickhouse
|
||||
const searchView = getByTestId('search-view');
|
||||
@@ -277,10 +273,10 @@ describe('Logs Explorer Tests', () => {
|
||||
const histogramToggle = getByRole('switch');
|
||||
expect(histogramToggle).toBeInTheDocument();
|
||||
expect(histogramToggle).toBeChecked();
|
||||
expect(queryByText(frequencyChartContent)).toBeInTheDocument();
|
||||
|
||||
// toggle the chart and check it gets removed from the DOM
|
||||
await fireEvent.click(histogramToggle);
|
||||
expect(histogramToggle).not.toBeChecked();
|
||||
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,9 @@ export interface PayloadPropsTraces {
|
||||
}
|
||||
|
||||
export interface PayloadPropsLogs {
|
||||
logs_ttl_duration_hrs: number;
|
||||
version: 'v1' | 'v2';
|
||||
default_ttl_days: number;
|
||||
logs_ttl_duration_hrs?: number;
|
||||
logs_move_ttl_duration_hrs?: number;
|
||||
status: TStatus;
|
||||
expected_logs_ttl_duration_hrs?: number;
|
||||
|
||||
@@ -7,6 +7,23 @@ export interface Props {
|
||||
toColdDuration?: string;
|
||||
}
|
||||
|
||||
export interface PropsV2 {
|
||||
type: TTTLType;
|
||||
defaultTTLDays: number;
|
||||
coldStorageVolume: string;
|
||||
coldStorageDuration: number;
|
||||
ttlConditions: {
|
||||
conditions: {
|
||||
key: string;
|
||||
values: string[];
|
||||
}[];
|
||||
ttlDays: number;
|
||||
}[];
|
||||
}
|
||||
export interface PayloadProps {
|
||||
success: 'message';
|
||||
}
|
||||
|
||||
export interface PayloadPropsV2 {
|
||||
message: string;
|
||||
}
|
||||
|
||||
@@ -3834,6 +3834,24 @@
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
"@radix-ui/react-visually-hidden" "1.0.3"
|
||||
|
||||
"@radix-ui/react-tooltip@^1.2.6":
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz#3f50267e25bccfc9e20bb3036bfd9ab4c2c30c2c"
|
||||
integrity sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.3"
|
||||
"@radix-ui/react-compose-refs" "1.1.2"
|
||||
"@radix-ui/react-context" "1.1.2"
|
||||
"@radix-ui/react-dismissable-layer" "1.1.11"
|
||||
"@radix-ui/react-id" "1.1.1"
|
||||
"@radix-ui/react-popper" "1.2.8"
|
||||
"@radix-ui/react-portal" "1.1.9"
|
||||
"@radix-ui/react-presence" "1.1.5"
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
"@radix-ui/react-slot" "1.2.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||
"@radix-ui/react-visually-hidden" "1.2.3"
|
||||
|
||||
"@radix-ui/react-use-callback-ref@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"
|
||||
@@ -3934,6 +3952,13 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
|
||||
"@radix-ui/react-visually-hidden@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz#a8c38c8607735dc9f05c32f87ab0f9c2b109efbf"
|
||||
integrity sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
|
||||
"@radix-ui/rect@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f"
|
||||
@@ -4284,6 +4309,53 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/sonner@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/sonner/-/sonner-0.1.0.tgz#1310cc530c60459608246550eb977a1ae27b6ce4"
|
||||
integrity sha512-P4gc1WdNiX89FZIAhIvR4Bj3sdL7VIpoM80L6otnoeLCZhyFfEOXHGAElvO2z7BuBqJBp1f3pdVDrBQNE6bhyw==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lucide-react "^0.445.0"
|
||||
next-themes "^0.4.6"
|
||||
sonner "^2.0.7"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/table@0.3.7":
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/table/-/table-0.3.7.tgz#895b710c02af124dfb5117e02bbc6d80ce062063"
|
||||
integrity sha512-XDwRHBTf2q48MOuxkzzr0htWd0/mmvgHoZLl0WAMLk2gddbbNHg9hkCPfVARYOke6mX8Z/4T3e7dzgkRUhDGDg==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
"@signozhq/tooltip" "0.0.2"
|
||||
"@tanstack/react-table" "^8.21.3"
|
||||
"@tanstack/react-virtual" "^3.13.9"
|
||||
"@types/lodash-es" "^4.17.12"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lodash-es "^4.17.21"
|
||||
lucide-react "^0.445.0"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/tooltip@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/tooltip/-/tooltip-0.0.2.tgz#bb4e2681868fa2e06db78eff5872ffb2a78b93b6"
|
||||
integrity sha512-itxvrFq0SHr+xTabd7Tf/O9Gfq45kpIyQ5qklDRfhbxr4PIVSBw5XMr2OgzOPbW7P8S9fugCwMu6gPyAZ0SM5A==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
"@radix-ui/react-tooltip" "^1.2.6"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lucide-react "^0.445.0"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@sinclair/typebox@^0.25.16":
|
||||
version "0.25.24"
|
||||
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz"
|
||||
@@ -4327,6 +4399,13 @@
|
||||
dependencies:
|
||||
"@tanstack/table-core" "8.20.5"
|
||||
|
||||
"@tanstack/react-table@^8.21.3":
|
||||
version "8.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b"
|
||||
integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==
|
||||
dependencies:
|
||||
"@tanstack/table-core" "8.21.3"
|
||||
|
||||
"@tanstack/react-virtual@3.11.2":
|
||||
version "3.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322"
|
||||
@@ -4334,16 +4413,33 @@
|
||||
dependencies:
|
||||
"@tanstack/virtual-core" "3.11.2"
|
||||
|
||||
"@tanstack/react-virtual@^3.13.9":
|
||||
version "3.13.12"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz#d372dc2783739cc04ec1a728ca8203937687a819"
|
||||
integrity sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==
|
||||
dependencies:
|
||||
"@tanstack/virtual-core" "3.13.12"
|
||||
|
||||
"@tanstack/table-core@8.20.5":
|
||||
version "8.20.5"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
|
||||
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==
|
||||
|
||||
"@tanstack/table-core@8.21.3":
|
||||
version "8.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c"
|
||||
integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==
|
||||
|
||||
"@tanstack/virtual-core@3.11.2":
|
||||
version "3.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212"
|
||||
integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==
|
||||
|
||||
"@tanstack/virtual-core@3.13.12":
|
||||
version "3.13.12"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz#1dff176df9cc8f93c78c5e46bcea11079b397578"
|
||||
integrity sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==
|
||||
|
||||
"@testing-library/dom@^8.5.0":
|
||||
version "8.20.0"
|
||||
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz"
|
||||
@@ -4825,6 +4921,13 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/lodash-es@^4.17.12":
|
||||
version "4.17.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
|
||||
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash-es@^4.17.4":
|
||||
version "4.17.7"
|
||||
resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz"
|
||||
@@ -13326,6 +13429,11 @@ new-array@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/new-array/-/new-array-1.0.0.tgz"
|
||||
integrity sha512-K5AyFYbuHZ4e/ti52y7k18q8UHsS78FlRd85w2Fmsd6AkuLipDihPflKC0p3PN5i8II7+uHxo+CtkLiJDfmS5A==
|
||||
|
||||
next-themes@^0.4.6:
|
||||
version "0.4.6"
|
||||
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6"
|
||||
integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==
|
||||
|
||||
ngraph.events@^1.0.0, ngraph.events@^1.2.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz"
|
||||
@@ -16430,6 +16538,11 @@ sockjs@^0.3.24:
|
||||
uuid "^8.3.2"
|
||||
websocket-driver "^0.7.4"
|
||||
|
||||
sonner@^2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.7.tgz#810c1487a67ec3370126e0f400dfb9edddc3e4f6"
|
||||
integrity sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==
|
||||
|
||||
sort-asc@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz"
|
||||
|
||||
503
pkg/modules/thirdpartyapi/translator.go
Normal file
503
pkg/modules/thirdpartyapi/translator.go
Normal file
@@ -0,0 +1,503 @@
|
||||
package thirdpartyapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/SigNoz/signoz/pkg/types/thirdpartyapitypes"
|
||||
"net"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
urlPathKeyLegacy = "http.url"
|
||||
serverAddressKeyLegacy = "net.peer.name"
|
||||
|
||||
urlPathKey = "url.full"
|
||||
serverAddressKey = "server.address"
|
||||
)
|
||||
|
||||
var defaultStepInterval = 60 * time.Second
|
||||
|
||||
type SemconvFieldMapping struct {
|
||||
LegacyField string
|
||||
CurrentField string
|
||||
FieldType telemetrytypes.FieldDataType
|
||||
Context telemetrytypes.FieldContext
|
||||
}
|
||||
|
||||
var dualSemconvGroupByKeys = map[string][]qbtypes.GroupByKey{
|
||||
"server": {
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: serverAddressKey,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
},
|
||||
},
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: serverAddressKeyLegacy,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
},
|
||||
},
|
||||
},
|
||||
"url": {
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: urlPathKey,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
},
|
||||
},
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: urlPathKeyLegacy,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func MergeSemconvColumns(result *qbtypes.QueryRangeResponse) *qbtypes.QueryRangeResponse {
|
||||
if result == nil || result.Data.Results == nil {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, res := range result.Data.Results {
|
||||
scalarData, ok := res.(*qbtypes.ScalarData)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
serverAddressKeyIdx := -1
|
||||
serverAddressKeyLegacyIdx := -1
|
||||
|
||||
for i, col := range scalarData.Columns {
|
||||
if col.Name == serverAddressKey {
|
||||
serverAddressKeyIdx = i
|
||||
} else if col.Name == serverAddressKeyLegacy {
|
||||
serverAddressKeyLegacyIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
if serverAddressKeyIdx == -1 || serverAddressKeyLegacyIdx == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
var newRows [][]any
|
||||
for _, row := range scalarData.Data {
|
||||
if len(row) <= serverAddressKeyIdx || len(row) <= serverAddressKeyLegacyIdx {
|
||||
continue
|
||||
}
|
||||
|
||||
var serverName any
|
||||
if isValidValue(row[serverAddressKeyIdx]) {
|
||||
serverName = row[serverAddressKeyIdx]
|
||||
} else if isValidValue(row[serverAddressKeyLegacyIdx]) {
|
||||
serverName = row[serverAddressKeyLegacyIdx]
|
||||
}
|
||||
|
||||
if serverName != nil {
|
||||
newRow := make([]any, len(row)-1)
|
||||
newRow[0] = serverName
|
||||
|
||||
targetIdx := 1
|
||||
for i, val := range row {
|
||||
if i != serverAddressKeyLegacyIdx && i != serverAddressKeyIdx {
|
||||
if targetIdx < len(newRow) {
|
||||
newRow[targetIdx] = val
|
||||
targetIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
newRows = append(newRows, newRow)
|
||||
}
|
||||
}
|
||||
|
||||
newColumns := make([]*qbtypes.ColumnDescriptor, len(scalarData.Columns)-1)
|
||||
targetIdx := 0
|
||||
for i, col := range scalarData.Columns {
|
||||
if i == serverAddressKeyIdx {
|
||||
newCol := &qbtypes.ColumnDescriptor{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: serverAddressKeyLegacy,
|
||||
FieldDataType: col.FieldDataType,
|
||||
FieldContext: col.FieldContext,
|
||||
Signal: col.Signal,
|
||||
},
|
||||
QueryName: col.QueryName,
|
||||
AggregationIndex: col.AggregationIndex,
|
||||
Meta: col.Meta,
|
||||
Type: col.Type,
|
||||
}
|
||||
newColumns[targetIdx] = newCol
|
||||
targetIdx++
|
||||
} else if i != serverAddressKeyLegacyIdx {
|
||||
newColumns[targetIdx] = col
|
||||
targetIdx++
|
||||
}
|
||||
}
|
||||
|
||||
scalarData.Columns = newColumns
|
||||
scalarData.Data = newRows
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func isValidValue(val any) bool {
|
||||
if val == nil {
|
||||
return false
|
||||
}
|
||||
if str, ok := val.(string); ok {
|
||||
return str != "" && str != "n/a"
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func FilterResponse(results []*qbtypes.QueryRangeResponse) []*qbtypes.QueryRangeResponse {
|
||||
filteredResults := make([]*qbtypes.QueryRangeResponse, 0, len(results))
|
||||
|
||||
for _, res := range results {
|
||||
if res.Data.Results == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
filteredData := make([]any, 0, len(res.Data.Results))
|
||||
for _, result := range res.Data.Results {
|
||||
if result == nil {
|
||||
filteredData = append(filteredData, result)
|
||||
continue
|
||||
}
|
||||
|
||||
switch resultData := result.(type) {
|
||||
case *qbtypes.TimeSeriesData:
|
||||
if resultData.Aggregations != nil {
|
||||
for _, agg := range resultData.Aggregations {
|
||||
filteredSeries := make([]*qbtypes.TimeSeries, 0, len(agg.Series))
|
||||
for _, series := range agg.Series {
|
||||
if shouldIncludeSeries(series) {
|
||||
filteredSeries = append(filteredSeries, series)
|
||||
}
|
||||
}
|
||||
agg.Series = filteredSeries
|
||||
}
|
||||
}
|
||||
case *qbtypes.RawData:
|
||||
filteredRows := make([]*qbtypes.RawRow, 0, len(resultData.Rows))
|
||||
for _, row := range resultData.Rows {
|
||||
if shouldIncludeRow(row) {
|
||||
filteredRows = append(filteredRows, row)
|
||||
}
|
||||
}
|
||||
resultData.Rows = filteredRows
|
||||
}
|
||||
|
||||
filteredData = append(filteredData, result)
|
||||
}
|
||||
|
||||
res.Data.Results = filteredData
|
||||
filteredResults = append(filteredResults, res)
|
||||
}
|
||||
|
||||
return filteredResults
|
||||
}
|
||||
|
||||
func shouldIncludeSeries(series *qbtypes.TimeSeries) bool {
|
||||
for _, label := range series.Labels {
|
||||
if label.Key.Name == serverAddressKeyLegacy || label.Key.Name == serverAddressKey {
|
||||
if strVal, ok := label.Value.(string); ok {
|
||||
if net.ParseIP(strVal) != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func shouldIncludeRow(row *qbtypes.RawRow) bool {
|
||||
if row.Data != nil {
|
||||
for _, key := range []string{serverAddressKeyLegacy, serverAddressKey} {
|
||||
if domainVal, ok := row.Data[key]; ok {
|
||||
if domainStr, ok := domainVal.(string); ok {
|
||||
if net.ParseIP(domainStr) != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func containsKindStringOverride(expression string) bool {
|
||||
kindStringPattern := regexp.MustCompile(`kind_string\s*[!=<>]+`)
|
||||
return kindStringPattern.MatchString(expression)
|
||||
}
|
||||
|
||||
func mergeGroupBy(base, additional []qbtypes.GroupByKey) []qbtypes.GroupByKey {
|
||||
return append(base, additional...)
|
||||
}
|
||||
|
||||
func BuildDomainList(req *thirdpartyapitypes.ThirdPartyApiRequest) (*qbtypes.QueryRangeRequest, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queries := []qbtypes.QueryEnvelope{
|
||||
buildEndpointsQuery(req),
|
||||
buildLastSeenQuery(req),
|
||||
buildRpsQuery(req),
|
||||
buildErrorQuery(req),
|
||||
buildTotalSpanQuery(req),
|
||||
buildP99Query(req),
|
||||
buildErrorRateFormula(),
|
||||
}
|
||||
|
||||
return &qbtypes.QueryRangeRequest{
|
||||
SchemaVersion: "v5",
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: queries,
|
||||
},
|
||||
FormatOptions: &qbtypes.FormatOptions{
|
||||
FormatTableResultForUI: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func BuildDomainInfo(req *thirdpartyapitypes.ThirdPartyApiRequest) (*qbtypes.QueryRangeRequest, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queries := []qbtypes.QueryEnvelope{
|
||||
buildEndpointsInfoQuery(req),
|
||||
buildP99InfoQuery(req),
|
||||
buildErrorRateInfoQuery(req),
|
||||
buildLastSeenInfoQuery(req),
|
||||
}
|
||||
|
||||
return &qbtypes.QueryRangeRequest{
|
||||
SchemaVersion: "v5",
|
||||
Start: req.Start,
|
||||
End: req.End,
|
||||
RequestType: qbtypes.RequestTypeScalar,
|
||||
CompositeQuery: qbtypes.CompositeQuery{
|
||||
Queries: queries,
|
||||
},
|
||||
FormatOptions: &qbtypes.FormatOptions{
|
||||
FormatTableResultForUI: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildEndpointsQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
|
||||
return qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "endpoints",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "count_distinct(http.url)"},
|
||||
},
|
||||
Filter: buildBaseFilter(req.Filter),
|
||||
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildLastSeenQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
|
||||
return qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "lastseen",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "max(timestamp)"},
|
||||
},
|
||||
Filter: buildBaseFilter(req.Filter),
|
||||
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildRpsQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
|
||||
return qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "rps",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "rate()"},
|
||||
},
|
||||
Filter: buildBaseFilter(req.Filter),
|
||||
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildErrorQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
|
||||
return qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "error",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
Filter: buildErrorFilter(req.Filter),
|
||||
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildTotalSpanQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
|
||||
return qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "total_span",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
Filter: buildBaseFilter(req.Filter),
|
||||
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildP99Query(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
|
||||
return qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "p99",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "p99(duration_nano)"},
|
||||
},
|
||||
Filter: buildBaseFilter(req.Filter),
|
||||
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildErrorRateFormula() qbtypes.QueryEnvelope {
|
||||
return qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeFormula,
|
||||
Spec: qbtypes.QueryBuilderFormula{
|
||||
Name: "error_rate",
|
||||
Expression: "(error/total_span)*100",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildEndpointsInfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
|
||||
return qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "endpoints",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "rate(http.url)"},
|
||||
},
|
||||
Filter: buildBaseFilter(req.Filter),
|
||||
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["url"], req.GroupBy),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildP99InfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
|
||||
return qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "p99",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "p99(duration_nano)"},
|
||||
},
|
||||
Filter: buildBaseFilter(req.Filter),
|
||||
GroupBy: req.GroupBy,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildErrorRateInfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
|
||||
return qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "error_rate",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "rate()"},
|
||||
},
|
||||
Filter: buildBaseFilter(req.Filter),
|
||||
GroupBy: req.GroupBy,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildLastSeenInfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
|
||||
return qbtypes.QueryEnvelope{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "lastseen",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{Expression: "max(timestamp)"},
|
||||
},
|
||||
Filter: buildBaseFilter(req.Filter),
|
||||
GroupBy: req.GroupBy,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildBaseFilter(additionalFilter *qbtypes.Filter) *qbtypes.Filter {
|
||||
baseExpression := fmt.Sprintf("(%s EXISTS OR %s EXISTS) AND kind_string = 'Client'",
|
||||
urlPathKeyLegacy, urlPathKey)
|
||||
|
||||
if additionalFilter != nil && additionalFilter.Expression != "" {
|
||||
if containsKindStringOverride(additionalFilter.Expression) {
|
||||
return &qbtypes.Filter{Expression: baseExpression}
|
||||
}
|
||||
baseExpression = fmt.Sprintf("(%s) AND (%s)", baseExpression, additionalFilter.Expression)
|
||||
}
|
||||
|
||||
return &qbtypes.Filter{Expression: baseExpression}
|
||||
}
|
||||
|
||||
func buildErrorFilter(additionalFilter *qbtypes.Filter) *qbtypes.Filter {
|
||||
errorExpression := fmt.Sprintf("has_error = true AND (%s EXISTS OR %s EXISTS) AND kind_string = 'Client'",
|
||||
urlPathKeyLegacy, urlPathKey)
|
||||
|
||||
if additionalFilter != nil && additionalFilter.Expression != "" {
|
||||
if containsKindStringOverride(additionalFilter.Expression) {
|
||||
return &qbtypes.Filter{Expression: errorExpression}
|
||||
}
|
||||
errorExpression = fmt.Sprintf("(%s) AND (%s)", errorExpression, additionalFilter.Expression)
|
||||
}
|
||||
|
||||
return &qbtypes.Filter{Expression: errorExpression}
|
||||
}
|
||||
233
pkg/modules/thirdpartyapi/translator_test.go
Normal file
233
pkg/modules/thirdpartyapi/translator_test.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package thirdpartyapi
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/types/thirdpartyapitypes"
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterResponse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []*qbtypes.QueryRangeResponse
|
||||
expected []*qbtypes.QueryRangeResponse
|
||||
}{
|
||||
{
|
||||
name: "should filter out IP addresses from series labels",
|
||||
input: []*qbtypes.QueryRangeResponse{
|
||||
{
|
||||
Data: qbtypes.QueryData{
|
||||
Results: []any{
|
||||
&qbtypes.TimeSeriesData{
|
||||
Aggregations: []*qbtypes.AggregationBucket{
|
||||
{
|
||||
Series: []*qbtypes.TimeSeries{
|
||||
{
|
||||
Labels: []*qbtypes.Label{
|
||||
{
|
||||
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
|
||||
Value: "192.168.1.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: []*qbtypes.Label{
|
||||
{
|
||||
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*qbtypes.QueryRangeResponse{
|
||||
{
|
||||
Data: qbtypes.QueryData{
|
||||
Results: []any{
|
||||
&qbtypes.TimeSeriesData{
|
||||
Aggregations: []*qbtypes.AggregationBucket{
|
||||
{
|
||||
Series: []*qbtypes.TimeSeries{
|
||||
{
|
||||
Labels: []*qbtypes.Label{
|
||||
{
|
||||
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
|
||||
Value: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should filter out IP addresses from raw data",
|
||||
input: []*qbtypes.QueryRangeResponse{
|
||||
{
|
||||
Data: qbtypes.QueryData{
|
||||
Results: []any{
|
||||
&qbtypes.RawData{
|
||||
Rows: []*qbtypes.RawRow{
|
||||
{
|
||||
Data: map[string]any{
|
||||
"net.peer.name": "192.168.1.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Data: map[string]any{
|
||||
"net.peer.name": "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*qbtypes.QueryRangeResponse{
|
||||
{
|
||||
Data: qbtypes.QueryData{
|
||||
Results: []any{
|
||||
&qbtypes.RawData{
|
||||
Rows: []*qbtypes.RawRow{
|
||||
{
|
||||
Data: map[string]any{
|
||||
"net.peer.name": "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := FilterResponse(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDomainList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *thirdpartyapitypes.ThirdPartyApiRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic domain list query",
|
||||
input: &thirdpartyapitypes.ThirdPartyApiRequest{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with filters and group by",
|
||||
input: &thirdpartyapitypes.ThirdPartyApiRequest{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "test = 'value'",
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := BuildDomainList(tt.input)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, tt.input.Start, result.Start)
|
||||
assert.Equal(t, tt.input.End, result.End)
|
||||
assert.NotNil(t, result.CompositeQuery)
|
||||
assert.Len(t, result.CompositeQuery.Queries, 7) // endpoints, lastseen, rps, error, total_span, p99, error_rate
|
||||
assert.Equal(t, "v5", result.SchemaVersion)
|
||||
assert.Equal(t, qbtypes.RequestTypeScalar, result.RequestType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDomainInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *thirdpartyapitypes.ThirdPartyApiRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic domain info query",
|
||||
input: &thirdpartyapitypes.ThirdPartyApiRequest{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with filters and group by",
|
||||
input: &thirdpartyapitypes.ThirdPartyApiRequest{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "test = 'value'",
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := BuildDomainInfo(tt.input)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, tt.input.Start, result.Start)
|
||||
assert.Equal(t, tt.input.End, result.End)
|
||||
assert.NotNil(t, result.CompositeQuery)
|
||||
assert.Len(t, result.CompositeQuery.Queries, 4) // endpoints, p99, error_rate, lastseen
|
||||
assert.Equal(t, "v5", result.SchemaVersion)
|
||||
assert.Equal(t, qbtypes.RequestTypeScalar, result.RequestType)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/emailtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
@@ -228,11 +230,11 @@ func (m *Module) UpdateUser(ctx context.Context, orgID string, id string, user *
|
||||
|
||||
// if the role is updated then send an email
|
||||
if existingUser.Role != updatedUser.Role {
|
||||
if err := m.emailing.SendHTML(ctx, existingUser.Email, "Your Role is updated in SigNoz", emailtypes.TemplateNameUpdateRole, map[string]any{
|
||||
if err := m.emailing.SendHTML(ctx, existingUser.Email, "Your Role Has Been Updated in SigNoz", emailtypes.TemplateNameUpdateRole, map[string]any{
|
||||
"CustomerName": existingUser.DisplayName,
|
||||
"UpdatedByEmail": requestor.Email,
|
||||
"OldRole": existingUser.Role,
|
||||
"NewRole": updatedUser.Role,
|
||||
"OldRole": cases.Title(language.English).String(strings.ToLower(existingUser.Role)),
|
||||
"NewRole": cases.Title(language.English).String(strings.ToLower(updatedUser.Role)),
|
||||
}); err != nil {
|
||||
m.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,14 @@ package app
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/SigNoz/signoz/pkg/modules/thirdpartyapi"
|
||||
|
||||
//qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
@@ -44,7 +48,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/inframetrics"
|
||||
queues2 "github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/queues"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations/thirdPartyApi"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logs"
|
||||
logsv3 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v3"
|
||||
logsv4 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v4"
|
||||
@@ -668,6 +671,10 @@ func (aH *APIHandler) getRule(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ruleResponse, err := aH.ruleManager.GetRule(r.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
@@ -1382,6 +1389,10 @@ func (aH *APIHandler) deleteRule(w http.ResponseWriter, r *http.Request) {
|
||||
err := aH.ruleManager.DeleteRule(r.Context(), id)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
@@ -1410,6 +1421,10 @@ func (aH *APIHandler) patchRule(w http.ResponseWriter, r *http.Request) {
|
||||
gettableRule, err := aH.ruleManager.PatchRule(r.Context(), string(body), id)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
@@ -1436,6 +1451,10 @@ func (aH *APIHandler) editRule(w http.ResponseWriter, r *http.Request) {
|
||||
err = aH.ruleManager.EditRule(r.Context(), string(body), id)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
|
||||
return
|
||||
}
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
@@ -4965,105 +4984,130 @@ func (aH *APIHandler) getQueueOverview(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getDomainList(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract claims from context for organization ID
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the request body to get third-party query parameters
|
||||
thirdPartyQueryRequest, apiErr := ParseRequestBody(r)
|
||||
if apiErr != nil {
|
||||
zap.L().Error(apiErr.Err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
zap.L().Error("Failed to parse request body", zap.Error(apiErr))
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, apiErr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams, err := thirdPartyApi.BuildDomainList(thirdPartyQueryRequest)
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var result []*v3.Result
|
||||
var errQuriesByName map[string]error
|
||||
|
||||
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), orgID, queryRangeParams)
|
||||
// Build the v5 query range request for domain listing
|
||||
queryRangeRequest, err := thirdpartyapi.BuildDomainList(thirdPartyQueryRequest)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
zap.L().Error("Failed to build domain list query", zap.Error(err))
|
||||
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
|
||||
render.Error(w, apiErrObj)
|
||||
return
|
||||
}
|
||||
|
||||
result, err = postprocess.PostProcessResult(result, queryRangeParams)
|
||||
// Validate the v5 query range request
|
||||
if err := queryRangeRequest.Validate(); err != nil {
|
||||
zap.L().Error("Query validation failed", zap.Error(err))
|
||||
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
|
||||
render.Error(w, apiErrObj)
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the query using the v5 querier
|
||||
result, err := aH.Signoz.Querier.QueryRange(r.Context(), orgID, queryRangeRequest)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
zap.L().Error("Query execution failed", zap.Error(err))
|
||||
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
|
||||
render.Error(w, apiErrObj)
|
||||
return
|
||||
}
|
||||
|
||||
result = thirdpartyapi.MergeSemconvColumns(result)
|
||||
|
||||
// Filter IP addresses if ShowIp is false
|
||||
var finalResult = result
|
||||
if !thirdPartyQueryRequest.ShowIp {
|
||||
result = thirdPartyApi.FilterResponse(result)
|
||||
filteredResults := thirdpartyapi.FilterResponse([]*qbtypes.QueryRangeResponse{result})
|
||||
if len(filteredResults) > 0 {
|
||||
finalResult = filteredResults[0]
|
||||
}
|
||||
}
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: result,
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
// Send the response
|
||||
aH.Respond(w, finalResult)
|
||||
}
|
||||
|
||||
// getDomainInfo handles requests for domain information using v5 query builder
|
||||
func (aH *APIHandler) getDomainInfo(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract claims from context for organization ID
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the request body to get third-party query parameters
|
||||
thirdPartyQueryRequest, apiErr := ParseRequestBody(r)
|
||||
if apiErr != nil {
|
||||
zap.L().Error(apiErr.Err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
zap.L().Error("Failed to parse request body", zap.Error(apiErr))
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, apiErr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeParams, err := thirdPartyApi.BuildDomainInfo(thirdPartyQueryRequest)
|
||||
|
||||
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var result []*v3.Result
|
||||
var errQuriesByName map[string]error
|
||||
|
||||
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), orgID, queryRangeParams)
|
||||
// Build the v5 query range request for domain info
|
||||
queryRangeRequest, err := thirdpartyapi.BuildDomainInfo(thirdPartyQueryRequest)
|
||||
if err != nil {
|
||||
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
zap.L().Error("Failed to build domain info query", zap.Error(err))
|
||||
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
|
||||
render.Error(w, apiErrObj)
|
||||
return
|
||||
}
|
||||
|
||||
result = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams)
|
||||
// Validate the v5 query range request
|
||||
if err := queryRangeRequest.Validate(); err != nil {
|
||||
zap.L().Error("Query validation failed", zap.Error(err))
|
||||
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
|
||||
render.Error(w, apiErrObj)
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the query using the v5 querier
|
||||
result, err := aH.Signoz.Querier.QueryRange(r.Context(), orgID, queryRangeRequest)
|
||||
if err != nil {
|
||||
zap.L().Error("Query execution failed", zap.Error(err))
|
||||
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
|
||||
render.Error(w, apiErrObj)
|
||||
return
|
||||
}
|
||||
|
||||
result = thirdpartyapi.MergeSemconvColumns(result)
|
||||
|
||||
// Filter IP addresses if ShowIp is false
|
||||
var finalResult *qbtypes.QueryRangeResponse = result
|
||||
if !thirdPartyQueryRequest.ShowIp {
|
||||
result = thirdPartyApi.FilterResponse(result)
|
||||
filteredResults := thirdpartyapi.FilterResponse([]*qbtypes.QueryRangeResponse{result})
|
||||
if len(filteredResults) > 0 {
|
||||
finalResult = filteredResults[0]
|
||||
}
|
||||
}
|
||||
|
||||
resp := v3.QueryRangeResponse{
|
||||
Result: result,
|
||||
}
|
||||
aH.Respond(w, resp)
|
||||
// Send the response
|
||||
aH.Respond(w, finalResult)
|
||||
}
|
||||
|
||||
// RegisterTraceFunnelsRoutes adds trace funnels routes
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package thirdPartyApi
|
||||
|
||||
import v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
|
||||
type ThirdPartyApis struct {
|
||||
Start int64 `json:"start"`
|
||||
End int64 `json:"end"`
|
||||
ShowIp bool `json:"show_ip,omitempty"`
|
||||
Domain int64 `json:"domain,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
Filters v3.FilterSet `json:"filters,omitempty"`
|
||||
GroupBy []v3.AttributeKey `json:"groupBy,omitempty"`
|
||||
}
|
||||
@@ -1,598 +0,0 @@
|
||||
package thirdPartyApi
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
urlPathKey = "http.url"
|
||||
serverNameKey = "net.peer.name"
|
||||
)
|
||||
|
||||
var defaultStepInterval int64 = 60
|
||||
|
||||
func FilterResponse(results []*v3.Result) []*v3.Result {
|
||||
filteredResults := make([]*v3.Result, 0, len(results))
|
||||
|
||||
for _, res := range results {
|
||||
if res.Table == nil {
|
||||
continue
|
||||
}
|
||||
filteredRows := make([]*v3.TableRow, 0, len(res.Table.Rows))
|
||||
for _, row := range res.Table.Rows {
|
||||
if row.Data != nil {
|
||||
if domainVal, ok := row.Data[serverNameKey]; ok {
|
||||
if domainStr, ok := domainVal.(string); ok {
|
||||
if net.ParseIP(domainStr) != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
filteredRows = append(filteredRows, row)
|
||||
}
|
||||
res.Table.Rows = filteredRows
|
||||
|
||||
filteredResults = append(filteredResults, res)
|
||||
}
|
||||
|
||||
return filteredResults
|
||||
}
|
||||
|
||||
func getFilterSet(existingFilters []v3.FilterItem, apiFilters v3.FilterSet) []v3.FilterItem {
|
||||
if len(apiFilters.Items) != 0 {
|
||||
existingFilters = append(existingFilters, apiFilters.Items...)
|
||||
}
|
||||
return existingFilters
|
||||
}
|
||||
|
||||
func getGroupBy(existingGroupBy []v3.AttributeKey, apiGroupBy []v3.AttributeKey) []v3.AttributeKey {
|
||||
if len(apiGroupBy) != 0 {
|
||||
existingGroupBy = append(existingGroupBy, apiGroupBy...)
|
||||
}
|
||||
return existingGroupBy
|
||||
}
|
||||
|
||||
func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, error) {
|
||||
|
||||
unixMilliStart := thirdPartyApis.Start
|
||||
unixMilliEnd := thirdPartyApis.End
|
||||
|
||||
builderQueries := make(map[string]*v3.BuilderQuery)
|
||||
|
||||
builderQueries["endpoints"] = &v3.BuilderQuery{
|
||||
QueryName: "endpoints",
|
||||
Legend: "endpoints",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorCountDistinct,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
TimeAggregation: v3.TimeAggregationCountDistinct,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: getFilterSet([]v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: false,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
Value: "",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "kind_string",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "Client",
|
||||
},
|
||||
}, thirdPartyApis.Filters),
|
||||
},
|
||||
Expression: "endpoints",
|
||||
GroupBy: getGroupBy([]v3.AttributeKey{
|
||||
{
|
||||
Key: serverNameKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
ShiftBy: 0,
|
||||
IsAnomaly: false,
|
||||
}
|
||||
|
||||
builderQueries["lastseen"] = &v3.BuilderQuery{
|
||||
QueryName: "lastseen",
|
||||
Legend: "lastseen",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorMax,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "timestamp",
|
||||
},
|
||||
TimeAggregation: v3.TimeAggregationMax,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: getFilterSet([]v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: false,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
Value: "",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "kind_string",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "Client",
|
||||
},
|
||||
}, thirdPartyApis.Filters),
|
||||
},
|
||||
Expression: "lastseen",
|
||||
GroupBy: getGroupBy([]v3.AttributeKey{
|
||||
{
|
||||
Key: serverNameKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
ShiftBy: 0,
|
||||
IsAnomaly: false,
|
||||
}
|
||||
|
||||
builderQueries["rps"] = &v3.BuilderQuery{
|
||||
QueryName: "rps",
|
||||
Legend: "rps",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorRate,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "",
|
||||
},
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: getFilterSet([]v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: false,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
Value: "",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "kind_string",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "Client",
|
||||
},
|
||||
}, thirdPartyApis.Filters),
|
||||
},
|
||||
Expression: "rps",
|
||||
GroupBy: getGroupBy([]v3.AttributeKey{
|
||||
{
|
||||
Key: serverNameKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
ShiftBy: 0,
|
||||
IsAnomaly: false,
|
||||
}
|
||||
|
||||
builderQueries["error"] = &v3.BuilderQuery{
|
||||
QueryName: "error",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorCount,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "span_id",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
TimeAggregation: v3.TimeAggregationCount,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: getFilterSet([]v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "has_error",
|
||||
DataType: v3.AttributeKeyDataTypeBool,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "true",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: false,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
Value: "",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "kind_string",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "Client",
|
||||
},
|
||||
}, thirdPartyApis.Filters),
|
||||
},
|
||||
Expression: "error",
|
||||
GroupBy: getGroupBy([]v3.AttributeKey{
|
||||
{
|
||||
Key: serverNameKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
Disabled: true,
|
||||
ShiftBy: 0,
|
||||
IsAnomaly: false,
|
||||
}
|
||||
|
||||
builderQueries["total_span"] = &v3.BuilderQuery{
|
||||
QueryName: "total_span",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorCount,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "span_id",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
TimeAggregation: v3.TimeAggregationCount,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: getFilterSet([]v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "http.url",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: false,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
Value: "",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "kind_string",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "Client",
|
||||
},
|
||||
}, thirdPartyApis.Filters),
|
||||
},
|
||||
Expression: "total_span",
|
||||
GroupBy: getGroupBy([]v3.AttributeKey{
|
||||
{
|
||||
Key: "net.peer.name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
Disabled: true,
|
||||
ShiftBy: 0,
|
||||
IsAnomaly: false,
|
||||
}
|
||||
|
||||
builderQueries["p99"] = &v3.BuilderQuery{
|
||||
QueryName: "p99",
|
||||
Legend: "p99",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorP99,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "duration_nano",
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
IsColumn: true,
|
||||
},
|
||||
TimeAggregation: "p99",
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: getFilterSet([]v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: false,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
Value: "",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "kind_string",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "Client",
|
||||
},
|
||||
}, thirdPartyApis.Filters),
|
||||
},
|
||||
Expression: "p99",
|
||||
GroupBy: getGroupBy([]v3.AttributeKey{
|
||||
{
|
||||
Key: serverNameKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
ShiftBy: 0,
|
||||
IsAnomaly: false,
|
||||
}
|
||||
|
||||
builderQueries["error_rate"] = &v3.BuilderQuery{
|
||||
QueryName: "error_rate",
|
||||
Expression: "(error/total_span)*100",
|
||||
Legend: "error_rate",
|
||||
Disabled: false,
|
||||
ShiftBy: 0,
|
||||
IsAnomaly: false,
|
||||
}
|
||||
|
||||
compositeQuery := &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
PanelType: v3.PanelTypeTable,
|
||||
FillGaps: false,
|
||||
BuilderQueries: builderQueries,
|
||||
}
|
||||
|
||||
queryRangeParams := &v3.QueryRangeParamsV3{
|
||||
Start: unixMilliStart,
|
||||
End: unixMilliEnd,
|
||||
Step: defaultStepInterval,
|
||||
CompositeQuery: compositeQuery,
|
||||
Version: "v4",
|
||||
FormatForWeb: true,
|
||||
}
|
||||
|
||||
return queryRangeParams, nil
|
||||
}
|
||||
|
||||
func BuildDomainInfo(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, error) {
|
||||
unixMilliStart := thirdPartyApis.Start
|
||||
unixMilliEnd := thirdPartyApis.End
|
||||
|
||||
builderQueries := make(map[string]*v3.BuilderQuery)
|
||||
|
||||
builderQueries["endpoints"] = &v3.BuilderQuery{
|
||||
QueryName: "endpoints",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorCount,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: getFilterSet([]v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: false,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
Value: "",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "kind_string",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "Client",
|
||||
},
|
||||
}, thirdPartyApis.Filters),
|
||||
},
|
||||
Expression: "endpoints",
|
||||
Disabled: false,
|
||||
GroupBy: getGroupBy([]v3.AttributeKey{
|
||||
{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
}, thirdPartyApis.GroupBy),
|
||||
Legend: "",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
}
|
||||
|
||||
builderQueries["p99"] = &v3.BuilderQuery{
|
||||
QueryName: "p99",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorP99,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "duration_nano",
|
||||
DataType: v3.AttributeKeyDataTypeFloat64,
|
||||
IsColumn: true,
|
||||
},
|
||||
TimeAggregation: "p99",
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: getFilterSet([]v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: false,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
Value: "",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "kind_string",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "Client",
|
||||
},
|
||||
}, thirdPartyApis.Filters),
|
||||
},
|
||||
Expression: "p99",
|
||||
Disabled: false,
|
||||
Having: nil,
|
||||
GroupBy: getGroupBy([]v3.AttributeKey{}, thirdPartyApis.GroupBy),
|
||||
Legend: "",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
}
|
||||
|
||||
builderQueries["error_rate"] = &v3.BuilderQuery{
|
||||
QueryName: "error_rate",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorRate,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "",
|
||||
},
|
||||
TimeAggregation: v3.TimeAggregationRate,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: getFilterSet([]v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: false,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
Value: "",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "kind_string",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "Client",
|
||||
},
|
||||
}, thirdPartyApis.Filters),
|
||||
},
|
||||
Expression: "error_rate",
|
||||
Disabled: false,
|
||||
GroupBy: getGroupBy([]v3.AttributeKey{}, thirdPartyApis.GroupBy),
|
||||
Legend: "",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
}
|
||||
|
||||
builderQueries["lastseen"] = &v3.BuilderQuery{
|
||||
QueryName: "lastseen",
|
||||
DataSource: v3.DataSourceTraces,
|
||||
StepInterval: defaultStepInterval,
|
||||
AggregateOperator: v3.AggregateOperatorMax,
|
||||
AggregateAttribute: v3.AttributeKey{
|
||||
Key: "timestamp",
|
||||
},
|
||||
TimeAggregation: v3.TimeAggregationMax,
|
||||
SpaceAggregation: v3.SpaceAggregationSum,
|
||||
Filters: &v3.FilterSet{
|
||||
Operator: "AND",
|
||||
Items: getFilterSet([]v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: urlPathKey,
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: false,
|
||||
Type: v3.AttributeKeyTypeTag,
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
Value: "",
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "kind_string",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
IsColumn: true,
|
||||
},
|
||||
Operator: "=",
|
||||
Value: "Client",
|
||||
},
|
||||
}, thirdPartyApis.Filters),
|
||||
},
|
||||
Expression: "lastseen",
|
||||
Disabled: false,
|
||||
Having: nil,
|
||||
OrderBy: nil,
|
||||
GroupBy: getGroupBy([]v3.AttributeKey{}, thirdPartyApis.GroupBy),
|
||||
Legend: "",
|
||||
ReduceTo: v3.ReduceToOperatorAvg,
|
||||
}
|
||||
|
||||
compositeQuery := &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
PanelType: v3.PanelTypeTable,
|
||||
FillGaps: false,
|
||||
BuilderQueries: builderQueries,
|
||||
}
|
||||
|
||||
queryRangeParams := &v3.QueryRangeParamsV3{
|
||||
Start: unixMilliStart,
|
||||
End: unixMilliEnd,
|
||||
Step: defaultStepInterval,
|
||||
CompositeQuery: compositeQuery,
|
||||
Version: "v4",
|
||||
FormatForWeb: true,
|
||||
}
|
||||
|
||||
return queryRangeParams, nil
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
package thirdPartyApi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterResponse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []*v3.Result
|
||||
expected []*v3.Result
|
||||
}{
|
||||
{
|
||||
name: "should filter out IP addresses from net.peer.name",
|
||||
input: []*v3.Result{
|
||||
{
|
||||
Table: &v3.Table{
|
||||
Rows: []*v3.TableRow{
|
||||
{
|
||||
Data: map[string]interface{}{
|
||||
"net.peer.name": "192.168.1.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Data: map[string]interface{}{
|
||||
"net.peer.name": "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*v3.Result{
|
||||
{
|
||||
Table: &v3.Table{
|
||||
Rows: []*v3.TableRow{
|
||||
{
|
||||
Data: map[string]interface{}{
|
||||
"net.peer.name": "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle nil data",
|
||||
input: []*v3.Result{
|
||||
{
|
||||
Table: &v3.Table{
|
||||
Rows: []*v3.TableRow{
|
||||
{
|
||||
Data: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*v3.Result{
|
||||
{
|
||||
Table: &v3.Table{
|
||||
Rows: []*v3.TableRow{
|
||||
{
|
||||
Data: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := FilterResponse(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFilterSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
existingFilters []v3.FilterItem
|
||||
apiFilters v3.FilterSet
|
||||
expected []v3.FilterItem
|
||||
}{
|
||||
{
|
||||
name: "should append new filters",
|
||||
existingFilters: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "existing"},
|
||||
},
|
||||
},
|
||||
apiFilters: v3.FilterSet{
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "new"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "existing"},
|
||||
},
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "new"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle empty api filters",
|
||||
existingFilters: []v3.FilterItem{{Key: v3.AttributeKey{Key: "existing"}}},
|
||||
apiFilters: v3.FilterSet{},
|
||||
expected: []v3.FilterItem{{Key: v3.AttributeKey{Key: "existing"}}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := getFilterSet(tt.existingFilters, tt.apiFilters)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGroupBy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
existingGroup []v3.AttributeKey
|
||||
apiGroup []v3.AttributeKey
|
||||
expected []v3.AttributeKey
|
||||
}{
|
||||
{
|
||||
name: "should append new group by attributes",
|
||||
existingGroup: []v3.AttributeKey{
|
||||
{Key: "existing"},
|
||||
},
|
||||
apiGroup: []v3.AttributeKey{
|
||||
{Key: "new"},
|
||||
},
|
||||
expected: []v3.AttributeKey{
|
||||
{Key: "existing"},
|
||||
{Key: "new"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle empty api group",
|
||||
existingGroup: []v3.AttributeKey{{Key: "existing"}},
|
||||
apiGroup: []v3.AttributeKey{},
|
||||
expected: []v3.AttributeKey{{Key: "existing"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := getGroupBy(tt.existingGroup, tt.apiGroup)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDomainList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *ThirdPartyApis
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic domain list query",
|
||||
input: &ThirdPartyApis{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with filters and group by",
|
||||
input: &ThirdPartyApis{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Filters: v3.FilterSet{
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{Key: "test"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := BuildDomainList(tt.input)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, tt.input.Start, result.Start)
|
||||
assert.Equal(t, tt.input.End, result.End)
|
||||
assert.NotNil(t, result.CompositeQuery)
|
||||
assert.NotNil(t, result.CompositeQuery.BuilderQueries)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDomainInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *ThirdPartyApis
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic domain info query",
|
||||
input: &ThirdPartyApis{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with filters and group by",
|
||||
input: &ThirdPartyApis{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Filters: v3.FilterSet{
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{Key: "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []v3.AttributeKey{
|
||||
{Key: "test"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := BuildDomainInfo(tt.input)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, tt.input.Start, result.Start)
|
||||
assert.Equal(t, tt.input.End, result.End)
|
||||
assert.NotNil(t, result.CompositeQuery)
|
||||
assert.NotNil(t, result.CompositeQuery.BuilderQueries)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/SigNoz/signoz/pkg/types/thirdpartyapitypes"
|
||||
"math"
|
||||
"net/http"
|
||||
"sort"
|
||||
@@ -14,16 +15,15 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/govaluate"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/kafka"
|
||||
queues2 "github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/queues"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations/thirdPartyApi"
|
||||
|
||||
"github.com/SigNoz/govaluate"
|
||||
"github.com/gorilla/mux"
|
||||
promModel "github.com/prometheus/common/model"
|
||||
"go.uber.org/multierr"
|
||||
"go.uber.org/zap"
|
||||
|
||||
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/metrics"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
@@ -981,10 +981,15 @@ func ParseQueueBody(r *http.Request) (*queues2.QueueListRequest, *model.ApiError
|
||||
}
|
||||
|
||||
// ParseRequestBody for third party APIs
|
||||
func ParseRequestBody(r *http.Request) (*thirdPartyApi.ThirdPartyApis, *model.ApiError) {
|
||||
thirdPartApis := new(thirdPartyApi.ThirdPartyApis)
|
||||
if err := json.NewDecoder(r.Body).Decode(thirdPartApis); err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot parse the request body: %v", err)}
|
||||
func ParseRequestBody(r *http.Request) (*thirdpartyapitypes.ThirdPartyApiRequest, error) {
|
||||
req := new(thirdpartyapitypes.ThirdPartyApiRequest)
|
||||
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||
return nil, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "cannot parse the request body: %v", err)
|
||||
}
|
||||
return thirdPartApis, nil
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
@@ -164,6 +164,7 @@ func (b *resourceFilterStatementBuilder[T]) addConditions(
|
||||
FullTextColumn: b.fullTextColumn,
|
||||
JsonBodyPrefix: b.jsonBodyPrefix,
|
||||
JsonKeyToKey: b.jsonKeyToKey,
|
||||
OnlyResourceFilter: true, // Only process resource terms
|
||||
SkipFullTextFilter: true,
|
||||
SkipFunctionCalls: true,
|
||||
// there is no need for "key" not found error for resource filtering
|
||||
|
||||
@@ -18,6 +18,199 @@ import (
|
||||
|
||||
var searchTroubleshootingGuideURL = "https://signoz.io/docs/userguide/search-troubleshooting/"
|
||||
|
||||
// BooleanExpression represents a boolean expression with proper evaluation context
|
||||
type BooleanExpression struct {
|
||||
SQL string
|
||||
IsTrue bool
|
||||
IsEmpty bool
|
||||
}
|
||||
|
||||
// NewBooleanExpression creates a BooleanExpression from SQL
|
||||
func NewBooleanExpression(sql string) BooleanExpression {
|
||||
return BooleanExpression{
|
||||
SQL: sql,
|
||||
IsTrue: sql == "true",
|
||||
IsEmpty: sql == "",
|
||||
}
|
||||
}
|
||||
|
||||
// booleanEvaluatingVisitor is a specialized visitor for resource filter context
|
||||
// that properly applies boolean algebra during tree traversal
|
||||
type booleanEvaluatingVisitor struct {
|
||||
*filterExpressionVisitor
|
||||
}
|
||||
|
||||
func newBooleanEvaluatingVisitor(opts FilterExprVisitorOpts) *booleanEvaluatingVisitor {
|
||||
return &booleanEvaluatingVisitor{
|
||||
filterExpressionVisitor: newFilterExpressionVisitor(opts),
|
||||
}
|
||||
}
|
||||
|
||||
// Visit dispatches to boolean-aware visit methods
|
||||
func (v *booleanEvaluatingVisitor) Visit(tree antlr.ParseTree) any {
|
||||
if tree == nil {
|
||||
return NewBooleanExpression("")
|
||||
}
|
||||
|
||||
switch t := tree.(type) {
|
||||
case *grammar.QueryContext:
|
||||
return v.VisitQuery(t)
|
||||
case *grammar.ExpressionContext:
|
||||
return v.VisitExpression(t)
|
||||
case *grammar.OrExpressionContext:
|
||||
return v.VisitOrExpression(t)
|
||||
case *grammar.AndExpressionContext:
|
||||
return v.VisitAndExpression(t)
|
||||
case *grammar.UnaryExpressionContext:
|
||||
return v.VisitUnaryExpression(t)
|
||||
case *grammar.PrimaryContext:
|
||||
return v.VisitPrimary(t)
|
||||
default:
|
||||
// For leaf nodes, delegate to original visitor and wrap result
|
||||
result := v.filterExpressionVisitor.Visit(tree)
|
||||
if sql, ok := result.(string); ok {
|
||||
return NewBooleanExpression(sql)
|
||||
}
|
||||
return NewBooleanExpression("")
|
||||
}
|
||||
}
|
||||
|
||||
func (v *booleanEvaluatingVisitor) VisitQuery(ctx *grammar.QueryContext) any {
|
||||
return v.Visit(ctx.Expression())
|
||||
}
|
||||
|
||||
func (v *booleanEvaluatingVisitor) VisitExpression(ctx *grammar.ExpressionContext) any {
|
||||
return v.Visit(ctx.OrExpression())
|
||||
}
|
||||
|
||||
func (v *booleanEvaluatingVisitor) VisitOrExpression(ctx *grammar.OrExpressionContext) any {
|
||||
andExpressions := ctx.AllAndExpression()
|
||||
|
||||
var result BooleanExpression
|
||||
hasTrue := false
|
||||
hasEmpty := false
|
||||
|
||||
for i, expr := range andExpressions {
|
||||
exprResult := v.Visit(expr).(BooleanExpression)
|
||||
if exprResult.IsTrue {
|
||||
hasTrue = true
|
||||
}
|
||||
if exprResult.IsEmpty {
|
||||
hasEmpty = true
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
result = exprResult
|
||||
} else {
|
||||
if result.IsEmpty {
|
||||
result = exprResult
|
||||
} else if !exprResult.IsEmpty {
|
||||
sql := v.builder.Or(result.SQL, exprResult.SQL)
|
||||
result = NewBooleanExpression(sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In resource filter context, if any operand is empty (meaning "include all resources"),
|
||||
// the entire OR should be empty (include all resources)
|
||||
if hasEmpty && v.onlyResourceFilter {
|
||||
result.IsEmpty = true
|
||||
result.IsTrue = true
|
||||
result.SQL = ""
|
||||
} else if hasTrue {
|
||||
// Mark as always true if any operand is true, but preserve the SQL structure
|
||||
result.IsTrue = true
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *booleanEvaluatingVisitor) VisitAndExpression(ctx *grammar.AndExpressionContext) any {
|
||||
unaryExpressions := ctx.AllUnaryExpression()
|
||||
|
||||
var result BooleanExpression
|
||||
allTrue := true
|
||||
|
||||
for i, expr := range unaryExpressions {
|
||||
exprResult := v.Visit(expr).(BooleanExpression)
|
||||
if !exprResult.IsTrue && !exprResult.IsEmpty {
|
||||
allTrue = false
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
result = exprResult
|
||||
} else {
|
||||
// Apply boolean AND logic
|
||||
if exprResult.IsTrue {
|
||||
// A AND true = A, continue with result
|
||||
continue
|
||||
}
|
||||
if result.IsTrue {
|
||||
result = exprResult
|
||||
} else if result.IsEmpty {
|
||||
result = exprResult
|
||||
} else if !exprResult.IsEmpty {
|
||||
sql := v.builder.And(result.SQL, exprResult.SQL)
|
||||
result = NewBooleanExpression(sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all terms were "true", mark the result as always true
|
||||
if allTrue && len(unaryExpressions) > 0 {
|
||||
result.IsTrue = true
|
||||
if result.SQL == "" {
|
||||
result.SQL = "true"
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *booleanEvaluatingVisitor) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) any {
|
||||
result := v.Visit(ctx.Primary()).(BooleanExpression)
|
||||
|
||||
if ctx.NOT() != nil {
|
||||
// Apply NOT logic with resource filter context awareness
|
||||
if v.onlyResourceFilter {
|
||||
if result.IsTrue {
|
||||
return NewBooleanExpression("") // NOT(true) = include all resources
|
||||
}
|
||||
if result.IsEmpty {
|
||||
return NewBooleanExpression("") // NOT(empty) = include all resources
|
||||
}
|
||||
}
|
||||
|
||||
sql := fmt.Sprintf("NOT (%s)", result.SQL)
|
||||
return NewBooleanExpression(sql)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *booleanEvaluatingVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any {
|
||||
if ctx.OrExpression() != nil {
|
||||
result := v.Visit(ctx.OrExpression()).(BooleanExpression)
|
||||
// If no boolean simplification happened, preserve original parentheses structure
|
||||
if !result.IsEmpty && !result.IsTrue {
|
||||
// Use original visitor to get proper parentheses structure
|
||||
originalSQL := v.filterExpressionVisitor.Visit(ctx)
|
||||
if sql, ok := originalSQL.(string); ok && sql != "" {
|
||||
return NewBooleanExpression(sql)
|
||||
}
|
||||
result.SQL = fmt.Sprintf("(%s)", result.SQL)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// For other cases, delegate to original visitor
|
||||
sqlResult := v.filterExpressionVisitor.Visit(ctx)
|
||||
if sql, ok := sqlResult.(string); ok {
|
||||
return NewBooleanExpression(sql)
|
||||
}
|
||||
return NewBooleanExpression("")
|
||||
}
|
||||
|
||||
// filterExpressionVisitor implements the FilterQueryVisitor interface
|
||||
// to convert the parsed filter expressions into ClickHouse WHERE clause
|
||||
type filterExpressionVisitor struct {
|
||||
@@ -34,6 +227,7 @@ type filterExpressionVisitor struct {
|
||||
jsonBodyPrefix string
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
|
||||
skipResourceFilter bool
|
||||
onlyResourceFilter bool
|
||||
skipFullTextFilter bool
|
||||
skipFunctionCalls bool
|
||||
ignoreNotFoundKeys bool
|
||||
@@ -52,6 +246,7 @@ type FilterExprVisitorOpts struct {
|
||||
JsonBodyPrefix string
|
||||
JsonKeyToKey qbtypes.JsonKeyToFieldFunc
|
||||
SkipResourceFilter bool
|
||||
OnlyResourceFilter bool // Only process resource terms, skip non-resource terms
|
||||
SkipFullTextFilter bool
|
||||
SkipFunctionCalls bool
|
||||
IgnoreNotFoundKeys bool
|
||||
@@ -70,6 +265,7 @@ func newFilterExpressionVisitor(opts FilterExprVisitorOpts) *filterExpressionVis
|
||||
jsonBodyPrefix: opts.JsonBodyPrefix,
|
||||
jsonKeyToKey: opts.JsonKeyToKey,
|
||||
skipResourceFilter: opts.SkipResourceFilter,
|
||||
onlyResourceFilter: opts.OnlyResourceFilter,
|
||||
skipFullTextFilter: opts.SkipFullTextFilter,
|
||||
skipFunctionCalls: opts.SkipFunctionCalls,
|
||||
ignoreNotFoundKeys: opts.IgnoreNotFoundKeys,
|
||||
@@ -95,8 +291,6 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWher
|
||||
opts.Builder = sb
|
||||
}
|
||||
|
||||
visitor := newFilterExpressionVisitor(opts)
|
||||
|
||||
// Set up error handling
|
||||
lexerErrorListener := NewErrorListener()
|
||||
lexer.RemoveErrorListeners()
|
||||
@@ -111,6 +305,17 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWher
|
||||
// Parse the query
|
||||
tree := parser.Query()
|
||||
|
||||
// override skipResourceFilter if the expression contains OR
|
||||
for _, tok := range tokens.GetAllTokens() {
|
||||
if tok.GetTokenType() == grammar.FilterQueryParserOR {
|
||||
opts.SkipResourceFilter = false
|
||||
break
|
||||
}
|
||||
}
|
||||
tokens.Reset()
|
||||
|
||||
visitor := newFilterExpressionVisitor(opts)
|
||||
|
||||
// Handle syntax errors
|
||||
if len(parserErrorListener.SyntaxErrors) > 0 {
|
||||
combinedErrors := errors.Newf(
|
||||
@@ -151,6 +356,31 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWher
|
||||
cond = "true"
|
||||
}
|
||||
|
||||
// In resource filter context, apply robust boolean evaluation only when needed
|
||||
if opts.OnlyResourceFilter {
|
||||
// Check if the condition contains patterns that need boolean simplification
|
||||
// We need boolean evaluation when:
|
||||
// 1. Expression contains " true" (indicating simplified non-resource terms)
|
||||
// 2. Expression is exactly "true"
|
||||
// 3. Expression contains "NOT" with true values that need simplification
|
||||
needsBooleanEval := strings.Contains(cond, " true") ||
|
||||
cond == "true" ||
|
||||
(strings.Contains(cond, "NOT") && strings.Contains(cond, "true"))
|
||||
|
||||
if needsBooleanEval {
|
||||
// Re-parse and evaluate with boolean algebra
|
||||
boolVisitor := newBooleanEvaluatingVisitor(opts)
|
||||
boolResult := boolVisitor.Visit(tree)
|
||||
if boolExpr, ok := boolResult.(BooleanExpression); ok {
|
||||
if boolExpr.IsEmpty {
|
||||
cond = "true" // Empty means include all resources
|
||||
} else {
|
||||
cond = boolExpr.SQL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
whereClause := sqlbuilder.NewWhereClause().AddWhereExpr(visitor.builder.Args, cond)
|
||||
|
||||
return &PreparedWhereClause{whereClause, visitor.warnings, visitor.mainWarnURL}, nil
|
||||
@@ -217,22 +447,23 @@ func (v *filterExpressionVisitor) VisitExpression(ctx *grammar.ExpressionContext
|
||||
func (v *filterExpressionVisitor) VisitOrExpression(ctx *grammar.OrExpressionContext) any {
|
||||
andExpressions := ctx.AllAndExpression()
|
||||
|
||||
andExpressionConditions := make([]string, len(andExpressions))
|
||||
for i, expr := range andExpressions {
|
||||
validConditions := []string{}
|
||||
|
||||
for _, expr := range andExpressions {
|
||||
if condExpr, ok := v.Visit(expr).(string); ok && condExpr != "" {
|
||||
andExpressionConditions[i] = condExpr
|
||||
validConditions = append(validConditions, condExpr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(andExpressionConditions) == 0 {
|
||||
if len(validConditions) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(andExpressionConditions) == 1 {
|
||||
return andExpressionConditions[0]
|
||||
if len(validConditions) == 1 {
|
||||
return validConditions[0]
|
||||
}
|
||||
|
||||
return v.builder.Or(andExpressionConditions...)
|
||||
return v.builder.Or(validConditions...)
|
||||
}
|
||||
|
||||
// VisitAndExpression handles AND expressions
|
||||
@@ -263,6 +494,17 @@ func (v *filterExpressionVisitor) VisitUnaryExpression(ctx *grammar.UnaryExpress
|
||||
|
||||
// Check if this is a NOT expression
|
||||
if ctx.NOT() != nil {
|
||||
// In resource filter context, handle NOT specially
|
||||
if v.onlyResourceFilter {
|
||||
// NOT(true) means NOT(all non-resource terms) which means "include all resources"
|
||||
if result == "true" {
|
||||
return "" // No filtering = include all resources
|
||||
}
|
||||
// NOT(empty) should return empty (no filtering)
|
||||
if result == "" {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("NOT (%s)", result)
|
||||
}
|
||||
|
||||
@@ -274,7 +516,7 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
|
||||
if ctx.OrExpression() != nil {
|
||||
// This is a parenthesized expression
|
||||
if condExpr, ok := v.Visit(ctx.OrExpression()).(string); ok && condExpr != "" {
|
||||
return fmt.Sprintf("(%s)", v.Visit(ctx.OrExpression()).(string))
|
||||
return fmt.Sprintf("(%s)", condExpr)
|
||||
}
|
||||
return ""
|
||||
} else if ctx.Comparison() != nil {
|
||||
@@ -356,6 +598,22 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
||||
}
|
||||
}
|
||||
|
||||
// this is used to only process resource terms in resource filter context
|
||||
if v.onlyResourceFilter {
|
||||
filteredKeys := []*telemetrytypes.TelemetryFieldKey{}
|
||||
for _, key := range keys {
|
||||
if key.FieldContext == telemetrytypes.FieldContextResource {
|
||||
filteredKeys = append(filteredKeys, key)
|
||||
}
|
||||
}
|
||||
keys = filteredKeys
|
||||
if len(keys) == 0 {
|
||||
// For non-resource terms in resource filter context, return "true"
|
||||
// This ensures OR expressions work correctly (e.g., resource OR non-resource)
|
||||
return "true"
|
||||
}
|
||||
}
|
||||
|
||||
// Handle EXISTS specially
|
||||
if ctx.EXISTS() != nil {
|
||||
op := qbtypes.FilterOperatorExists
|
||||
|
||||
@@ -74,6 +74,35 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Time series with OR b/w resource attr and attribute filter",
|
||||
requestType: qbtypes.RequestTypeTimeSeries,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
||||
Aggregations: []qbtypes.LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name = 'redis-manual' OR http.method = 'GET'",
|
||||
},
|
||||
Limit: 10,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "service.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "redis-manual", true, "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, true, "redis-manual", true, "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Time series with limit + custom order by",
|
||||
requestType: qbtypes.RequestTypeTimeSeries,
|
||||
@@ -113,6 +142,111 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Time series with NOT predicate containing only non-resource terms",
|
||||
requestType: qbtypes.RequestTypeTimeSeries,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Aggregations: []qbtypes.LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "NOT (message CONTAINS 'foo' AND hasToken(body, 'bar'))",
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND NOT ((((LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?) AND hasToken(LOWER(body), LOWER(?))))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "%foo%", true, "bar", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Time series with NOT OR mixed resource/non-resource terms",
|
||||
requestType: qbtypes.RequestTypeTimeSeries,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Aggregations: []qbtypes.LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "NOT (service.name = 'redis-manual' OR http.method = 'GET')",
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND NOT ((((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "redis-manual", true, "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Time series with complex NOT expression and nested OR conditions",
|
||||
requestType: qbtypes.RequestTypeTimeSeries,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Aggregations: []qbtypes.LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name IN 'redis' AND request.type = 'External' AND http.status_code < 500 AND http.status_code >= 400 AND NOT ((http.request.header.tenant_id = '[\"tenant-1\"]' AND http.status_code = 401) OR (http.request.header.tenant_id = '[\"tenant-2\"]' AND http.status_code = 404 AND http.route = '/tenants/{tenant_id}'))",
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ?) AND labels LIKE ? AND (labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((resources_string['service.name'] = ?) AND mapContains(resources_string, 'service.name') = ?) AND (attributes_string['request.type'] = ? AND mapContains(attributes_string, 'request.type') = ?) AND (toFloat64(attributes_number['http.status_code']) < ? AND mapContains(attributes_number, 'http.status_code') = ?) AND (toFloat64(attributes_number['http.status_code']) >= ? AND mapContains(attributes_number, 'http.status_code') = ?) AND NOT ((((((attributes_string['http.request.header.tenant_id'] = ? AND mapContains(attributes_string, 'http.request.header.tenant_id') = ?) AND (toFloat64(attributes_number['http.status_code']) = ? AND mapContains(attributes_number, 'http.status_code') = ?))) OR (((attributes_string['http.request.header.tenant_id'] = ? AND mapContains(attributes_string, 'http.request.header.tenant_id') = ?) AND (toFloat64(attributes_number['http.status_code']) = ? AND mapContains(attributes_number, 'http.status_code') = ?) AND (attributes_string['http.route'] = ? AND mapContains(attributes_string, 'http.route') = ?))))))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts",
|
||||
Args: []any{"redis", "%service.name%", "%service.name\":\"redis%", uint64(1747945619), uint64(1747983448), "redis", true, "External", true, float64(500), true, float64(400), true, "[\"tenant-1\"]", true, float64(401), true, "[\"tenant-2\"]", true, float64(404), true, "/tenants/{tenant_id}", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Time series with complex OR expression containing NOT with nested conditions",
|
||||
requestType: qbtypes.RequestTypeTimeSeries,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Aggregations: []qbtypes.LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "(service.name IN 'redis' AND request.type = 'External' AND http.status_code < 500 AND http.status_code >= 400 OR NOT ((http.request.header.tenant_id = '[\"tenant-1\"]' AND http.status_code = 401) OR (http.request.header.tenant_id = '[\"tenant-2\"]' AND http.status_code = 404 AND http.route = '/tenants/{tenant_id}')))",
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((((resources_string['service.name'] = ?) AND mapContains(resources_string, 'service.name') = ?) AND (attributes_string['request.type'] = ? AND mapContains(attributes_string, 'request.type') = ?) AND (toFloat64(attributes_number['http.status_code']) < ? AND mapContains(attributes_number, 'http.status_code') = ?) AND (toFloat64(attributes_number['http.status_code']) >= ? AND mapContains(attributes_number, 'http.status_code') = ?)) OR NOT ((((((attributes_string['http.request.header.tenant_id'] = ? AND mapContains(attributes_string, 'http.request.header.tenant_id') = ?) AND (toFloat64(attributes_number['http.status_code']) = ? AND mapContains(attributes_number, 'http.status_code') = ?))) OR (((attributes_string['http.request.header.tenant_id'] = ? AND mapContains(attributes_string, 'http.request.header.tenant_id') = ?) AND (toFloat64(attributes_number['http.status_code']) = ? AND mapContains(attributes_number, 'http.status_code') = ?) AND (attributes_string['http.route'] = ? AND mapContains(attributes_string, 'http.route') = ?)))))))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts",
|
||||
Args: []any{uint64(1747945619), uint64(1747983448), "redis", true, "External", true, float64(500), true, float64(400), true, "[\"tenant-1\"]", true, float64(401), true, "[\"tenant-2\"]", true, float64(404), true, "/tenants/{tenant_id}", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Time series with OR between multiple resource conditions",
|
||||
requestType: qbtypes.RequestTypeTimeSeries,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Aggregations: []qbtypes.LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name = 'redis' OR service.name = 'driver'",
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts",
|
||||
Args: []any{"redis", "%service.name%", "%service.name\":\"redis%", "driver", "%service.name%", "%service.name\":\"driver%", uint64(1747945619), uint64(1747983448), "redis", true, "driver", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
|
||||
@@ -862,6 +862,27 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
|
||||
Materialized: true,
|
||||
},
|
||||
},
|
||||
"request.type": {
|
||||
{
|
||||
Name: "request.type",
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
},
|
||||
"http.request.header.tenant_id": {
|
||||
{
|
||||
Name: "http.request.header.tenant_id",
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
},
|
||||
"http.route": {
|
||||
{
|
||||
Name: "http.route",
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, keys := range keysMap {
|
||||
|
||||
@@ -89,8 +89,8 @@ func TestStatementBuilder(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "redis-manual", true, "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "redis-manual", true, "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
|
||||
@@ -145,6 +145,25 @@ func (q *QueryBuilderQuery[T]) Validate(requestType RequestType) error {
|
||||
}
|
||||
}
|
||||
|
||||
if requestType == RequestTypeRaw {
|
||||
if err := q.validateSelectFields(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *QueryBuilderQuery[T]) validateSelectFields() error {
|
||||
// isRoot and isEntryPoint are returned by the Metadata API, so if someone sends them, we have to reject the request.
|
||||
for _, v := range q.SelectFields {
|
||||
if v.Name == "isRoot" || v.Name == "isEntryPoint" {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"isRoot and isEntryPoint fields are not supported in selectFields",
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
29
pkg/types/thirdpartyapitypes/thirdpartyapi.go
Normal file
29
pkg/types/thirdpartyapitypes/thirdpartyapi.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package thirdpartyapitypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
type ThirdPartyApiRequest struct {
|
||||
Start uint64 `json:"start"`
|
||||
End uint64 `json:"end"`
|
||||
ShowIp bool `json:"show_ip,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
Filter *qbtypes.Filter `json:"filters,omitempty"`
|
||||
GroupBy []qbtypes.GroupByKey `json:"groupBy,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates the ThirdPartyApiRequest
|
||||
func (req *ThirdPartyApiRequest) Validate() error {
|
||||
if req.Start >= req.End {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "start time must be before end time")
|
||||
}
|
||||
|
||||
if req.Filter != nil && req.Filter.Expression == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "filter expression cannot be empty when filter is provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<p>Hi {{.CustomerName}},</p>
|
||||
|
||||
<p>We wanted to inform you that your role in the <strong>SigNoz</strong> project has been updated by <strong>{{.UpdatedByEmail}}</strong>.</p>
|
||||
Hi {{.CustomerName}},<br>
|
||||
Your role in <strong>SigNoz</strong> has been updated by {{.UpdatedByEmail}}.
|
||||
|
||||
<p>
|
||||
<strong>Previous Role:</strong> {{.OldRole}}<br>
|
||||
@@ -11,13 +10,19 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please note that you will need to <strong>log out and log back in</strong> for the changes to take effect.
|
||||
Please note that you will need to log out and log back in for the changes to take effect.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you were not expecting this change or have any questions, please reach out to your project administrator or contact us at <a href="mailto:support@signoz.io">support@signoz.io</a>.
|
||||
</p>
|
||||
{{if eq .OldRole "Admin"}}
|
||||
<p>
|
||||
If you were not expecting this change or have any questions, please contact us at <a href="mailto:support@signoz.io">support@signoz.io</a>.
|
||||
</p>
|
||||
{{else}}
|
||||
<p>
|
||||
If you were not expecting this change or have any questions, please reach out to your administrator.
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
<p>Thanks,<br/>The SigNoz Team</p>
|
||||
<p>Best regards,<br>The SigNoz Team</p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user