Compare commits

..

1 Commits

Author SHA1 Message Date
nityanandagohain
b8b1f7e56c fix: ensure timestamp is always in ms 2026-05-27 18:19:29 +05:30
84 changed files with 423 additions and 869 deletions

View File

@@ -41,22 +41,14 @@ $item-spacing: 8px;
width: 100%;
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
outline: none;
height: auto;
color: var(--l1-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
padding: 0;
&:focus,
&:focus-visible,
&:hover {
border: none;
&.ant-input:focus {
box-shadow: none;
outline: none;
}
&::placeholder {

View File

@@ -6,7 +6,7 @@ import {
useState,
} from 'react';
import { Color } from '@signozhq/design-tokens';
import { Input } from '@signozhq/ui/input';
import { Input } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';

View File

@@ -1,6 +1,5 @@
import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Card, Form } from 'antd';
import { Card, Form, Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';

View File

@@ -1,6 +1,5 @@
import { useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { Button, Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { X } from '@signozhq/icons';

View File

@@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button, InputNumber, Popover, Tooltip } from 'antd';
import { Button, Input, InputNumber, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { DefaultOptionType } from 'antd/es/select';
import cx from 'classnames';

View File

@@ -259,14 +259,6 @@
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
&:focus:not(:focus-visible),
&.ant-btn:focus:not(:focus-visible) {
border-color: var(--l2-border);
border-left-color: transparent;
outline: none;
box-shadow: none;
}
}
}
}
@@ -292,21 +284,5 @@
.cm-placeholder {
font-size: 12px !important;
}
$add-on-row-height: 38px;
.periscope-input-with-label {
.input {
.ant-select {
height: $add-on-row-height;
}
}
}
.input-with-label {
.input {
height: $add-on-row-height;
}
}
}
}

View File

@@ -4,23 +4,6 @@
padding: 12px;
gap: 12px;
border-bottom: 1px solid var(--l1-border);
.search {
input {
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
&::placeholder {
color: var(--l3-foreground);
}
--input-font-size: 14px;
--input-border-color: var(--l1-border);
--input-focus-border-color: var(--primary-background);
--input-focus-outline-width: 0;
--input-focus-outline-offset: 0;
}
}
.filter-header-checkbox {
display: flex;
align-items: center;

View File

@@ -1,7 +1,6 @@
/* eslint-disable sonarjs/no-identical-functions */
import { Fragment, useMemo, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button, Checkbox, Skeleton } from 'antd';
import { Button, Checkbox, Input, Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';

View File

@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { Button, Input } from 'antd';
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';

View File

@@ -21,17 +21,14 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { useNotifications } from 'hooks/useNotifications';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import ErrorState from './ErrorState';
import {
getStepIntervalForQuery,
getTracesTimeRangeFromStepInterval,
prepareStatusCodeBarChartsConfig,
} from './utils';
import { prepareStatusCodeBarChartsConfig } from './utils';
function StatusCodeBarCharts({
endPointStatusCodeBarChartsDataQuery,
@@ -138,18 +135,6 @@ function StatusCodeBarCharts({
[domainName, filters],
);
const activeApiResponse = useMemo(
() =>
currentWidgetInfoIndex === 0
? formattedEndPointStatusCodeBarChartsDataPayload
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
[
currentWidgetInfoIndex,
formattedEndPointStatusCodeBarChartsDataPayload,
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
],
);
const graphClickHandler = useCallback(
(
xValue: number,
@@ -159,14 +144,11 @@ function StatusCodeBarCharts({
metric?: { [key: string]: string },
queryData?: { queryName: string; inFocusOrNot: boolean },
): void => {
const TWO_AND_HALF_MINUTES_IN_MILLISECONDS = 2.5 * 60 * 1000; // 150,000 milliseconds
const customFilters = getCustomFiltersForBarChart(metric);
const stepInterval = getStepIntervalForQuery(
activeApiResponse,
queryData?.queryName,
);
const { start, end } = getTracesTimeRangeFromStepInterval(
const { start, end } = getStartAndEndTimesInMilliseconds(
xValue,
stepInterval,
TWO_AND_HALF_MINUTES_IN_MILLISECONDS,
);
handleGraphClick({
@@ -189,7 +171,6 @@ function StatusCodeBarCharts({
});
},
[
activeApiResponse,
widget,
navigateToExplorerPages,
navigateToExplorer,

View File

@@ -1,68 +0,0 @@
import {
getMinStepIntervalFromApiResponse,
getStepIntervalForQuery,
getTracesTimeRangeFromStepInterval,
} from '../utils';
describe('StatusCodeBarCharts utils', () => {
describe('getTracesTimeRangeFromStepInterval', () => {
const xValue = 1609459200; // seconds
it('keeps start at click time with a minimum 5 minute end range', () => {
const { start, end } = getTracesTimeRangeFromStepInterval(xValue, 60);
expect(start).toBe(xValue * 1000);
expect(end - start).toBe(5 * 60 * 1000);
expect(end).toBe(xValue * 1000 + 5 * 60 * 1000);
});
it('extends end when step interval is larger than 5 minutes', () => {
const stepInterval = 600; // 10 minutes
const { start, end } = getTracesTimeRangeFromStepInterval(
xValue,
stepInterval,
);
expect(start).toBe(xValue * 1000);
expect(end - start).toBe(10 * 60 * 1000);
expect(end).toBe(xValue * 1000 + 10 * 60 * 1000);
});
});
describe('getMinStepIntervalFromApiResponse', () => {
it('returns 60 when step intervals are missing', () => {
expect(getMinStepIntervalFromApiResponse({} as any)).toBe(60);
});
it('returns the minimum step interval from the response', () => {
const apiResponse = {
data: {
newResult: {
meta: {
stepIntervals: { A: 120, B: 60 },
},
},
},
};
expect(getMinStepIntervalFromApiResponse(apiResponse as any)).toBe(60);
});
});
describe('getStepIntervalForQuery', () => {
it('returns query-specific step interval when available', () => {
const apiResponse = {
data: {
newResult: {
meta: {
stepIntervals: { A: 120, B: 60 },
},
},
},
};
expect(getStepIntervalForQuery(apiResponse as any, 'A')).toBe(120);
expect(getStepIntervalForQuery(apiResponse as any, 'B')).toBe(60);
});
});
});

View File

@@ -13,65 +13,6 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryData } from 'types/api/widgets/getQuery';
import { v4 } from 'uuid';
const DEFAULT_STEP_INTERVAL_SECONDS = 60;
const MIN_TRACES_TIME_RANGE_MINUTES = 5;
export function getMinStepIntervalFromApiResponse(
apiResponse: MetricRangePayloadProps,
): number {
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
'data.newResult.meta.stepIntervals',
{},
);
const values = Object.values(stepIntervals).filter(
(value): value is number =>
typeof value === 'number' && Number.isFinite(value),
);
if (values.length === 0) {
return DEFAULT_STEP_INTERVAL_SECONDS;
}
return Math.min(...values);
}
export function getStepIntervalForQuery(
apiResponse: MetricRangePayloadProps,
queryName?: string,
): number {
const minStepInterval = getMinStepIntervalFromApiResponse(apiResponse);
if (!queryName) {
return minStepInterval;
}
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
'data.newResult.meta.stepIntervals',
{},
);
return get(stepIntervals, queryName, minStepInterval) ?? minStepInterval;
}
export function getTracesTimeRangeFromStepInterval(
xValue: number,
stepIntervalSeconds: number,
): { start: number; end: number } {
const rangeMinutes = Math.max(
stepIntervalSeconds / 60,
MIN_TRACES_TIME_RANGE_MINUTES,
);
const rangeMs = rangeMinutes * 60 * 1000;
const start = Math.floor(xValue * 1000);
return {
start,
end: Math.ceil(start + rangeMs),
};
}
export const prepareStatusCodeBarChartsConfig = ({
timezone,
isDarkMode,
@@ -100,7 +41,7 @@ export const prepareStatusCodeBarChartsConfig = ({
'data.newResult.meta.stepIntervals',
{},
);
const minStepInterval = getMinStepIntervalFromApiResponse(apiResponse);
const minStepInterval = Math.min(...Object.values(stepIntervals));
const config = buildBaseConfig({
id: v4(),

View File

@@ -1,6 +1,5 @@
import { useMemo, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button, Select, Tooltip } from 'antd';
import { Button, Input, Select, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { CircleX, Trash } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';

View File

@@ -1,5 +1,4 @@
import { Input } from '@signozhq/ui/input';
import { Collapse } from 'antd';
import { Collapse, Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useCreateAlertState } from '../context';

View File

@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { Input } from '@signozhq/ui/input';
import { Select } from 'antd';
import { Input, Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../../context/constants';

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Input } from 'antd';
import './TimeInput.scss';
export interface TimeInputProps {

View File

@@ -1,5 +1,4 @@
import { Input } from '@signozhq/ui/input';
import { Select } from 'antd';
import { Input, Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useCreateAlertState } from '../context';

View File

@@ -16,8 +16,7 @@ import {
Plus,
X,
} from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Button, Card, Modal, Popover, Tag, Tooltip } from 'antd';
import { Button, Card, Input, Modal, Popover, Tag, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';

View File

@@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { Button, Input } from 'antd';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import { ResizeTable } from 'components/ResizeTable';
import { useNotifications } from 'hooks/useNotifications';

View File

@@ -19,11 +19,11 @@ import {
Info,
} from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Input } from '@signozhq/ui/input';
import {
Button,
ColorPicker,
Divider,
Input,
Modal,
RefSelectProps,
Select,

View File

@@ -1,7 +1,7 @@
import { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Form } from 'antd';
import { Form, Input } from 'antd';
import { EmailChannel } from '../../CreateAlertChannels/config';
function EmailForm({ setSelectedConfig }: EmailFormProps): JSX.Element {

View File

@@ -1,7 +1,6 @@
import { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Form } from 'antd';
import { Form, Input } from 'antd';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { WebhookChannel } from '../../CreateAlertChannels/config';

View File

@@ -1,8 +1,7 @@
import { Dispatch, ReactElement, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Form, FormInstance, Input, Select } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Form, FormInstance, Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import type { Store } from 'antd/lib/form/interface';
import ROUTES from 'constants/routes';

View File

@@ -6,8 +6,7 @@ import { useIsFetching } from 'react-query';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { Input } from '@signozhq/ui/input';
import { Button, Form, Modal } from 'antd';
import { Button, Form, Input, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';

View File

@@ -5,12 +5,12 @@ import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import {
Col,
Collapse,
DatePicker,
Form,
Input,
InputNumber,
Modal,
Row,

View File

@@ -1,5 +1,4 @@
import { Input } from '@signozhq/ui/input';
import { Form } from 'antd';
import { Form, Input } from 'antd';
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
function RenderConnectionFields({

View File

@@ -1,7 +1,6 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Button, Form } from 'antd';
import { Button, Form, Input } from 'antd';
import apply from 'api/v3/licenses/post';
import { useNotifications } from 'hooks/useNotifications';
import APIError from 'types/api/error';

View File

@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { ChangeEvent, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button, Modal } from 'antd';
import { Button, Input, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import ApacheIcon from 'assets/CustomIcons/ApacheIcon';
import DockerIcon from 'assets/CustomIcons/DockerIcon';

View File

@@ -12,11 +12,11 @@ import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { Input } from '@signozhq/ui/input';
import {
Button,
Dropdown,
Flex,
Input,
MenuProps,
Modal,
Popover,

View File

@@ -1,8 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LoaderCircle, Check } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Button, Space } from 'antd';
import { Button, Input, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { useNotifications } from 'hooks/useNotifications';

View File

@@ -2,9 +2,8 @@ import { ReactNode, useState } from 'react';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Switch } from '@signozhq/ui/switch';
import { Collapse, Divider, Tag } from 'antd';
import { Collapse, Divider, Input, Tag } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';

View File

@@ -2,8 +2,7 @@ import { ChangeEvent, useCallback, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { CirclePlus, X } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Col } from 'antd';
import { Col, Input } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { AppState } from 'store/reducers';

View File

@@ -2,8 +2,7 @@ import { useCallback, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { SquareX, X } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Button, Select } from 'antd';
import { Button, Input, Select } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import {
ConditionalOperators,

View File

@@ -1,7 +1,6 @@
import { Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
// TODO(@signozhq/ui-input): migrate this <Input> once @signozhq/ui Input
// supports the `onWheel` handler (used to blur on scroll for number inputs).
import { Input, Select } from 'antd';
import { Select } from 'antd';
import classNames from 'classnames';
import { TIME_AGGREGATION_OPTIONS } from './constants';

View File

@@ -1,8 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import type { TableColumnsType as ColumnsType } from 'antd';
import { Input } from '@signozhq/ui/input';
import { Button, Collapse, Select, Spin } from 'antd';
import { Button, Collapse, Input, Select, Spin } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import {

View File

@@ -7,8 +7,7 @@ import {
DropResult,
} from 'react-beautiful-dnd';
import { Color } from '@signozhq/design-tokens';
import { Input } from '@signozhq/ui/input';
import { Button, Divider, Dropdown, MenuProps, Tooltip } from 'antd';
import { Button, Divider, Dropdown, Input, MenuProps, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { FieldDataType } from 'api/v5/v5';
import { SOMETHING_WENT_WRONG } from 'constants/api';

View File

@@ -1,7 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
// TODO(@signozhq/ui-input): migrate <Input> once @signozhq/ui Input
// supports the `spellCheck` prop on the URL input below.
import { Button, Col, Form, Input, Input as AntInput, Row } from 'antd';
import { Button, Col, Form, Input as AntInput, Input, Row } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { CONTEXT_LINK_FIELDS } from 'container/NewWidget/RightContainer/ContextLinks/constants';
import {

View File

@@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Blocks, Check, LoaderCircle } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Button, Card, Form, Select, Space } from 'antd';
import { Button, Card, Form, Input, Select, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';

View File

@@ -1,8 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Check, Server, LoaderCircle } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Button, Card, Form, Space } from 'antd';
import { Button, Card, Form, Input, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';

View File

@@ -1,7 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Plus, Trash2 } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Button, Form, FormInstance, Select, Space } from 'antd';
import { Button, Form, FormInstance, Input, Select, Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';

View File

@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Form } from 'antd';
import { Form, Input } from 'antd';
import { ProcessorFormField } from '../../AddNewProcessor/config';
import { formValidationRules } from '../../config';

View File

@@ -1,6 +1,4 @@
import { ChangeEventHandler, useState } from 'react';
// TODO(@signozhq/ui-input): migrate to @signozhq/ui Input once the antd
// `InputProps` spread (`size`, etc.) is no longer needed on this wrapper.
import { Input, InputProps } from 'antd';
function CSVInput({ value, onChange, ...otherProps }: InputProps): JSX.Element {

View File

@@ -1,8 +1,7 @@
import { useEffect, useState } from 'react';
import { Info } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Switch } from '@signozhq/ui/switch';
import { Flex, Form, Space, Tooltip } from 'antd';
import { Flex, Form, Input, Space, Tooltip } from 'antd';
import { ProcessorData } from 'types/api/pipeline/def';
import { PREDEFINED_MAPPING } from '../config';

View File

@@ -1,7 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Form, Input, Select, Space } from 'antd';
import { Switch } from '@signozhq/ui/switch';
import { Form, Select, Space } from 'antd';
import { ModalFooterTitle } from 'container/PipelinePage/styles';
import { ProcessorData } from 'types/api/pipeline/def';

View File

@@ -2,8 +2,7 @@ import React, { ChangeEvent, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Plus, Search } from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Input } from '@signozhq/ui/input';
import { Button, Flex, Form, Tooltip } from 'antd';
import { Button, Flex, Form, Input, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
useDeleteDowntimeScheduleByID,

View File

@@ -8,16 +8,4 @@
grid-template-columns: 60% 35%;
justify-content: space-between;
gap: 16px;
.input-with-label {
.label {
box-sizing: border-box;
height: 36px;
}
.input {
box-sizing: border-box;
height: 36px;
}
}
}

View File

@@ -1,7 +1,6 @@
import { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { Input } from '@signozhq/ui/input';
import { Skeleton } from 'antd';
import { Input, Skeleton } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';

View File

@@ -1,8 +1,7 @@
import { ChangeEvent, useMemo } from 'react';
import { Plus, Search } from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Input } from '@signozhq/ui/input';
import { Button, Flex, Tooltip } from 'antd';
import { Button, Flex, Input, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';

View File

@@ -1,5 +1,5 @@
import { useCallback, useMemo, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';

View File

@@ -1,6 +1,5 @@
import { useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Collapse, Modal } from 'antd';
import { Collapse, Input, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { Diamond } from '@signozhq/icons';

View File

@@ -9,10 +9,10 @@ import {
useState,
} from 'react';
import { useMutation, useQuery } from 'react-query';
import { Input } from '@signozhq/ui/input';
import {
Button,
Checkbox,
Input,
Modal,
Select,
Skeleton,

View File

@@ -1,7 +1,5 @@
// TODO(@signozhq/ui-input): migrate this styled(Input) once @signozhq/ui
// Input supports `addonAfter` (the consumer renders `<InputComponent addonAfter="ms">`).
import { Typography } from '@signozhq/ui/typography';
import { Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const DurationText = styled.div`

View File

@@ -8,8 +8,7 @@ import {
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Input } from '@signozhq/ui/input';
import { AutoComplete } from 'antd';
import { AutoComplete, Input } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';

View File

@@ -2,8 +2,7 @@ import { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Input } from '@signozhq/ui/input';
import { AutoComplete, Space } from 'antd';
import { AutoComplete, Input, Space } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';

View File

@@ -1,7 +1,6 @@
import { ChangeEvent, useEffect, useMemo, useState } from 'react';
import { ArrowLeft, Check, Loader, Plus, Search } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Button, Spin } from 'antd';
import { Button, Input, Spin } from 'antd';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import SignozModal from 'components/SignozModal/SignozModal';

View File

@@ -1,4 +1,4 @@
import { Input } from '@signozhq/ui/input';
import { Input } from 'antd';
import styled from 'styled-components';
export const InputComponent = styled(Input)`

View File

@@ -1,6 +1,5 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Select } from 'antd';
import { Input, Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import './DropRateView.styles.scss';

View File

@@ -28,10 +28,6 @@
.learn-more {
font-size: 14px;
}
.search-input-container {
margin-top: 16px;
margin-bottom: 8px;
}
.ant-input-affix-wrapper {
margin-top: 16px;

View File

@@ -3,8 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { ColorPicker, Modal, Table, TableProps } from 'antd';
import { ColorPicker, Input, Modal, Table, TableProps } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import {
@@ -312,15 +311,12 @@ function SaveView(): JSX.Element {
Learn more
</Typography.Link>
</Typography.Text>
<div className="search-input-container">
<Input
placeholder="Search for views..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchValue}
onChange={handleSearch}
className="search-input"
/>
</div>
<Input
placeholder="Search for views..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchValue}
onChange={handleSearch}
/>
<Table
columns={columns}

View File

@@ -7,8 +7,8 @@ import {
} from '@signozhq/ui/tabs';
import cx from 'classnames';
import { DetailsHeader } from 'components/DetailsPanel';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { generateColorPair } from 'pages/TraceDetailsV3/utils/generateColorPair';
import { themeColors } from 'constants/theme';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { FloatingPanel } from 'periscope/components/FloatingPanel';
import { useTraceStore } from '../../stores/traceStore';
@@ -35,7 +35,6 @@ function AnalyticsPanel({
}: AnalyticsPanelProps): JSX.Element | null {
const aggregations = useTraceStore((s) => s.aggregations);
const colorByFieldName = useTraceStore((s) => s.colorByField.name);
const isDarkMode = useIsDarkMode();
const execTimePct = useMemo(
() =>
@@ -58,16 +57,13 @@ function AnalyticsPanel({
return [];
}
return Object.entries(execTimePct)
.map(([group, percentage]) => {
const pair = generateColorPair(group);
return {
group,
percentage,
color: isDarkMode ? pair.color : pair.colorDark,
};
})
.map(([group, percentage]) => ({
group,
percentage,
color: generateColor(group, themeColors.traceDetailColorsV3),
}))
.sort((a, b) => b.percentage - a.percentage);
}, [execTimePct, isDarkMode]);
}, [execTimePct]);
const spanCountRows = useMemo(() => {
if (!spanCounts) {
@@ -75,17 +71,14 @@ function AnalyticsPanel({
}
const max = Math.max(...Object.values(spanCounts), 1);
return Object.entries(spanCounts)
.map(([group, count]) => {
const pair = generateColorPair(group);
return {
group,
count,
max,
color: isDarkMode ? pair.color : pair.colorDark,
};
})
.map(([group, count]) => ({
group,
count,
max,
color: generateColor(group, themeColors.traceDetailColorsV3),
}))
.sort((a, b) => b.count - a.count);
}, [spanCounts, isDarkMode]);
}, [spanCounts]);
if (!isOpen) {
return null;

View File

@@ -1,5 +1,4 @@
import { Input } from '@signozhq/ui/input';
import { Checkbox, Select, Skeleton } from 'antd';
import { Checkbox, Input, Select, Skeleton } from 'antd';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';

View File

@@ -5,7 +5,6 @@ import {
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTraceStore } from 'pages/TraceDetailsV3/stores/traceStore';
import { getSpanAttribute, resolveSpanColor } from 'pages/TraceDetailsV3/utils';
import { useMemo } from 'react';
@@ -102,7 +101,6 @@ export function SpanHoverCard({
}: SpanHoverCardProps): JSX.Element {
const previewFields = useTraceStore((s) => s.previewFields);
const colorByFieldName = useTraceStore((s) => s.colorByField.name);
const isDarkMode = useIsDarkMode();
const hoverCardData = useMemo(() => {
if (!hoveredSpanId) {
@@ -123,12 +121,11 @@ export function SpanHoverCard({
})
.filter((r): r is SpanPreviewRow => r !== null);
const pair = resolveSpanColor(span, colorByFieldName);
return {
anchorTop: idx * rowHeight,
tooltip: {
spanName: span.name,
color: isDarkMode ? pair.color : pair.colorDark,
color: resolveSpanColor(span, colorByFieldName),
hasError: span.has_error,
relativeStartMs: span.timestamp - traceStartTime,
durationMs: span.duration_nano / 1e6,
@@ -142,7 +139,6 @@ export function SpanHoverCard({
colorByFieldName,
rowHeight,
traceStartTime,
isDarkMode,
]);
return (

View File

@@ -87,7 +87,6 @@ describe('Canvas Draw Utils', () => {
spanRectsArray,
eventRectsArray: [],
color: '#1890ff',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
});
@@ -95,9 +94,7 @@ describe('Canvas Draw Utils', () => {
expect(ctx.beginPath).toHaveBeenCalled();
expect(ctx.roundRect).toHaveBeenCalledWith(10, 1, 100, 22, 2);
expect(ctx.fill).toHaveBeenCalled();
// Rest state draws a subtle 1px rgba(0,0,0,0.3) outline to match spec
expect(ctx.stroke).toHaveBeenCalled();
expect(ctx.strokeStyle).toBe('rgba(0, 0, 0, 0.3)');
expect(ctx.stroke).not.toHaveBeenCalled();
expect(spanRectsArray).toHaveLength(1);
expect(spanRectsArray[0]).toMatchObject({
x: 10,
@@ -129,17 +126,15 @@ describe('Canvas Draw Utils', () => {
spanRectsArray,
eventRectsArray: [],
color: '#2F80ED',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
selectedSpanId: 'sel',
});
// Selected spans get solid l2-background fill + dashed border.
// Light mode uses colorDark for the stroke for contrast against l2-background.
// Selected spans get solid l2-background fill + dashed border
expect(ctx.fill).toHaveBeenCalled();
expect(ctx.setLineDash).toHaveBeenCalledWith(DASHED_BORDER_LINE_DASH);
expect(ctx.strokeStyle).toBe('#000');
expect(ctx.strokeStyle).toBe('#2F80ED');
expect(ctx.lineWidth).toBe(2);
expect(ctx.stroke).toHaveBeenCalled();
expect(ctx.setLineDash).toHaveBeenLastCalledWith([]);
@@ -166,7 +161,6 @@ describe('Canvas Draw Utils', () => {
spanRectsArray,
eventRectsArray: [],
color: '#2F80ED',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
hoveredSpanId: 'hov',
@@ -199,7 +193,6 @@ describe('Canvas Draw Utils', () => {
spanRectsArray,
eventRectsArray: [],
color: '#000',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
});
@@ -237,7 +230,6 @@ describe('Canvas Draw Utils', () => {
spanRectsArray,
eventRectsArray: [],
color: '#000',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
});
@@ -262,7 +254,6 @@ describe('Canvas Draw Utils', () => {
spanRectsArray: [],
eventRectsArray: [],
color: '#000',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
});
@@ -288,7 +279,6 @@ describe('Canvas Draw Utils', () => {
spanRectsArray: [],
eventRectsArray: [],
color: '#000',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
});
@@ -324,7 +314,6 @@ describe('Canvas Draw Utils', () => {
spanRectsArray: [],
eventRectsArray: [],
color: '#000',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
});
@@ -355,7 +344,6 @@ describe('Canvas Draw Utils', () => {
spanRectsArray: [],
eventRectsArray: [],
color: '#000',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
});
@@ -383,8 +371,8 @@ describe('Canvas Draw Utils', () => {
expect(ctx.save).toHaveBeenCalled();
expect(ctx.translate).toHaveBeenCalledWith(50, 11);
expect(ctx.rotate).toHaveBeenCalledWith(Math.PI / 4);
expect(ctx.fillStyle).toBe('#FC4E4E');
expect(ctx.strokeStyle).toBe('#fd2c2c');
expect(ctx.fillStyle).toBe('rgb(220, 38, 38)');
expect(ctx.strokeStyle).toBe('rgb(153, 27, 27)');
expect(ctx.fillRect).toHaveBeenCalledWith(-3, -3, 6, 6);
expect(ctx.strokeRect).toHaveBeenCalledWith(-3, -3, 6, 6);
expect(ctx.restore).toHaveBeenCalled();
@@ -420,8 +408,8 @@ describe('Canvas Draw Utils', () => {
eventDotSize: 6,
});
expect(ctx.fillStyle).toBe('#FC4E4E');
expect(ctx.strokeStyle).toBe('#fd2c2c');
expect(ctx.fillStyle).toBe('rgb(239, 68, 68)');
expect(ctx.strokeStyle).toBe('rgb(185, 28, 28)');
});
it('falls back to cyan/blue for unparseable span colors', () => {
@@ -473,7 +461,6 @@ describe('Canvas Draw Utils', () => {
spanRectsArray: [],
eventRectsArray: [],
color: '#000',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
hoveredSpanId: 'p',
@@ -496,7 +483,6 @@ describe('Canvas Draw Utils', () => {
spanRectsArray: [],
eventRectsArray: [],
color: '#000',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
selectedSpanId: 'p',
@@ -538,7 +524,6 @@ describe('Canvas Draw Utils', () => {
spanRectsArray: [],
eventRectsArray: [],
color: '#000',
colorDark: '#000',
isDarkMode: false,
metrics: METRICS,
});

View File

@@ -3,13 +3,11 @@ import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { getFlamegraphSpanGroupValue, getSpanColor } from '../utils';
import { MOCK_SPAN } from './testUtils';
const mockGenerateColorPair = jest.fn();
const mockGenerateColor = jest.fn();
jest.mock('pages/TraceDetailsV3/utils/generateColorPair', () => ({
generateColorPair: (name: string): { color: string; colorDark: string } =>
mockGenerateColorPair(name),
RESERVED_ERROR: '#FC4E4E',
darkenHex: (hex: string): string => hex,
jest.mock('lib/uPlotLib/utils/generateColor', () => ({
generateColor: (key: string, colorMap: Record<string, string>): string =>
mockGenerateColor(key, colorMap),
}));
const SERVICE_FIELD: TelemetryFieldKey = {
@@ -26,39 +24,48 @@ const HOST_FIELD: TelemetryFieldKey = {
describe('Presentation / Styling Utils', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGenerateColorPair.mockReturnValue({
color: '#2F80ED',
colorDark: '#1a4d99',
});
mockGenerateColor.mockReturnValue('#2F80ED');
});
describe('getSpanColor', () => {
it('uses generated colour from groupValue for normal span', () => {
mockGenerateColorPair.mockReturnValue({
color: '#1890ff',
colorDark: '#0d5599',
});
mockGenerateColor.mockReturnValue('#1890ff');
const result = getSpanColor({
const color = getSpanColor({
span: { ...MOCK_SPAN, hasError: false },
isDarkMode: false,
groupValue: 'my-bucket',
});
expect(mockGenerateColorPair).toHaveBeenCalledWith('my-bucket');
expect(result.color).toBe('#1890ff');
expect(result.colorDark).toBe('#0d5599');
expect(mockGenerateColor).toHaveBeenCalledWith(
'my-bucket',
expect.any(Object),
);
expect(color).toBe('#1890ff');
});
it('overrides with reserved error color when span has error', () => {
const result = getSpanColor({
it('overrides with error color in light mode when span has error', () => {
mockGenerateColor.mockReturnValue('#1890ff');
const color = getSpanColor({
span: { ...MOCK_SPAN, hasError: true },
isDarkMode: false,
groupValue: 'my-bucket',
});
expect(result.color).toBe('#FC4E4E');
expect(result.colorDark).toBe('#FC4E4E');
expect(color).toBe('rgb(220, 38, 38)');
});
it('overrides with error color in dark mode when span has error', () => {
mockGenerateColor.mockReturnValue('#1890ff');
const color = getSpanColor({
span: { ...MOCK_SPAN, hasError: true },
isDarkMode: true,
groupValue: 'my-bucket',
});
expect(color).toBe('rgb(239, 68, 68)');
});
});

View File

@@ -13,7 +13,7 @@ export const EVENT_DOT_SIZE_RATIO = EVENT_DOT_SIZE / SPAN_BAR_HEIGHT;
export const MIN_EVENT_DOT_SIZE = 4;
export const MAX_EVENT_DOT_SIZE = EVENT_DOT_SIZE;
export const LABEL_FONT = '500 11px Inter, sans-serif';
export const LABEL_FONT = '11px Inter, sans-serif';
export const LABEL_PADDING_X = 8;
export const MIN_WIDTH_FOR_NAME = 30;
export const MIN_WIDTH_FOR_NAME_AND_DURATION = 80;

View File

@@ -1,5 +1,6 @@
import React, { RefObject, useCallback, useMemo, useRef } from 'react';
import { generateColorPair } from 'pages/TraceDetailsV3/utils/generateColorPair';
import { themeColors } from 'constants/theme';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { useTraceStore } from 'pages/TraceDetailsV3/stores/traceStore';
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
@@ -117,11 +118,7 @@ function drawLevel(args: DrawLevelArgs): void {
width = clamp(width, 1, Infinity);
const groupValue = getFlamegraphSpanGroupValue(span, colorByField);
const { color, colorDark } = getSpanColor({
span,
isDarkMode,
groupValue,
});
const color = getSpanColor({ span, isDarkMode, groupValue });
const isDimmedByFilter =
!!isFilterActiveInLevel &&
@@ -138,7 +135,6 @@ function drawLevel(args: DrawLevelArgs): void {
spanRectsArray,
eventRectsArray,
color,
colorDark,
isDarkMode,
metrics,
selectedSpanId,
@@ -159,7 +155,6 @@ interface DrawConnectorLinesArgs {
viewportHeight: number;
metrics: FlamegraphRowMetrics;
colorByField: TelemetryFieldKey;
isDarkMode: boolean;
}
function drawConnectorLines(args: DrawConnectorLinesArgs): void {
@@ -173,7 +168,6 @@ function drawConnectorLines(args: DrawConnectorLinesArgs): void {
viewportHeight,
metrics,
colorByField,
isDarkMode,
} = args;
ctx.save();
@@ -203,8 +197,8 @@ function drawConnectorLines(args: DrawConnectorLinesArgs): void {
{ serviceName: conn.serviceName, resource: conn.resource },
colorByField,
);
const pair = generateColorPair(groupValue);
ctx.strokeStyle = isDarkMode ? pair.color : pair.colorDark;
const color = generateColor(groupValue, themeColors.traceDetailColorsV3);
ctx.strokeStyle = color;
const x = clamp(xFrac * cssWidth, 0, cssWidth);
ctx.beginPath();
@@ -300,7 +294,6 @@ export function useFlamegraphDraw(
viewportHeight,
metrics,
colorByField,
isDarkMode,
});
const spanRectsArray: SpanRect[] = [];

View File

@@ -211,14 +211,11 @@ export function useFlamegraphHover(
durationMs: span.durationNano / 1e6,
clientX: e.clientX,
clientY: e.clientY,
spanColor: ((): string => {
const pair = getSpanColor({
span,
isDarkMode,
groupValue: getFlamegraphSpanGroupValue(span, colorByField),
});
return isDarkMode ? pair.color : pair.colorDark;
})(),
spanColor: getSpanColor({
span,
isDarkMode,
groupValue: getFlamegraphSpanGroupValue(span, colorByField),
}),
event: {
name: event.name,
timeOffsetMs: eventTimeMs - span.timestamp,
@@ -247,14 +244,11 @@ export function useFlamegraphHover(
durationMs: span.durationNano / 1e6,
clientX: e.clientX,
clientY: e.clientY,
spanColor: ((): string => {
const pair = getSpanColor({
span,
isDarkMode,
groupValue: getFlamegraphSpanGroupValue(span, colorByField),
});
return isDarkMode ? pair.color : pair.colorDark;
})(),
spanColor: getSpanColor({
span,
isDarkMode,
groupValue: getFlamegraphSpanGroupValue(span, colorByField),
}),
previewRows: buildPreviewRows(span),
});
updateCursor(canvas, span);

View File

@@ -1,12 +1,8 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { themeColors } from 'constants/theme';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { getSpanAttribute } from 'pages/TraceDetailsV3/utils';
import {
ColorPair,
darkenHex,
generateColorPair,
RESERVED_ERROR,
} from 'pages/TraceDetailsV3/utils/generateColorPair';
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
@@ -110,12 +106,15 @@ interface GetSpanColorArgs {
groupValue: string;
}
export function getSpanColor(args: GetSpanColorArgs): ColorPair {
const { span, groupValue } = args;
export function getSpanColor(args: GetSpanColorArgs): string {
const { span, isDarkMode, groupValue } = args;
let color = generateColor(groupValue, themeColors.traceDetailColorsV3);
if (span.hasError) {
return { color: RESERVED_ERROR, colorDark: RESERVED_ERROR };
color = isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)';
}
return generateColorPair(groupValue);
return color;
}
export interface EventDotColor {
@@ -131,8 +130,8 @@ export function getEventDotColor(
): EventDotColor {
if (isError) {
return {
fill: RESERVED_ERROR,
stroke: darkenHex(RESERVED_ERROR, 0.22),
fill: isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)',
stroke: isDarkMode ? 'rgb(185, 28, 28)' : 'rgb(153, 27, 27)',
};
}
@@ -210,9 +209,6 @@ interface DrawSpanBarArgs {
spanRectsArray: SpanRect[];
eventRectsArray: EventRect[];
color: string;
// Darkened variant used as foreground (stroke + label) on light mode
// hover/selected, where the base color sits against a near-white panel.
colorDark: string;
isDarkMode: boolean;
metrics: FlamegraphRowMetrics;
selectedSpanId?: string | null;
@@ -232,7 +228,6 @@ export function drawSpanBar(args: DrawSpanBarArgs): void {
spanRectsArray,
eventRectsArray,
color,
colorDark,
isDarkMode,
metrics,
selectedSpanId,
@@ -264,21 +259,15 @@ export function drawSpanBar(args: DrawSpanBarArgs): void {
if (isSelected) {
ctx.setLineDash(DASHED_BORDER_LINE_DASH);
}
ctx.strokeStyle = isDarkMode ? color : colorDark;
ctx.strokeStyle = color;
ctx.lineWidth = isSelected ? 2 : 1;
ctx.stroke();
if (isSelected) {
ctx.setLineDash([]);
}
} else {
// Light mode uses the darkened variant as fill so bars contrast against
// the white panel background; dark mode keeps the bright base.
ctx.fillStyle = isDarkMode ? color : colorDark;
ctx.fillStyle = color;
ctx.fill();
// Subtle outline to match spec: 1px semi-transparent black border at rest
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 1;
ctx.stroke();
}
spanRectsArray.push({
@@ -303,10 +292,7 @@ export function drawSpanBar(args: DrawSpanBarArgs): void {
const eventX = x + (clampedOffset / 100) * width;
const eventY = spanY + metrics.SPAN_BAR_HEIGHT / 2;
// Event dots derive from the effective bar color so they track the
// light/dark variant the bar is rendered with.
const parentBarColor = isDarkMode ? color : colorDark;
const dotColor = getEventDotColor(parentBarColor, event.isError, isDarkMode);
const dotColor = getEventDotColor(color, event.isError, isDarkMode);
const eventKey = `${span.spanId}-${event.name}-${event.timeUnixNano}`;
const isEventHovered = hoveredEventKey === eventKey;
const dotSize = isEventHovered
@@ -342,7 +328,6 @@ export function drawSpanBar(args: DrawSpanBarArgs): void {
y: spanY,
width,
color,
colorDark,
isSelectedOrHovered,
isDarkMode,
spanBarHeight: metrics.SPAN_BAR_HEIGHT,
@@ -362,7 +347,6 @@ interface DrawSpanLabelArgs {
y: number;
width: number;
color: string;
colorDark: string;
isSelectedOrHovered: boolean;
isDarkMode: boolean;
spanBarHeight: number;
@@ -376,7 +360,6 @@ function drawSpanLabel(args: DrawSpanLabelArgs): void {
y,
width,
color,
colorDark,
isSelectedOrHovered,
isDarkMode,
spanBarHeight,
@@ -396,12 +379,11 @@ function drawSpanLabel(args: DrawSpanLabelArgs): void {
ctx.clip();
ctx.font = LABEL_FONT;
const hoverLabelColor = isDarkMode ? color : colorDark;
ctx.fillStyle = isSelectedOrHovered
? hoverLabelColor
? color
: isDarkMode
? 'rgba(0, 0, 0, 0.7)'
: 'rgba(255, 255, 255, 0.95)';
? 'rgba(0, 0, 0, 0.9)'
: 'rgba(255, 255, 255, 0.9)';
ctx.textBaseline = 'middle';
const textY = y + spanBarHeight / 2;

View File

@@ -1,7 +1,6 @@
import { ChangeEvent, useEffect, useMemo, useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Spin } from 'antd';
import { Input, Spin } from 'antd';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import SignozModal from 'components/SignozModal/SignozModal';

View File

@@ -433,7 +433,6 @@
border-radius: 50%;
flex-shrink: 0;
margin: 0 6px;
background-color: var(--service-dot-color);
&.hasError {
box-shadow: 0 0 0 2px rgba(255, 70, 70, 0.3);
@@ -515,7 +514,7 @@
.spanBar {
position: absolute;
height: 18px;
top: 3px;
top: 5px;
border-radius: 2px;
display: flex;
align-items: center;
@@ -523,9 +522,7 @@
overflow: hidden;
cursor: pointer;
white-space: nowrap;
// Theme-resolved in JS: dark text on the bright dark-mode fill, white text on
// the darkened light-mode fill. See SpanDuration in Success.tsx.
color: var(--span-text-color);
color: rgba(0, 0, 0, 0.9);
background-color: var(--span-color);
border: 1px solid transparent;
}
@@ -551,6 +548,7 @@
.spanDurationText {
color: inherit;
opacity: 0.8;
font-size: 10px;
margin-left: 8px;
flex-shrink: 0;
@@ -609,6 +607,25 @@
}
}
// `.spanBar` text color is the one place where semantic tokens don't fit
// cleanly: in dark mode the bar's bright `--span-color` background needs dark
// text; in light mode `generateColor` produces darker bar fills, so the text
// must flip to white.
:global(.lightMode) {
.root {
.spanDuration .spanBar {
color: rgba(255, 255, 255, 0.9);
}
.timelineRow:hover .spanBar,
.timelineRow.hoveredSpan .spanBar,
.isInterested .spanBar,
.isSelectedNonMatching .spanBar {
color: var(--span-color);
}
}
}
// Tooltips for the row's hover-revealed action buttons (Copy / Add to Funnel).
// Bumped above FloatingPanel (z-index 999) so they stay visible when the
// SpanDetailsPanel is docked as a floating panel.

View File

@@ -29,7 +29,6 @@ import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
import TimelineV3 from 'components/TimelineV3/TimelineV3';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import { useCopySpanLink } from 'hooks/trace/useCopySpanLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { colorToRgb } from 'lib/uPlotLib/utils/generateColor';
@@ -215,12 +214,8 @@ const SpanOverview = memo(function SpanOverview({
const isRootSpan = span.level === 0;
const { onSpanCopy } = useCopySpanLink(span);
const colorByFieldName = useTraceStore((s) => s.colorByField.name);
const isDarkMode = useIsDarkMode();
const { color, colorDark } = resolveSpanColor(span, colorByFieldName);
// Single theme-resolved color: bright base in dark mode, darkened variant in
// light mode so the dot stands out against the white panel.
const effectiveColor = isDarkMode ? color : colorDark;
const color = resolveSpanColor(span, colorByFieldName);
// Smart highlighting logic
const {
@@ -322,11 +317,7 @@ const SpanOverview = memo(function SpanOverview({
{/* Colored service dot */}
<span
className={cx(styles.treeIcon, { [styles.hasError]: span.has_error })}
style={
{
'--service-dot-color': effectiveColor,
} as React.CSSProperties
}
style={{ backgroundColor: color }}
/>
{/* Span name + service name */}
@@ -400,16 +391,9 @@ export const SpanDuration = memo(function SpanDuration({
const width = (span.duration_nano * 1e2) / (spread * 1e6);
const colorByFieldName = useTraceStore((s) => s.colorByField.name);
const isDarkMode = useIsDarkMode();
const { color, colorDark } = resolveSpanColor(span, colorByFieldName);
// Single theme-resolved color: bright base in dark mode, darkened variant in
// light mode (so the bar stands out against the white panel and hover/selected
// foregrounds stay legible). The bar's text flips dark↔white to suit the fill.
const effectiveColor = isDarkMode ? color : colorDark;
const rgbColor = colorToRgb(effectiveColor);
const spanTextColor = isDarkMode
? 'rgba(0, 0, 0, 0.7)'
: 'rgba(255, 255, 255, 0.95)';
const color = resolveSpanColor(span, colorByFieldName);
// `resolveSpanColor` returns a CSS variable for errors; `colorToRgb` can't parse it.
const rgbColor = span.has_error ? '239, 68, 68' : colorToRgb(color);
const {
isSelected,
@@ -440,9 +424,8 @@ export const SpanDuration = memo(function SpanDuration({
{
left: `${leftOffset}%`,
width: `${width}%`,
'--span-color': effectiveColor,
'--span-color': color,
'--span-color-rgb': rgbColor,
'--span-text-color': spanTextColor,
} as React.CSSProperties
}
>

View File

@@ -103,7 +103,6 @@ jest.mock('components/TimelineV3/TimelineV3', () => {
jest.mock('lib/uPlotLib/utils/generateColor', () => ({
generateColor: (): string => '#1890ff',
colorToRgb: (): string => '24, 144, 255',
hashFn: (): number => 0,
}));
jest.mock('container/TraceDetail/utils', () => ({

View File

@@ -1,11 +1,7 @@
import { themeColors } from 'constants/theme';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { SpanV3 } from 'types/api/trace/getTraceV3';
import {
ColorPair,
generateColorPair,
RESERVED_ERROR,
} from './utils/generateColorPair';
/**
* Look up an attribute from both `resource` and `attributes` on a span.
* Resources are checked first (service.name, k8s.* etc. live there).
@@ -96,16 +92,18 @@ export function getSpanGroupValue(
/**
* Resolves the rendering colour for a span. Error spans always get the
* reserved error colour; everything else is derived deterministically from its
* group value via `generateColorPair`. Returns both the base color and a
* darkened variant for light-mode hover/selected foregrounds.
* semantic destructive colour; everything else is derived deterministically
* from its group value via `generateColor`.
*/
export function resolveSpanColor(
span: SpanV3,
colorByFieldName: string,
): ColorPair {
): string {
if (span.has_error) {
return { color: RESERVED_ERROR, colorDark: RESERVED_ERROR };
return 'var(--destructive)';
}
return generateColorPair(getSpanGroupValue(span, colorByFieldName));
return generateColor(
getSpanGroupValue(span, colorByFieldName),
themeColors.traceDetailColorsV3,
);
}

View File

@@ -1,126 +0,0 @@
import {
darkenHex,
generateColorPair,
PALETTE_V3,
RESERVED_ERROR,
RESERVED_OK,
RESERVED_WARNING,
} from '../generateColorPair';
describe('generateColorPair', () => {
it('is deterministic: same name returns the same pair across calls', () => {
const a = generateColorPair('payment-service');
const b = generateColorPair('payment-service');
expect(a).toBe(b); // cache hit returns the same reference
expect(a.color).toBe(b.color);
expect(a.colorDark).toBe(b.colorDark);
});
it('returns a palette color for a normal name', () => {
const { color } = generateColorPair('any-service');
expect(PALETTE_V3).toContain(color);
});
it('colorDark differs from color (darker variant computed via darkenHex)', () => {
const { color, colorDark } = generateColorPair('checkout-svc');
expect(colorDark).not.toBe(color);
expect(colorDark).toMatch(/^#[0-9a-f]{6}$/i);
});
it('produces different colors for different names (palette wraps modulo length)', () => {
const a = generateColorPair('aaa');
const b = generateColorPair('bbb');
// Not strictly guaranteed (hash collisions exist with 28 buckets), but
// for these two short strings djb2 produces different bucket indices.
expect(a.color).not.toBe(b.color);
});
});
describe('darkenHex', () => {
it('returns a darker hex than the input for amount > 0', () => {
const input = '#4D6BD8';
const out = darkenHex(input, 0.22);
expect(out).toMatch(/^#[0-9a-f]{6}$/i);
expect(out).not.toBe(input);
});
it('handles amount = 0 as a near-identity', () => {
const out = darkenHex('#4D6BD8', 0);
// HSL round-trip may shift a digit; only assert format.
expect(out).toMatch(/^#[0-9a-f]{6}$/i);
});
});
describe('reserved status colors', () => {
it('matches spec section 8 hexes', () => {
expect(RESERVED_ERROR).toBe('#FC4E4E');
expect(RESERVED_WARNING).toBe('#fbbf24');
expect(RESERVED_OK).toBe('#4ade80');
});
});
// Visual inspection table: each palette color paired with its darkenHex(0.22)
// variant. Confirms the darkening produces a distinct, non-collapsed hex per
// entry. Run with `yarn jest generateColorPair --verbose` to see the table.
describe('PALETTE_V3 darken-pair table', () => {
const PALETTE_NAMES = [
'Slate blue',
'Sage',
'Amber',
'Dusty pink',
'Lavender',
'Peach',
'Sky teal',
'Fuchsia',
'Terracotta',
'Forest',
'Cornflower',
'Iris',
'Olive gold',
'Mint',
'Mauve',
'Dusty teal',
'Burnt orange',
'Pistachio',
'Periwinkle',
'Coral blush',
'Sienna',
'Robin',
'Sandy gold',
'Powder blue',
'Umber',
'Aqua',
'Warm tan',
'Antique rose',
];
it.each(
PALETTE_V3.map((hex, i) => [PALETTE_NAMES[i] ?? `idx-${i}`, hex] as const),
)('%s (%s) darkens to a distinct hex', (name, hex) => {
const dark = darkenHex(hex, 0.22);
expect(dark).toMatch(/^#[0-9a-f]{6}$/i);
expect(dark.toLowerCase()).not.toBe(hex.toLowerCase());
});
it('all 28 darkened variants are unique (no collisions)', () => {
const darks = PALETTE_V3.map((hex) => darkenHex(hex, 0.22).toLowerCase());
const unique = new Set(darks);
expect(unique.size).toBe(PALETTE_V3.length);
});
it('prints the base→dark table for visual inspection', () => {
// eslint-disable-next-line no-console
console.log('\nPALETTE_V3 base → darkenHex(0.22) pairs:');
// eslint-disable-next-line no-console
console.log('idx name base dark');
PALETTE_V3.forEach((hex, i) => {
const dark = darkenHex(hex, 0.22);
const name = (PALETTE_NAMES[i] ?? '').padEnd(13);
const idx = String(i).padStart(2, ' ');
// eslint-disable-next-line no-console
console.log(`${idx} ${name} ${hex} ${dark}`);
});
// Sentinel assertion so the test is not flagged as having none.
expect(PALETTE_V3).toHaveLength(28);
});
});

View File

@@ -1,116 +0,0 @@
// Source-of-truth doc: ./COLOR_PALETTE.md
//
// Color system for TraceDetailsV3 (waterfall + flamegraph). Returns a base
// color (deterministic per group name) plus a darkened variant used as the
// light-mode foreground / fill. Reuses the shared djb2 `hashFn`.
import { hashFn } from 'lib/uPlotLib/utils/generateColor';
// 28 colors from the doc's "Updated Colour Palette" (Section 1), in doc order.
// Hash output `% PALETTE.length` adjusts automatically if entries are added.
export const PALETTE_V3: readonly string[] = [
'#4D6BD8', // Slate blue
'#84B270', // Sage
'#EB9E40', // Amber
'#D58998', // Dusty pink
'#8278D5', // Lavender
'#E69C6F', // Peach
'#3CB4DA', // Sky teal
'#E85DA8', // Fuchsia
'#D4694A', // Terracotta
'#4FCC8E', // Forest
'#5BA2D6', // Cornflower
'#9D57D0', // Iris
'#D4B638', // Olive gold
'#6CC4A4', // Mint
'#D188CB', // Mauve
'#2FB59B', // Dusty teal
'#E68340', // Burnt orange
'#B8C474', // Pistachio
'#3C84E5', // Periwinkle
'#E29F8E', // Coral blush
'#C56330', // Sienna
'#4E8CF8', // Robin
'#E8B752', // Sandy gold
'#8DBEDF', // Powder blue
'#8B7544', // Umber
'#23E0E8', // Aqua
'#CB874A', // Warm tan
'#C886A9', // Antique rose
];
// Reserved status colors per spec section 8. Error is wired today;
// warning + OK are exported for future use (no render path consumes them yet).
export const RESERVED_ERROR = '#FC4E4E';
export const RESERVED_WARNING = '#fbbf24';
export const RESERVED_OK = '#4ade80';
function hexToHsl(hex: string): [number, number, number] {
const n = parseInt(hex.slice(1), 16);
const r = ((n >> 16) & 255) / 255;
const g = ((n >> 8) & 255) / 255;
const b = (n & 255) / 255;
const mx = Math.max(r, g, b);
const mn = Math.min(r, g, b);
const d = mx - mn;
const l = (mx + mn) / 2;
const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
let h: number;
if (d === 0) {
h = 0;
} else if (mx === r) {
h = 60 * (((g - b) / d) % 6);
} else if (mx === g) {
h = 60 * ((b - r) / d + 2);
} else {
h = 60 * ((r - g) / d + 4);
}
return [(h + 360) % 360, s * 100, l * 100];
}
function hslToHex(h: number, s: number, l: number): string {
const S = s / 100;
const L = l / 100;
const k = (n: number): number => (n + h / 30) % 12;
const a = S * Math.min(L, 1 - L);
const f = (n: number): number =>
Math.round(
255 * (L - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))),
);
return `#${[f(0), f(8), f(4)]
.map((v) => v.toString(16).padStart(2, '0'))
.join('')}`;
}
// Gentle darken: compress lightness relatively (l reduced by ~amount*0.45 of
// itself) and barely bump saturation. hexToHsl here returns 0100, so the
// spec's 01 saturation step (`amount * 0.06`) is scaled by 100.
export function darkenHex(hex: string, amount: number): string {
const [h, s, l] = hexToHsl(hex);
const newL = Math.max(0, l - l * amount * 0.45);
const newS = Math.min(100, s + amount * 6);
return hslToHex(h, newS, newL);
}
export interface ColorPair {
color: string;
colorDark: string;
}
// Distinct-name cardinality is bounded by deployment service count (~10s, not 1000s),
// so unbounded growth is not a concern.
const cache = new Map<string, ColorPair>();
export function generateColorPair(name: string): ColorPair {
const hit = cache.get(name);
if (hit) {
return hit;
}
const base = PALETTE_V3[hashFn(name) % PALETTE_V3.length];
const result: ColorPair = {
color: base,
colorDark: darkenHex(base, 0.22),
};
cache.set(name, result);
return result;
}

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useQueryClient } from 'react-query';
import { Input } from '@signozhq/ui/input';
import { Input } from 'antd';
import SignozModal from 'components/SignozModal/SignozModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useRenameFunnel } from 'hooks/TracesFunnels/useFunnels';

View File

@@ -9,10 +9,10 @@
.ant-input-prefix {
margin-inline-end: 6px;
}
&,
input {
font-family: Inter;
background: var(--l2-background);
font-size: 14px;
line-height: 18px;
font-style: normal;

View File

@@ -1,7 +1,6 @@
import { ChangeEvent } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Input } from '@signozhq/ui/input';
import { Button, Popover, Tooltip } from 'antd';
import { Button, Input, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { ArrowDownWideNarrow, Check, Plus, Search } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';

View File

@@ -204,12 +204,5 @@
background: var(--l2-background);
height: 38px;
width: 38px;
&:focus:not(:focus-visible),
&.ant-btn:focus:not(:focus-visible) {
border-color: var(--l2-border);
outline: none;
box-shadow: none;
}
}
}

View File

@@ -86,6 +86,12 @@ func New(
func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest) (*qbtypes.QueryRangeResponse, error) {
// Coerce the window to epoch milliseconds up front so every downstream
// consumer (TimeRange, narrowWindowByTraceID, step interval, etc.) can
// safely assume ms regardless of the resolution the caller sent.
req.Start = querybuilder.ToMilliSecs(req.Start)
req.End = querybuilder.ToMilliSecs(req.End)
tmplVars := req.Variables
if tmplVars == nil {
tmplVars = make(map[string]qbtypes.VariableItem)
@@ -408,6 +414,11 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.Q
func (q *querier) QueryRawStream(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, client *qbtypes.RawStream) {
// Coerce the window to epoch milliseconds up front (End may be 0 for the
// open-ended stream, which ToMilliSecs leaves untouched).
req.Start = querybuilder.ToMilliSecs(req.Start)
req.End = querybuilder.ToMilliSecs(req.End)
event := &qbtypes.QBEvent{
Version: "v5",
NumberOfQueries: len(req.CompositeQuery.Queries),

View File

@@ -2,11 +2,14 @@ package logparsingpipeline
import (
"encoding/json"
"fmt"
"strings"
"sync"
"gopkg.in/yaml.v3"
"log/slog"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
@@ -23,13 +26,6 @@ var (
CodeCollectorConfigLogsPipelineNotFound = errors.MustNewCode("collector_config_logs_pipeline_not_found")
)
const (
memoryLimiterProcessor = "memory_limiter"
memoryLimiterProcessorPrefix = "memory_limiter/"
batchProcessor = "batch"
batchProcessorPrefix = "batch/"
)
// check if the processors already exist
// if yes then update the processor.
// if something doesn't exists then remove it.
@@ -83,13 +79,6 @@ func getOtelPipelineFromConfig(config map[string]interface{}) (*otelPipeline, er
return &p, nil
}
// buildCollectorPipelineProcessorsList assembles the final processor list in the
// required order:
//
// 1. memory_limiter processors (any processor named "memory_limiter" or "memory_limiter/<id>")
// 2. signoz user-pipeline processors (in the order given by signozPipelineProcessorNames)
// 3. custom processors (non-signoz, non-memory_limiter, non-batch processors from the current config)
// 4. batch processors (any processor named "batch" or "batch/<id>") and anything after them
func buildCollectorPipelineProcessorsList(
currentCollectorProcessors []string,
signozPipelineProcessorNames []string,
@@ -97,42 +86,90 @@ func buildCollectorPipelineProcessorsList(
lockLogsPipelineSpec.Lock()
defer lockLogsPipelineSpec.Unlock()
// Build a set of user pipeline names so custom processors can skip duplicates.
userPipelineSet := make(map[string]struct{}, len(signozPipelineProcessorNames))
for _, p := range signozPipelineProcessorNames {
userPipelineSet[p] = struct{}{}
exists := map[string]struct{}{}
for _, v := range signozPipelineProcessorNames {
exists[v] = struct{}{}
}
var memoryLimiters []string
var customProcessors []string
batchIdx := -1
// removed the old processors which are not used
var pipeline []string
for _, procName := range currentCollectorProcessors {
_, isInDesiredPipelineProcs := exists[procName]
if isInDesiredPipelineProcs || !hasSignozPipelineProcessorPrefix(procName) {
pipeline = append(pipeline, procName)
}
}
for idx, p := range currentCollectorProcessors {
switch {
case p == batchProcessor || strings.HasPrefix(p, batchProcessorPrefix):
batchIdx = idx
case p == memoryLimiterProcessor || strings.HasPrefix(p, memoryLimiterProcessorPrefix):
memoryLimiters = append(memoryLimiters, p)
case hasSignozPipelineProcessorPrefix(p):
// stale signoz pipeline processor — dropped; signozPipelineProcessorNames is authoritative
default:
if _, inUserPipelines := userPipelineSet[p]; !inUserPipelines {
customProcessors = append(customProcessors, p)
// create a reverse map of existing config processors and their position
existing := map[string]int{}
for i, p := range pipeline {
name := p
existing[name] = i
}
// create mapping from our logsParserPipeline to position in existing processors (from current config)
// this means, if "batch" holds position 3 in the current effective config, and 2 in our config, the map will be [2]: 3
specVsExistingMap := map[int]int{}
existingVsSpec := map[int]int{}
// go through plan and map its elements to current positions in effective config
for i, m := range signozPipelineProcessorNames {
if loc, ok := existing[m]; ok {
specVsExistingMap[i] = loc
existingVsSpec[loc] = i
}
}
lastMatched := 0
newPipeline := []string{}
for i := 0; i < len(signozPipelineProcessorNames); i++ {
m := signozPipelineProcessorNames[i]
if loc, ok := specVsExistingMap[i]; ok {
for j := lastMatched; j < loc; j++ {
if hasSignozPipelineProcessorPrefix(pipeline[j]) {
delete(specVsExistingMap, existingVsSpec[j])
} else {
newPipeline = append(newPipeline, pipeline[j])
}
}
newPipeline = append(newPipeline, pipeline[loc])
lastMatched = loc + 1
} else {
newPipeline = append(newPipeline, m)
}
if batchIdx >= 0 {
break
}
}
if lastMatched < len(pipeline) {
newPipeline = append(newPipeline, pipeline[lastMatched:]...)
}
result := make([]string, 0, len(currentCollectorProcessors)+len(signozPipelineProcessorNames))
result = append(result, memoryLimiters...)
result = append(result, signozPipelineProcessorNames...)
result = append(result, customProcessors...)
if batchIdx >= 0 {
result = append(result, currentCollectorProcessors[batchIdx:]...)
if checkDuplicateString(newPipeline) {
// duplicates are most likely because the processor sequence in effective config conflicts
// with the planned sequence as per planned pipeline
return pipeline, fmt.Errorf("the effective config has an unexpected processor sequence: %v", pipeline)
}
return result, nil
return newPipeline, nil
}
func checkDuplicateString(pipeline []string) bool {
exists := make(map[string]bool, len(pipeline))
slog.Debug("checking duplicate processors in the pipeline", "pipeline", pipeline)
for _, processor := range pipeline {
name := processor
if _, ok := exists[name]; ok {
slog.Error(
"duplicate processor name detected in generated collector config for log pipelines",
"processor", processor,
"pipeline", pipeline,
)
return true
}
exists[name] = true
}
return false
}
func GenerateCollectorConfigWithPipelines(config []byte, pipelines []pipelinetypes.GettablePipeline) ([]byte, error) {

View File

@@ -106,109 +106,107 @@ func TestBuildLogParsingProcessors(t *testing.T) {
}
var BuildLogsPipelineTestData = []struct {
Name string
fromCollector []string
userPipelines []string
finalOutput []string
Name string
currentPipeline []string
logsPipeline []string
expectedPipeline []string
}{
{
Name: "Add new pipelines",
fromCollector: []string{"processor1", "processor2"},
userPipelines: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b"},
finalOutput: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", "processor1", "processor2"},
Name: "Add new pipelines",
currentPipeline: []string{"processor1", "processor2"},
logsPipeline: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b"},
expectedPipeline: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", "processor1", "processor2"},
},
{
Name: "Add new pipeline and respect custom processors",
fromCollector: []string{constants.LogsPPLPfx + "a", "processor1", constants.LogsPPLPfx + "b", "processor2"},
userPipelines: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c"},
finalOutput: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c", "processor1", "processor2"},
Name: "Add new pipeline and respect custom processors",
currentPipeline: []string{constants.LogsPPLPfx + "a", "processor1", constants.LogsPPLPfx + "b", "processor2"},
logsPipeline: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c"},
expectedPipeline: []string{constants.LogsPPLPfx + "a", "processor1", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c", "processor2"},
},
{
Name: "Add new pipeline and respect custom processors with more",
fromCollector: []string{constants.LogsPPLPfx + "a", "processor1", constants.LogsPPLPfx + "b", "processor2"},
userPipelines: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c", constants.LogsPPLPfx + "d"},
finalOutput: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c", constants.LogsPPLPfx + "d", "processor1", "processor2"},
Name: "Add new pipeline and respect custom processors",
currentPipeline: []string{constants.LogsPPLPfx + "a", "processor1", constants.LogsPPLPfx + "b", "processor2"},
logsPipeline: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c", constants.LogsPPLPfx + "d"},
expectedPipeline: []string{constants.LogsPPLPfx + "a", "processor1", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c", constants.LogsPPLPfx + "d", "processor2"},
},
{
Name: "Add new pipeline and respect custom processors in the beginning and middle",
fromCollector: []string{"processor1", constants.LogsPPLPfx + "a", "processor2", constants.LogsPPLPfx + "b", "batch"},
userPipelines: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c"},
finalOutput: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c", "processor1", "processor2", "batch"},
Name: "Add new pipeline and respect custom processors in the beginning and middle",
currentPipeline: []string{"processor1", constants.LogsPPLPfx + "a", "processor2", constants.LogsPPLPfx + "b", "batch"},
logsPipeline: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c"},
expectedPipeline: []string{"processor1", constants.LogsPPLPfx + "a", "processor2", constants.LogsPPLPfx + "b", constants.LogsPPLPfx + "c", "batch"},
},
{
Name: "Remove old pipeline add add new",
fromCollector: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", "processor1", "processor2"},
userPipelines: []string{constants.LogsPPLPfx + "a"},
finalOutput: []string{constants.LogsPPLPfx + "a", "processor1", "processor2"},
Name: "Remove old pipeline add add new",
currentPipeline: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "b", "processor1", "processor2"},
logsPipeline: []string{constants.LogsPPLPfx + "a"},
expectedPipeline: []string{constants.LogsPPLPfx + "a", "processor1", "processor2"},
},
{
Name: "Remove old pipeline from middle",
fromCollector: []string{"processor1", "processor2", constants.LogsPPLPfx + "a", "processor3", constants.LogsPPLPfx + "b", "batch"},
userPipelines: []string{constants.LogsPPLPfx + "a"},
finalOutput: []string{constants.LogsPPLPfx + "a", "processor1", "processor2", "processor3", "batch"},
Name: "Remove old pipeline from middle",
currentPipeline: []string{"processor1", "processor2", constants.LogsPPLPfx + "a", "processor3", constants.LogsPPLPfx + "b", "batch"},
logsPipeline: []string{constants.LogsPPLPfx + "a"},
expectedPipeline: []string{"processor1", "processor2", constants.LogsPPLPfx + "a", "processor3", "batch"},
},
{
Name: "Remove old pipeline from middle and add new pipeline",
fromCollector: []string{"memory_limiter", "processor1", "processor2", constants.LogsPPLPfx + "a", "processor3", constants.LogsPPLPfx + "b", "batch"},
userPipelines: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "c"},
finalOutput: []string{"memory_limiter", constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "c", "processor1", "processor2", "processor3", "batch"},
Name: "Remove old pipeline from middle and add new pipeline",
currentPipeline: []string{"processor1", "processor2", constants.LogsPPLPfx + "a", "processor3", constants.LogsPPLPfx + "b", "batch"},
logsPipeline: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "c"},
expectedPipeline: []string{"processor1", "processor2", constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "c", "processor3", "batch"},
},
{
Name: "Remove multiple old pipelines from middle and add multiple new ones",
fromCollector: []string{"processor1", constants.LogsPPLPfx + "a", "processor2", constants.LogsPPLPfx + "b", "processor3", constants.LogsPPLPfx + "c", "processor4", constants.LogsPPLPfx + "d", "processor5", "batch"},
userPipelines: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "a1", constants.LogsPPLPfx + "c", constants.LogsPPLPfx + "c1"},
finalOutput: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "a1", constants.LogsPPLPfx + "c", constants.LogsPPLPfx + "c1", "processor1", "processor2", "processor3", "processor4", "processor5", "batch"},
Name: "Remove multiple old pipelines from middle and add multiple new ones",
currentPipeline: []string{"processor1", constants.LogsPPLPfx + "a", "processor2", constants.LogsPPLPfx + "b", "processor3", constants.LogsPPLPfx + "c", "processor4", constants.LogsPPLPfx + "d", "processor5", "batch"},
logsPipeline: []string{constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "a1", constants.LogsPPLPfx + "c", constants.LogsPPLPfx + "c1"},
expectedPipeline: []string{"processor1", constants.LogsPPLPfx + "a", constants.LogsPPLPfx + "a1", "processor2", "processor3", constants.LogsPPLPfx + "c", constants.LogsPPLPfx + "c1", "processor4", "processor5", "batch"},
},
// working
{
Name: "rearrange pipelines",
currentPipeline: []string{"processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", constants.LogsPPLPfx + "_b", "batch"},
logsPipeline: []string{constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a"},
expectedPipeline: []string{"processor1", "processor2", "processor3", constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", "batch"},
},
{
Name: "rearrange pipelines",
fromCollector: []string{"processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", constants.LogsPPLPfx + "_b", "batch"},
userPipelines: []string{constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a"},
finalOutput: []string{constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", "processor1", "processor2", "processor3", "batch"},
Name: "rearrange pipelines with new processor",
currentPipeline: []string{"processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", constants.LogsPPLPfx + "_b", "batch"},
logsPipeline: []string{constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_c"},
expectedPipeline: []string{"processor1", "processor2", "processor3", constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_c", "batch"},
// expectedPipeline: []string{"processor1", "processor2", constants.LogsPPLPfx + "_b", "processor3", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_c", "batch"},
},
{
Name: "rearrange pipelines with new processor",
fromCollector: []string{"processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", constants.LogsPPLPfx + "_b", "batch"},
userPipelines: []string{constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_c"},
finalOutput: []string{constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_c", "processor1", "processor2", "processor3", "batch"},
Name: "delete processor",
currentPipeline: []string{"processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", constants.LogsPPLPfx + "_b", "batch"},
logsPipeline: []string{},
expectedPipeline: []string{"processor1", "processor2", "processor3", "batch"},
},
{
Name: "delete processor",
fromCollector: []string{"processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", constants.LogsPPLPfx + "_b", "batch"},
userPipelines: []string{},
finalOutput: []string{"processor1", "processor2", "processor3", "batch"},
Name: "last to first",
currentPipeline: []string{"processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", "processor4", constants.LogsPPLPfx + "_b", "batch", constants.LogsPPLPfx + "_c"},
logsPipeline: []string{constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_b"},
expectedPipeline: []string{"processor1", "processor2", "processor3", "processor4", "batch", constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_b"},
},
{
Name: "last to first",
fromCollector: []string{"processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", "processor4", constants.LogsPPLPfx + "_b", "batch", constants.LogsPPLPfx + "_c"},
userPipelines: []string{constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_b"},
finalOutput: []string{constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_b", "processor1", "processor2", "processor3", "processor4", "batch", constants.LogsPPLPfx + "_c"},
Name: "multiple rearrange pipelines",
currentPipeline: []string{"processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", constants.LogsPPLPfx + "_b", "batch", constants.LogsPPLPfx + "_c", "processor4", "processor5", constants.LogsPPLPfx + "_d", "processor6", "processor7"},
logsPipeline: []string{constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_d", constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_e"},
expectedPipeline: []string{"processor1", "processor2", "processor3", constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", "batch", "processor4", "processor5", constants.LogsPPLPfx + "_d", constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_e", "processor6", "processor7"},
},
{
Name: "multiple rearrange pipelines",
fromCollector: []string{"processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", constants.LogsPPLPfx + "_b", "batch", constants.LogsPPLPfx + "_c", "processor4", "processor5", constants.LogsPPLPfx + "_d", "processor6", "processor7"},
userPipelines: []string{constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_d", constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_e"},
finalOutput: []string{constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_d", constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_e", "processor1", "processor2", "processor3", "batch", constants.LogsPPLPfx + "_c", "processor4", "processor5", constants.LogsPPLPfx + "_d", "processor6", "processor7"},
},
{
Name: "multiple rearrange with new pipelines",
fromCollector: []string{"memory_limiter", "processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", constants.LogsPPLPfx + "_b", "batch", constants.LogsPPLPfx + "_c", "processor4", "processor5", constants.LogsPPLPfx + "_d", "processor6", "processor7"},
userPipelines: []string{constants.LogsPPLPfx + "_z", constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_d", constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_e"},
finalOutput: []string{"memory_limiter", constants.LogsPPLPfx + "_z", constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_d", constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_e", "processor1", "processor2", "processor3", "batch", constants.LogsPPLPfx + "_c", "processor4", "processor5", constants.LogsPPLPfx + "_d", "processor6", "processor7"},
},
{
Name: "Prefixed proc in desired set not duplicated from others",
fromCollector: []string{"memory_limiter/logs", "custom_proc", "resourcedetection", "batch/logs"},
userPipelines: []string{"custom_proc", constants.LogsPPLPfx + "a"},
finalOutput: []string{"memory_limiter/logs", "custom_proc", constants.LogsPPLPfx + "a", "resourcedetection", "batch/logs"},
Name: "multiple rearrange with new pipelines",
currentPipeline: []string{"processor1", "processor2", constants.LogsPPLPfx + "_a", "processor3", constants.LogsPPLPfx + "_b", "batch", constants.LogsPPLPfx + "_c", "processor4", "processor5", constants.LogsPPLPfx + "_d", "processor6", "processor7"},
logsPipeline: []string{constants.LogsPPLPfx + "_z", constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", constants.LogsPPLPfx + "_d", constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_e"},
expectedPipeline: []string{constants.LogsPPLPfx + "_z", "processor1", "processor2", "processor3", constants.LogsPPLPfx + "_b", constants.LogsPPLPfx + "_a", "batch", "processor4", "processor5", constants.LogsPPLPfx + "_d", constants.LogsPPLPfx + "_c", constants.LogsPPLPfx + "_e", "processor6", "processor7"},
},
}
func TestBuildLogsPipeline(t *testing.T) {
for _, test := range BuildLogsPipelineTestData {
Convey(test.Name, t, func() {
v, err := buildCollectorPipelineProcessorsList(test.fromCollector, test.userPipelines)
v, err := buildCollectorPipelineProcessorsList(test.currentPipeline, test.logsPipeline)
So(err, ShouldBeNil)
So(v, ShouldResemble, test.finalOutput)
fmt.Println(test.Name, "\n", test.currentPipeline, "\n", v, "\n", test.expectedPipeline)
So(v, ShouldResemble, test.expectedPipeline)
})
}
}

View File

@@ -33,6 +33,28 @@ func ToNanoSecs(epoch uint64) uint64 {
return temp * uint64(math.Pow(10, float64(19-count)))
}
// ToMilliSecs takes an epoch whose resolution is inferred from its magnitude
// (s/ms/µs/ns) and returns it in milliseconds. A millisecond epoch for the
// current era has 13 digits (e.g. ~1.7e12 in 2026), so the value is scaled so
// its digit-width matches: smaller values (seconds) are scaled up, larger ones
// (micro/nanoseconds) are scaled down. Zero is returned unchanged.
func ToMilliSecs(epoch uint64) uint64 {
if epoch == 0 {
return 0
}
temp := epoch
count := 0
for epoch != 0 {
epoch /= 10
count++
}
const msDigits = 13
if count < msDigits {
return temp * uint64(math.Pow(10, float64(msDigits-count)))
}
return temp / uint64(math.Pow(10, float64(count-msDigits)))
}
// TODO(srikanthccv): should these be rounded to nearest multiple of 60 instead of 5 if step > 60?
// That would make graph look nice but "nice" but should be less important than the usefulness.
func RecommendedStepInterval(start, end uint64) uint64 {

View File

@@ -60,3 +60,51 @@ func TestToNanoSecs(t *testing.T) {
})
}
}
func TestToMilliSecs(t *testing.T) {
tests := []struct {
name string
epoch uint64
expected uint64
}{
{
name: "10-digit Unix timestamp (seconds) - 2023-01-01 00:00:00 UTC",
epoch: 1672531200, // seconds
expected: 1672531200000, // * 10^3
},
{
name: "13-digit Unix timestamp (milliseconds) - already ms",
epoch: 1672531200000,
expected: 1672531200000, // unchanged
},
{
name: "16-digit Unix timestamp (microseconds)",
epoch: 1672531200000000, // microseconds
expected: 1672531200000, // / 10^3
},
{
name: "19-digit Unix timestamp (nanoseconds)",
epoch: 1672531200000000000, // nanoseconds
expected: 1672531200000, // / 10^6
},
{
name: "Unix epoch start - zero is unchanged",
epoch: 0,
expected: 0,
},
{
name: "Recent timestamp in seconds - 2024-05-25 12:00:00 UTC",
epoch: 1716638400,
expected: 1716638400000,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ToMilliSecs(tt.epoch)
if result != tt.expected {
t.Errorf("ToMilliSecs(%d) = %d, want %d", tt.epoch, result, tt.expected)
}
})
}
}