Compare commits
12 Commits
improveTra
...
temporalit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4699b1fed | ||
|
|
87ce197631 | ||
|
|
3cc5a24a4b | ||
|
|
9b8a892079 | ||
|
|
396e0cdc2d | ||
|
|
c838d7e2d4 | ||
|
|
1a193fb1a9 | ||
|
|
88dff3f552 | ||
|
|
5bb6d78c42 | ||
|
|
369f77977d | ||
|
|
836605def5 | ||
|
|
cc80923265 |
@@ -57,7 +57,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
|
||||
FluxInterval: opts.FluxInterval,
|
||||
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
|
||||
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
||||
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
|
||||
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore, signoz.Cache),
|
||||
Signoz: signoz,
|
||||
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics),
|
||||
})
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/sonner": "0.1.0",
|
||||
"@signozhq/table": "0.3.4",
|
||||
"@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;
|
||||
@@ -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}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ColumnDef, DataTable, Row } from '@signozhq/table';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
@@ -5,14 +6,17 @@ 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';
|
||||
|
||||
@@ -60,6 +64,8 @@ function ColumnView({
|
||||
| undefined
|
||||
>();
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
useEffect(() => {
|
||||
if (activeLogId) {
|
||||
const log = logs.find(({ id }) => id === activeLogId);
|
||||
@@ -134,7 +140,7 @@ function ColumnView({
|
||||
enableInfiniteScroll: true,
|
||||
enableScrollRestoration: false,
|
||||
fixedHeight: isFrequencyChartVisible ? 560 : 760,
|
||||
enableDynamicRowHeight: false,
|
||||
enableDynamicRowHeight: true,
|
||||
};
|
||||
|
||||
const selectedColumns = useMemo(
|
||||
@@ -148,8 +154,10 @@ function ColumnView({
|
||||
// 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 : 1080,
|
||||
pin: field.key === 'state-indicator' ? 'left' : 'none',
|
||||
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,
|
||||
@@ -165,13 +173,28 @@ function ColumnView({
|
||||
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' : ''
|
||||
}`}
|
||||
>
|
||||
{getValue()}
|
||||
{cellContent}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
color: white !important;
|
||||
|
||||
.cursor-col-resize {
|
||||
width: 2px !important;
|
||||
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;
|
||||
|
||||
@@ -27,7 +27,7 @@ import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import NoLogs from '../NoLogs/NoLogs';
|
||||
import ColumnView from './ColumnView/ColumnView';
|
||||
import InfinityTableView from './InfinityTableView';
|
||||
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
|
||||
import { InfinityWrapperStyled } from './styles';
|
||||
import {
|
||||
@@ -48,7 +48,6 @@ function LogsExplorerList({
|
||||
isError,
|
||||
error,
|
||||
isFilterApplied,
|
||||
isFrequencyChartVisible,
|
||||
}: LogsExplorerListProps): JSX.Element {
|
||||
const ref = useRef<VirtuosoHandle>(null);
|
||||
|
||||
@@ -130,6 +129,75 @@ function LogsExplorerList({
|
||||
],
|
||||
);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
const components = isLoading
|
||||
? {
|
||||
Footer,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (options.format === 'table') {
|
||||
return (
|
||||
<InfinityTableView
|
||||
ref={ref}
|
||||
isLoading={isLoading}
|
||||
tableViewProps={{
|
||||
logs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLines,
|
||||
fontSize: options.fontSize,
|
||||
appendTo: 'end',
|
||||
activeLogIndex,
|
||||
}}
|
||||
infitiyTableProps={{ onEndReached }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getMarginTop(): string {
|
||||
switch (options.fontSize) {
|
||||
case FontSize.SMALL:
|
||||
return '10px';
|
||||
case FontSize.MEDIUM:
|
||||
return '12px';
|
||||
case FontSize.LARGE:
|
||||
return '15px';
|
||||
default:
|
||||
return '15px';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{ width: '100%', marginTop: getMarginTop() }}
|
||||
bodyStyle={CARD_BODY_STYLE}
|
||||
>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
key={activeLogIndex || 'logs-virtuoso'}
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
endReached={onEndReached}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
components={components}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
);
|
||||
}, [
|
||||
isLoading,
|
||||
options.format,
|
||||
options.maxLines,
|
||||
options.fontSize,
|
||||
activeLogIndex,
|
||||
logs,
|
||||
onEndReached,
|
||||
getItemContent,
|
||||
selectedFields,
|
||||
]);
|
||||
|
||||
const isTraceToLogsNavigation = useMemo(() => {
|
||||
if (!currentStagedQueryData) return false;
|
||||
return isTraceToLogsQuery(currentStagedQueryData);
|
||||
@@ -169,83 +237,6 @@ function LogsExplorerList({
|
||||
return getEmptyLogsListConfig(handleClearFilters);
|
||||
}, [isTraceToLogsNavigation, handleClearFilters]);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (isLoading || isFetching) return;
|
||||
|
||||
onEndReached(logs.length);
|
||||
}, [isLoading, isFetching, onEndReached, logs.length]);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
const components = isLoading
|
||||
? {
|
||||
Footer,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (options.format === 'table') {
|
||||
return (
|
||||
<ColumnView
|
||||
logs={logs}
|
||||
onLoadMore={handleLoadMore}
|
||||
selectedFields={selectedFields}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
options={{
|
||||
maxLinesPerRow: options.maxLines,
|
||||
fontSize: options.fontSize,
|
||||
}}
|
||||
isFrequencyChartVisible={isFrequencyChartVisible}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getMarginTop(): string {
|
||||
switch (options.fontSize) {
|
||||
case FontSize.SMALL:
|
||||
return '10px';
|
||||
case FontSize.MEDIUM:
|
||||
return '12px';
|
||||
case FontSize.LARGE:
|
||||
return '15px';
|
||||
default:
|
||||
return '15px';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<InfinityWrapperStyled data-testid="logs-list-virtuoso">
|
||||
<Card
|
||||
style={{ width: '100%', marginTop: getMarginTop() }}
|
||||
bodyStyle={CARD_BODY_STYLE}
|
||||
>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
key={activeLogIndex || 'logs-virtuoso'}
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
endReached={onEndReached}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
components={components}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
</InfinityWrapperStyled>
|
||||
);
|
||||
}, [
|
||||
isLoading,
|
||||
activeLogIndex,
|
||||
handleLoadMore,
|
||||
isFetching,
|
||||
logs,
|
||||
onEndReached,
|
||||
getItemContent,
|
||||
selectedFields,
|
||||
isFrequencyChartVisible,
|
||||
options,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="logs-list-view-container">
|
||||
{(isLoading || (isFetching && logs.length === 0)) && <LogsLoading />}
|
||||
@@ -274,7 +265,9 @@ function LogsExplorerList({
|
||||
|
||||
{!isLoading && !isError && logs.length > 0 && (
|
||||
<>
|
||||
{renderContent}
|
||||
<InfinityWrapperStyled data-testid="logs-list-virtuoso">
|
||||
{renderContent}
|
||||
</InfinityWrapperStyled>
|
||||
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
@@ -4299,13 +4324,14 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/table@0.3.4":
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/table/-/table-0.3.4.tgz#5f243f2977f21fe351207d4ae6db390e400a7933"
|
||||
integrity sha512-5B3kxYrfNEE9TH1orxAl6CNlESnyNVOgEW08ji9u2kz+khUd0euMCnZPrTBbotRW/INoEwRQxyBPj3auaa3jjQ==
|
||||
"@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"
|
||||
@@ -4316,6 +4342,20 @@
|
||||
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"
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||
@@ -25,10 +26,12 @@ type API struct {
|
||||
func NewAPI(
|
||||
settings factory.ProviderSettings,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
cache cache.Cache,
|
||||
) *API {
|
||||
telemetryMetadataStore := telemetrymetadata.NewTelemetryMetaStore(
|
||||
settings,
|
||||
telemetryStore,
|
||||
cache,
|
||||
telemetrytraces.DBName,
|
||||
telemetrytraces.TagAttributesV2TableName,
|
||||
telemetrytraces.SpanAttributesKeysTblName,
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
var metricTemporality map[string]metrictypes.Temporality
|
||||
if len(metricNames) > 0 {
|
||||
var err error
|
||||
metricTemporality, err = q.metadataStore.FetchTemporalityMulti(ctx, metricNames...)
|
||||
metricTemporality, err = q.metadataStore.FetchTemporalityMulti(ctx, orgID, metricNames...)
|
||||
if err != nil {
|
||||
q.logger.WarnContext(ctx, "failed to fetch metric temporality", "error", err, "metrics", metricNames)
|
||||
// Continue without temporality - statement builder will handle unspecified
|
||||
|
||||
@@ -48,6 +48,7 @@ func newProvider(
|
||||
telemetryMetadataStore := telemetrymetadata.NewTelemetryMetaStore(
|
||||
settings,
|
||||
telemetryStore,
|
||||
cache,
|
||||
telemetrytraces.DBName,
|
||||
telemetrytraces.TagAttributesV2TableName,
|
||||
telemetrytraces.SpanAttributesKeysTblName,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
|
||||
FluxInterval: config.Querier.FluxInterval,
|
||||
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
|
||||
LicensingAPI: nooplicensing.NewLicenseAPI(),
|
||||
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
|
||||
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore, signoz.Cache),
|
||||
Signoz: signoz,
|
||||
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics),
|
||||
})
|
||||
|
||||
@@ -95,8 +95,6 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWher
|
||||
opts.Builder = sb
|
||||
}
|
||||
|
||||
visitor := newFilterExpressionVisitor(opts)
|
||||
|
||||
// Set up error handling
|
||||
lexerErrorListener := NewErrorListener()
|
||||
lexer.RemoveErrorListeners()
|
||||
@@ -111,6 +109,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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
@@ -51,6 +54,7 @@ type telemetryMetaStore struct {
|
||||
|
||||
fm qbtypes.FieldMapper
|
||||
conditionBuilder qbtypes.ConditionBuilder
|
||||
temporalityCache *TemporalityCache
|
||||
}
|
||||
|
||||
func escapeForLike(s string) string {
|
||||
@@ -60,6 +64,7 @@ func escapeForLike(s string) string {
|
||||
func NewTelemetryMetaStore(
|
||||
settings factory.ProviderSettings,
|
||||
telemetrystore telemetrystore.TelemetryStore,
|
||||
cache cache.Cache,
|
||||
tracesDBName string,
|
||||
tracesFieldsTblName string,
|
||||
spanAttributesKeysTblName string,
|
||||
@@ -104,6 +109,34 @@ func NewTelemetryMetaStore(
|
||||
t.fm = fm
|
||||
t.conditionBuilder = conditionBuilder
|
||||
|
||||
if cache != nil &&
|
||||
strings.ToLower(os.Getenv("TEMPORALITY_CACHE_ENABLED")) != "false" {
|
||||
// TODO(srikanthccv): is there a ever a case to make this configurable?
|
||||
temporalityCacheConfig := DefaultTemporalityCacheConfig()
|
||||
t.temporalityCache = NewTemporalityCache(
|
||||
settings,
|
||||
cache,
|
||||
temporalityCacheConfig,
|
||||
func(ctx context.Context, orgID valuer.UUID, metricName string) (metrictypes.Temporality, error) {
|
||||
temporalityMap, err := t.fetchTemporalityMultiDirect(ctx, metricName)
|
||||
if err != nil {
|
||||
return metrictypes.Unknown, err
|
||||
}
|
||||
temporality, ok := temporalityMap[metricName]
|
||||
if !ok {
|
||||
return metrictypes.Unknown, nil
|
||||
}
|
||||
return temporality, nil
|
||||
},
|
||||
)
|
||||
|
||||
t.temporalityCache.SetRefreshMultiCallback(func(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]metrictypes.Temporality, error) {
|
||||
return t.fetchTemporalityMultiDirect(ctx, metricNames...)
|
||||
})
|
||||
} else {
|
||||
t.logger.Info("skipping temporality cache")
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
@@ -1429,12 +1462,20 @@ func (t *telemetryMetaStore) GetAllValues(ctx context.Context, fieldValueSelecto
|
||||
return values, complete, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, metricName string) (metrictypes.Temporality, error) {
|
||||
func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, orgID valuer.UUID, metricName string) (metrictypes.Temporality, error) {
|
||||
if metricName == "" {
|
||||
return metrictypes.Unknown, errors.Newf(errors.TypeInternal, errors.CodeInternal, "metric name cannot be empty")
|
||||
}
|
||||
|
||||
temporalityMap, err := t.FetchTemporalityMulti(ctx, metricName)
|
||||
if t.temporalityCache != nil {
|
||||
return t.temporalityCache.Get(ctx, orgID, metricName)
|
||||
}
|
||||
|
||||
return t.fetchTemporalityDirect(ctx, metricName)
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) fetchTemporalityDirect(ctx context.Context, metricName string) (metrictypes.Temporality, error) {
|
||||
temporalityMap, err := t.fetchTemporalityMultiDirect(ctx, metricName)
|
||||
if err != nil {
|
||||
return metrictypes.Unknown, err
|
||||
}
|
||||
@@ -1447,11 +1488,20 @@ func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, metricName st
|
||||
return temporality, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) FetchTemporalityMulti(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
func (t *telemetryMetaStore) FetchTemporalityMulti(ctx context.Context, orgID valuer.UUID, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
if len(metricNames) == 0 {
|
||||
return make(map[string]metrictypes.Temporality), nil
|
||||
}
|
||||
|
||||
if t.temporalityCache != nil {
|
||||
return t.temporalityCache.GetMulti(ctx, orgID, metricNames)
|
||||
}
|
||||
|
||||
return t.fetchTemporalityMultiDirect(ctx, metricNames...)
|
||||
}
|
||||
|
||||
// fetchTemporalityMultiDirect fetches temporalities directly from database without cache
|
||||
func (t *telemetryMetaStore) fetchTemporalityMultiDirect(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
result := make(map[string]metrictypes.Temporality)
|
||||
metricsTemporality, err := t.fetchMetricsTemporality(ctx, metricNames...)
|
||||
if err != nil {
|
||||
|
||||
24
pkg/telemetrymetadata/metadata_temporality_test.go
Normal file
24
pkg/telemetrymetadata/metadata_temporality_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package telemetrymetadata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMetadataStore_TemporalityCacheIntegration(t *testing.T) {
|
||||
// Test that metadata store works without cache
|
||||
metaStore := NewTelemetryMetaStore(
|
||||
instrumentationtest.New().ToProviderSettings(),
|
||||
nil, // telemetrystore
|
||||
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
|
||||
nil, // No cache
|
||||
)
|
||||
|
||||
store, ok := metaStore.(*telemetryMetaStore)
|
||||
require.True(t, ok)
|
||||
assert.Nil(t, store.temporalityCache, "Should not have cache when none provided")
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ func TestGetKeys(t *testing.T) {
|
||||
telemetrylogs.LogResourceKeysTblName,
|
||||
DBName,
|
||||
AttributesMetadataLocalTableName,
|
||||
nil, // No cache for tests
|
||||
)
|
||||
|
||||
rows := cmock.NewRows([]cmock.ColumnType{
|
||||
|
||||
281
pkg/telemetrymetadata/temporality_cache.go
Normal file
281
pkg/telemetrymetadata/temporality_cache.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package telemetrymetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// TemporalityCacheEntry represents a cached temporality entry
|
||||
type TemporalityCacheEntry struct {
|
||||
Temporality metrictypes.Temporality `json:"temporality"`
|
||||
CachedAt time.Time `json:"cached_at"`
|
||||
SoftTTL time.Duration `json:"soft_ttl"`
|
||||
HardTTL time.Duration `json:"hard_ttl"`
|
||||
}
|
||||
|
||||
func (e *TemporalityCacheEntry) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
||||
func (e *TemporalityCacheEntry) UnmarshalBinary(data []byte) error {
|
||||
return json.Unmarshal(data, e)
|
||||
}
|
||||
|
||||
type TemporalityCache struct {
|
||||
cache cache.Cache
|
||||
logger *slog.Logger
|
||||
softTTL time.Duration
|
||||
hardTTL time.Duration
|
||||
jitterPercent int
|
||||
refreshing sync.Map // map[string]bool to track ongoing refreshes
|
||||
refreshCallback func(ctx context.Context, orgID valuer.UUID, metricName string) (metrictypes.Temporality, error)
|
||||
refreshMultiCallback func(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]metrictypes.Temporality, error)
|
||||
}
|
||||
|
||||
// TemporalityCacheConfig holds configuration for the temporality cache
|
||||
type TemporalityCacheConfig struct {
|
||||
SoftTTL time.Duration
|
||||
HardTTL time.Duration
|
||||
JitterPercent int // Percentage of TTL to use as jitter range (e.g., 10 for ±10%)
|
||||
}
|
||||
|
||||
// DefaultTemporalityCacheConfig returns default cache configuration
|
||||
func DefaultTemporalityCacheConfig() TemporalityCacheConfig {
|
||||
return TemporalityCacheConfig{
|
||||
SoftTTL: 30 * time.Minute, // Fresh data threshold
|
||||
HardTTL: 240 * time.Minute, // Maximum cache lifetime
|
||||
JitterPercent: 20, // 20% jitter
|
||||
}
|
||||
}
|
||||
|
||||
// NewTemporalityCache creates a new temporality cache
|
||||
func NewTemporalityCache(
|
||||
settings factory.ProviderSettings,
|
||||
cache cache.Cache,
|
||||
config TemporalityCacheConfig,
|
||||
refreshCallback func(ctx context.Context, orgID valuer.UUID, metricName string) (metrictypes.Temporality, error),
|
||||
) *TemporalityCache {
|
||||
cacheSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrymetadata/temporality_cache")
|
||||
|
||||
return &TemporalityCache{
|
||||
cache: cache,
|
||||
logger: cacheSettings.Logger(),
|
||||
softTTL: config.SoftTTL,
|
||||
hardTTL: config.HardTTL,
|
||||
jitterPercent: config.JitterPercent,
|
||||
refreshCallback: refreshCallback,
|
||||
}
|
||||
}
|
||||
|
||||
// SetRefreshMultiCallback sets the batch refresh callback
|
||||
func (tc *TemporalityCache) SetRefreshMultiCallback(callback func(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]metrictypes.Temporality, error)) {
|
||||
tc.refreshMultiCallback = callback
|
||||
}
|
||||
|
||||
func (tc *TemporalityCache) generateCacheKey(metricName string) string {
|
||||
return fmt.Sprintf("temporality:metric:%s", metricName)
|
||||
}
|
||||
|
||||
func (tc *TemporalityCache) applyJitter(duration time.Duration) time.Duration {
|
||||
if tc.jitterPercent <= 0 {
|
||||
return duration
|
||||
}
|
||||
|
||||
jitterRange := int64(float64(duration.Nanoseconds()) * float64(tc.jitterPercent) / 100.0)
|
||||
jitter := rand.Int63n(2*jitterRange) - jitterRange
|
||||
|
||||
result := duration + time.Duration(jitter)
|
||||
if result < 0 {
|
||||
return 0
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (tc *TemporalityCache) Get(ctx context.Context, orgID valuer.UUID, metricName string) (metrictypes.Temporality, error) {
|
||||
cacheKey := tc.generateCacheKey(metricName)
|
||||
|
||||
var entry TemporalityCacheEntry
|
||||
err := tc.cache.Get(ctx, orgID, cacheKey, &entry, false)
|
||||
|
||||
if err != nil {
|
||||
if !errors.Ast(err, errors.TypeNotFound) {
|
||||
tc.logger.ErrorContext(ctx, "error getting cached temporality", "metric", metricName, "error", err)
|
||||
}
|
||||
|
||||
temporality, err := tc.refreshCallback(ctx, orgID, metricName)
|
||||
if err != nil {
|
||||
return metrictypes.Unknown, err
|
||||
}
|
||||
|
||||
tc.put(ctx, orgID, metricName, temporality)
|
||||
return temporality, nil
|
||||
}
|
||||
|
||||
age := time.Since(entry.CachedAt)
|
||||
|
||||
if age < entry.SoftTTL {
|
||||
tc.logger.DebugContext(ctx, "returning fresh cached temporality",
|
||||
"metric", metricName,
|
||||
"age", age,
|
||||
"soft_ttl", entry.SoftTTL)
|
||||
return entry.Temporality, nil
|
||||
}
|
||||
|
||||
if age < entry.HardTTL {
|
||||
tc.logger.DebugContext(ctx, "returning stale cached temporality and triggering refresh",
|
||||
"metric", metricName,
|
||||
"age", age,
|
||||
"soft_ttl", entry.SoftTTL,
|
||||
"hard_ttl", entry.HardTTL)
|
||||
|
||||
tc.triggerBackgroundRefresh(ctx, orgID, metricName)
|
||||
|
||||
return entry.Temporality, nil
|
||||
}
|
||||
|
||||
tc.logger.DebugContext(ctx, "cached temporality exceeded hard TTL, fetching fresh",
|
||||
"metric", metricName,
|
||||
"age", age,
|
||||
"hard_ttl", entry.HardTTL)
|
||||
|
||||
temporality, err := tc.refreshCallback(ctx, orgID, metricName)
|
||||
if err != nil {
|
||||
// when refresh fails and we have stale data, return it as fallback
|
||||
if entry.Temporality != metrictypes.Unknown {
|
||||
tc.logger.WarnContext(ctx, "failed to refresh temporality, returning stale data",
|
||||
"metric", metricName,
|
||||
"error", err)
|
||||
return entry.Temporality, nil
|
||||
}
|
||||
return metrictypes.Unknown, err
|
||||
}
|
||||
|
||||
tc.put(ctx, orgID, metricName, temporality)
|
||||
return temporality, nil
|
||||
}
|
||||
|
||||
func (tc *TemporalityCache) GetMulti(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]metrictypes.Temporality, error) {
|
||||
result := make(map[string]metrictypes.Temporality)
|
||||
var missedMetrics []string
|
||||
staleMetrics := make(map[string]metrictypes.Temporality)
|
||||
|
||||
// for each metric, check if it fresh, stale + valid, or invalid/missing
|
||||
// then trigger fetch for all missing metrics
|
||||
for _, metricName := range metricNames {
|
||||
cacheKey := tc.generateCacheKey(metricName)
|
||||
|
||||
var entry TemporalityCacheEntry
|
||||
err := tc.cache.Get(ctx, orgID, cacheKey, &entry, false)
|
||||
|
||||
if err != nil {
|
||||
missedMetrics = append(missedMetrics, metricName)
|
||||
continue
|
||||
}
|
||||
|
||||
age := time.Since(entry.CachedAt)
|
||||
|
||||
if age < entry.SoftTTL {
|
||||
result[metricName] = entry.Temporality
|
||||
} else if age < entry.HardTTL {
|
||||
result[metricName] = entry.Temporality
|
||||
staleMetrics[metricName] = entry.Temporality
|
||||
} else {
|
||||
missedMetrics = append(missedMetrics, metricName)
|
||||
}
|
||||
}
|
||||
|
||||
for metricName := range staleMetrics {
|
||||
tc.triggerBackgroundRefresh(ctx, orgID, metricName)
|
||||
}
|
||||
|
||||
if len(missedMetrics) > 0 {
|
||||
temporalities, err := tc.refreshMulti(ctx, orgID, missedMetrics)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
for metricName, temporality := range temporalities {
|
||||
tc.put(ctx, orgID, metricName, temporality)
|
||||
result[metricName] = temporality
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (tc *TemporalityCache) put(ctx context.Context, orgID valuer.UUID, metricName string, temporality metrictypes.Temporality) {
|
||||
entry := TemporalityCacheEntry{
|
||||
Temporality: temporality,
|
||||
CachedAt: time.Now(),
|
||||
SoftTTL: tc.applyJitter(tc.softTTL),
|
||||
HardTTL: tc.applyJitter(tc.hardTTL),
|
||||
}
|
||||
|
||||
cacheKey := tc.generateCacheKey(metricName)
|
||||
|
||||
if err := tc.cache.Set(ctx, orgID, cacheKey, &entry, entry.HardTTL); err != nil {
|
||||
tc.logger.ErrorContext(ctx, "failed to cache temporality",
|
||||
"metric", metricName,
|
||||
"error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *TemporalityCache) triggerBackgroundRefresh(_ context.Context, orgID valuer.UUID, metricName string) {
|
||||
if _, loading := tc.refreshing.LoadOrStore(metricName, true); loading {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer tc.refreshing.Delete(metricName)
|
||||
|
||||
refreshCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
tc.logger.DebugContext(refreshCtx, "starting background refresh", "metric", metricName)
|
||||
|
||||
temporality, err := tc.refreshCallback(refreshCtx, orgID, metricName)
|
||||
if err != nil {
|
||||
tc.logger.ErrorContext(refreshCtx, "background refresh failed",
|
||||
"metric", metricName,
|
||||
"error", err)
|
||||
return
|
||||
}
|
||||
|
||||
tc.put(refreshCtx, orgID, metricName, temporality)
|
||||
|
||||
tc.logger.DebugContext(refreshCtx, "background refresh completed", "metric", metricName)
|
||||
}()
|
||||
}
|
||||
|
||||
func (tc *TemporalityCache) refreshMulti(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]metrictypes.Temporality, error) {
|
||||
if tc.refreshMultiCallback != nil {
|
||||
return tc.refreshMultiCallback(ctx, orgID, metricNames)
|
||||
}
|
||||
|
||||
result := make(map[string]metrictypes.Temporality)
|
||||
|
||||
for _, metricName := range metricNames {
|
||||
temporality, err := tc.refreshCallback(ctx, orgID, metricName)
|
||||
if err != nil {
|
||||
tc.logger.ErrorContext(ctx, "failed to refresh temporality",
|
||||
"metric", metricName,
|
||||
"error", err)
|
||||
result[metricName] = metrictypes.Unknown
|
||||
continue
|
||||
}
|
||||
result[metricName] = temporality
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
67
pkg/telemetrymetadata/temporality_cache_test.go
Normal file
67
pkg/telemetrymetadata/temporality_cache_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package telemetrymetadata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTemporalityCache_Jitter(t *testing.T) {
|
||||
config := TemporalityCacheConfig{
|
||||
SoftTTL: 1 * time.Minute,
|
||||
HardTTL: 5 * time.Minute,
|
||||
JitterPercent: 20,
|
||||
}
|
||||
|
||||
tc := &TemporalityCache{
|
||||
jitterPercent: config.JitterPercent,
|
||||
}
|
||||
|
||||
// Test jitter produces different values
|
||||
ttlValues := make(map[time.Duration]bool)
|
||||
for i := 0; i < 10; i++ {
|
||||
jittered := tc.applyJitter(config.SoftTTL)
|
||||
ttlValues[jittered] = true
|
||||
|
||||
// Check jitter is within expected range
|
||||
minTTL := time.Duration(float64(config.SoftTTL) * 0.8)
|
||||
maxTTL := time.Duration(float64(config.SoftTTL) * 1.2)
|
||||
assert.GreaterOrEqual(t, jittered, minTTL)
|
||||
assert.LessOrEqual(t, jittered, maxTTL)
|
||||
}
|
||||
|
||||
// Should have multiple different values due to jitter
|
||||
assert.Greater(t, len(ttlValues), 1, "Jitter should produce different TTL values")
|
||||
}
|
||||
|
||||
func TestTemporalityCacheConfig_Default(t *testing.T) {
|
||||
config := DefaultTemporalityCacheConfig()
|
||||
|
||||
assert.Equal(t, 5*time.Minute, config.SoftTTL)
|
||||
assert.Equal(t, 30*time.Minute, config.HardTTL)
|
||||
assert.Equal(t, 10, config.JitterPercent)
|
||||
}
|
||||
|
||||
func TestTemporalityCacheEntry_Serialization(t *testing.T) {
|
||||
entry := TemporalityCacheEntry{
|
||||
Temporality: metrictypes.Delta,
|
||||
CachedAt: time.Now(),
|
||||
SoftTTL: 5 * time.Minute,
|
||||
HardTTL: 30 * time.Minute,
|
||||
}
|
||||
|
||||
// Test marshaling
|
||||
data, err := entry.MarshalBinary()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Test unmarshaling
|
||||
var decoded TemporalityCacheEntry
|
||||
err = decoded.UnmarshalBinary(data)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, entry.Temporality, decoded.Temporality)
|
||||
assert.Equal(t, entry.SoftTTL, decoded.SoftTTL)
|
||||
assert.Equal(t, entry.HardTTL, decoded.HardTTL)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MetadataStore is the interface for the telemetry metadata store.
|
||||
@@ -26,8 +27,8 @@ type MetadataStore interface {
|
||||
GetAllValues(ctx context.Context, fieldValueSelector *FieldValueSelector) (*TelemetryFieldValues, bool, error)
|
||||
|
||||
// FetchTemporality fetches the temporality for metric
|
||||
FetchTemporality(ctx context.Context, metricName string) (metrictypes.Temporality, error)
|
||||
FetchTemporality(ctx context.Context, orgID valuer.UUID, metricName string) (metrictypes.Temporality, error)
|
||||
|
||||
// FetchTemporalityMulti fetches the temporality for multiple metrics
|
||||
FetchTemporalityMulti(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, error)
|
||||
FetchTemporalityMulti(ctx context.Context, orgID valuer.UUID, metricNames ...string) (map[string]metrictypes.Temporality, error)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MockMetadataStore implements the MetadataStore interface for testing purposes
|
||||
@@ -258,7 +259,7 @@ func (m *MockMetadataStore) SetAllValues(lookupKey string, values *telemetrytype
|
||||
}
|
||||
|
||||
// FetchTemporality fetches the temporality for a metric
|
||||
func (m *MockMetadataStore) FetchTemporality(ctx context.Context, metricName string) (metrictypes.Temporality, error) {
|
||||
func (m *MockMetadataStore) FetchTemporality(ctx context.Context, orgID valuer.UUID, metricName string) (metrictypes.Temporality, error) {
|
||||
if temporality, exists := m.TemporalityMap[metricName]; exists {
|
||||
return temporality, nil
|
||||
}
|
||||
@@ -266,7 +267,7 @@ func (m *MockMetadataStore) FetchTemporality(ctx context.Context, metricName str
|
||||
}
|
||||
|
||||
// FetchTemporalityMulti fetches the temporality for multiple metrics
|
||||
func (m *MockMetadataStore) FetchTemporalityMulti(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
func (m *MockMetadataStore) FetchTemporalityMulti(ctx context.Context, orgID valuer.UUID, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
result := make(map[string]metrictypes.Temporality)
|
||||
|
||||
for _, metricName := range metricNames {
|
||||
|
||||
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