Compare commits

...

22 Commits

Author SHA1 Message Date
nityanandagohain
c99a112dc7 fix: set default only if not present 2024-03-30 20:02:03 +05:30
nityanandagohain
e4d9c4e239 fix: error check 2024-03-30 19:39:52 +05:30
nityanandagohain
781732f25a Merge remote-tracking branch 'origin/develop' into issue_4777 2024-03-30 19:22:20 +05:30
nityanandagohain
77e55a0ec9 feat: allow query restrictions for log queries 2024-03-30 18:41:53 +05:30
Nityananda Gohain
a34c59762b feat: allow characters in attribute names (#4775) 2024-03-30 17:57:01 +05:30
Nityananda Gohain
397da5857f fix: enrich all queries with non materialized attributes (#4772) 2024-03-30 08:55:46 +05:30
Vikrant Gupta
43ceb052d8 feat: do not retry query range API's with i/o timeout error (#4768)
* feat: do not retry query range API's with i/o timeout error

* feat: do not retry query range API's with i/o timeout error
2024-03-29 16:00:22 +05:30
Yunus M
6eced60bf5 feat: update time range selection flows to handle relative and absolu… (#4742)
* feat: update time range selection flows to handle relative and absolute times

* fix: lint error

* fix: lint error

* feat: update logic to handle custom relative times on load and standardize relative time formats

* fix: type issue

* fix: handle light mode and on custom time range select

* chore: update alert frequency corresponding times

* chore: update copy URL

* feat: update styles
2024-03-29 14:53:48 +05:30
Rajat Dabade
7c2f5352d2 [Refactor]: Table Grid Formula issue. (#4758)
* refactor: change the logic to match data from another query

* refactor: updated logic

* refactor: clean up

* refactor: updated case to handle formula

* chore: nit

* refactor: isEqual instead of nested loops

* chore: added comments

* refactor: updated logic

* refactor: clean up

* refactor: updated case to handle formula

* chore: nit

* refactor: isEqual instead of nested loops
2024-03-29 14:41:16 +05:30
Vikrant Gupta
e6e377beff fix: billing graph page crash (#4764) 2024-03-29 11:08:33 +05:30
Prashant Shahi
6da9de6591 Merge pull request #4588 from SigNoz/chore/send-language-service-as-list
chore: send language and service name events as list
2024-03-28 22:21:55 +05:45
Vishal Sharma
7549aee656 Merge branch 'develop' into chore/send-language-service-as-list 2024-03-28 21:54:16 +05:30
Vishal Sharma
da4a6266c5 feat: add events API (#4761) 2024-03-28 21:43:41 +05:30
Vishal Sharma
6ac938f2a6 Merge branch 'develop' into chore/send-language-service-as-list 2024-03-28 21:40:57 +05:30
Raj Kamal Singh
990fc83269 Feat/integrations v0 mongo and nginx (#4763)
* feat: flesh out pre-requisites for collecting mongodb logs and metrics

* chore: remove stale pipelines in bundled integrations

* chore: clean up 'collect metrics' step for mongodb

* feat: add instructions for collecting and parsing mongodb logs

* feat: add metrics and logs attributes to mongodb data collected list

* feat: nginx logs collection instructions and some other cleanup

* feat: add list of parsed log attributes to data collected list for nginx

* chore: do not run pipeline population integration test if no built-in integration has a pipeline
2024-03-28 19:57:07 +05:30
Yunus M
5d5ff47d5e fix: update devtool property to eval-source-map (#4760) 2024-03-28 16:58:35 +05:30
Yunus M
9f30bba9a8 feat: add support to pin attributes in logs details view (#4692)
* feat: add support to pin attributes in logs details view

* feat: add safety checks

* feat: update styles

* feat: update styles

* feat: move json parsing in try catch block
2024-03-28 16:55:59 +05:30
Yunus M
6014bb76b6 feat: support drag select in chart - alerts page (#4618)
* feat: support drag select in chart - alerts page

* feat: handle back navigation after drag select
2024-03-28 16:51:29 +05:30
Vikrant Gupta
e25b54f86a fix: 404 resource not found issues (#4757) 2024-03-28 16:46:16 +05:30
Vikrant Gupta
5959963b9d fix: [SIG-575]: no data in new trace explorer page specific scenario (#4748)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2024-03-28 16:34:09 +05:30
Vishal Sharma
5c2a9e8362 Merge branch 'develop' into chore/send-language-service-as-list 2024-02-29 15:58:24 +05:30
makeavish
aad840da59 chore: send language and service name events as list 2024-02-23 14:08:17 +05:30
82 changed files with 1784 additions and 540 deletions

View File

@@ -445,7 +445,7 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
data["tracesUsed"] = signozTracesUsed
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail, true, false)
}
}
return data, true
@@ -488,7 +488,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
if _, ok := telemetry.EnabledPaths()[path]; ok {
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail, true, false)
}
}

View File

@@ -204,7 +204,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
zap.L().Error("License validation completed with error", zap.Error(reterr))
atomic.AddUint64(&lm.failedAttempts, 1)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
map[string]interface{}{"err": reterr.Error()}, "")
map[string]interface{}{"err": reterr.Error()}, "", true, false)
} else {
zap.L().Info("License validation completed with no errors")
}
@@ -263,7 +263,7 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail)
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false)
}
}
}()

View File

@@ -30,7 +30,8 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
statusCode,
payload: null,
error: errorMessage,
message: null,
message: (response.data as any)?.status,
body: JSON.stringify((response.data as any).data),
};
}

View File

@@ -8,7 +8,7 @@ const listAllDomain = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(`orgs/${props.orgId}/domains`);
const response = await axios.get(`/orgs/${props.orgId}/domains`);
return {
statusCode: 200,

View File

@@ -24,7 +24,7 @@ export const getAggregateAttribute = async ({
const response: AxiosResponse<{
data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get(
`autocomplete/aggregate_attributes?${createQueryParams({
`/autocomplete/aggregate_attributes?${createQueryParams({
aggregateOperator,
searchText,
dataSource,

View File

@@ -25,7 +25,7 @@ export const getAggregateKeys = async ({
const response: AxiosResponse<{
data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get(
`autocomplete/attribute_keys?${createQueryParams({
`/autocomplete/attribute_keys?${createQueryParams({
aggregateOperator,
searchText,
dataSource,

View File

@@ -2,4 +2,4 @@ import axios from 'api';
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> =>
axios.delete(`explorer/views/${uuid}`);
axios.delete(`/explorer/views/${uuid}`);

View File

@@ -6,4 +6,4 @@ import { DataSource } from 'types/common/queryBuilder';
export const getAllViews = (
sourcepage: DataSource,
): Promise<AxiosResponse<AllViewsProps>> =>
axios.get(`explorer/views?sourcePage=${sourcepage}`);
axios.get(`/explorer/views?sourcePage=${sourcepage}`);

View File

@@ -8,7 +8,7 @@ export const saveView = ({
viewName,
extraData,
}: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> =>
axios.post('explorer/views', {
axios.post('/explorer/views', {
name: viewName,
sourcePage,
compositeQuery,

View File

@@ -11,7 +11,7 @@ export const updateView = ({
sourcePage,
viewKey,
}: UpdateViewProps): Promise<UpdateViewPayloadProps> =>
axios.put(`explorer/views/${viewKey}`, {
axios.put(`/explorer/views/${viewKey}`, {
name: viewName,
compositeQuery,
extraData,

View File

@@ -5,13 +5,14 @@ import './CustomTimePicker.styles.scss';
import { Input, Popover, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { Options } from 'container/TopNav/DateTimeSelection/config';
import {
FixedDurationSuggestionOptions,
Options,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { defaultTo, noop } from 'lodash-es';
import { isValidTimeFormat } from 'lib/getMinMax';
import { defaultTo, isFunction, noop } from 'lodash-es';
import debounce from 'lodash-es/debounce';
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
import {
@@ -33,7 +34,14 @@ interface CustomTimePickerProps {
onError: (value: boolean) => void;
selectedValue: string;
selectedTime: string;
onValidCustomDateChange: ([t1, t2]: any[]) => void;
onValidCustomDateChange: ({
time: [t1, t2],
timeStr,
}: {
time: [dayjs.Dayjs | null, dayjs.Dayjs | null];
timeStr: string;
}) => void;
onCustomTimeStatusUpdate?: (isValid: boolean) => void;
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
items: any[];
@@ -53,6 +61,7 @@ function CustomTimePicker({
open,
setOpen,
onValidCustomDateChange,
onCustomTimeStatusUpdate,
newPopover,
customDateTimeVisible,
setCustomDTPickerVisible,
@@ -85,6 +94,7 @@ function CustomTimePicker({
return Options[index].label;
}
}
for (
let index = 0;
index < RelativeDurationSuggestionOptions.length;
@@ -94,12 +104,17 @@ function CustomTimePicker({
return RelativeDurationSuggestionOptions[index].label;
}
}
for (let index = 0; index < FixedDurationSuggestionOptions.length; index++) {
if (FixedDurationSuggestionOptions[index].value === selectedTime) {
return FixedDurationSuggestionOptions[index].label;
}
}
if (isValidTimeFormat(selectedTime)) {
return selectedTime;
}
return '';
};
@@ -161,13 +176,22 @@ function CustomTimePicker({
setInputStatus('error');
onError(true);
setInputErrorMessage('Please enter time less than 6 months');
if (isFunction(onCustomTimeStatusUpdate)) {
onCustomTimeStatusUpdate(true);
}
} else {
onValidCustomDateChange([minTime, currentTime]);
onValidCustomDateChange({
time: [minTime, currentTime],
timeStr: inputValue,
});
}
} else {
setInputStatus('error');
onError(true);
setInputErrorMessage(null);
if (isFunction(onCustomTimeStatusUpdate)) {
onCustomTimeStatusUpdate(false);
}
}
}, 300);
@@ -320,4 +344,5 @@ CustomTimePicker.defaultProps = {
setCustomDTPickerVisible: noop,
onCustomDateHandler: noop,
handleGoLive: noop,
onCustomTimeStatusUpdate: noop,
};

View File

@@ -17,4 +17,5 @@ export enum LOCALSTORAGE {
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
}

View File

@@ -29,4 +29,5 @@ export enum QueryParams {
expandedWidgetId = 'expandedWidgetId',
integration = 'integration',
pagination = 'pagination',
relativeTime = 'relativeTime',
}

View File

@@ -46,7 +46,7 @@ const calculateStartEndTime = (
): { startTime: number; endTime: number } => {
const timestamps: number[] = [];
data?.details?.breakdown?.forEach((breakdown: any) => {
breakdown?.dayWiseBreakdown?.breakdown.forEach((entry: any) => {
breakdown?.dayWiseBreakdown?.breakdown?.forEach((entry: any) => {
timestamps.push(entry?.timestamp);
});
});

View File

@@ -1,20 +1,30 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { AlertDef } from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -32,7 +42,7 @@ export interface ChartPreviewProps {
query: Query | null;
graphType?: PANEL_TYPES;
selectedTime?: timePreferenceType;
selectedInterval?: Time | TimeV2;
selectedInterval?: Time | TimeV2 | CustomTimeType;
headline?: JSX.Element;
alertDef?: AlertDef;
userQueryKey?: string;
@@ -46,7 +56,7 @@ function ChartPreview({
query,
graphType = PANEL_TYPES.TIME_SERIES,
selectedTime = 'GLOBAL_TIME',
selectedInterval = '5min',
selectedInterval = '5m',
headline,
userQueryKey,
allowSelectedIntervalForStepGen = false,
@@ -54,6 +64,7 @@ function ChartPreview({
yAxisUnit,
}: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const dispatch = useDispatch();
const threshold = alertDef?.condition.target || 0;
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
@@ -63,6 +74,30 @@ function ChartPreview({
GlobalReducer
>((state) => state.globalTime);
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime);
if (startTime && endTime && startTime !== endTime) {
dispatch(
UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10),
parseInt(getTimeString(endTime), 10),
]),
);
}
};
useEffect(() => {
window.addEventListener('popstate', handleBackNavigation);
return (): void => {
window.removeEventListener('popstate', handleBackNavigation);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const canQuery = useMemo((): boolean => {
if (!query || query == null) {
return false;
@@ -131,10 +166,34 @@ function ChartPreview({
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();
const urlQuery = useUrlQuery();
const location = useLocation();
const optionName =
getFormatNameByOptionId(alertDef?.condition.targetUnit || '') || '';
const onDragSelect = useCallback(
(start: number, end: number): void => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
const { maxTime, minTime } = GetMinMax('custom', [
startTimestamp,
endTimestamp,
]);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
);
const options = useMemo(
() =>
getUPlotChartOptions({
@@ -145,6 +204,7 @@ function ChartPreview({
minTimeScale,
maxTimeScale,
isDarkMode,
onDragSelect,
thresholds: [
{
index: '0', // no impact
@@ -174,6 +234,7 @@ function ChartPreview({
minTimeScale,
maxTimeScale,
isDarkMode,
onDragSelect,
threshold,
t,
optionName,

View File

@@ -12,22 +12,30 @@ import {
// toChartInterval converts eval window to chart selection time interval
export const toChartInterval = (evalWindow: string | undefined): Time => {
switch (evalWindow) {
case '1m0s':
return '1m';
case '5m0s':
return '5min';
return '5m';
case '10m0s':
return '10min';
return '10m';
case '15m0s':
return '15min';
return '15m';
case '30m0s':
return '30min';
return '30m';
case '1h0m0s':
return '1hr';
return '1h';
case '3h0m0s':
return '3h';
case '4h0m0s':
return '4hr';
return '4h';
case '6h0m0s':
return '6h';
case '12h0m0s':
return '12h';
case '24h0m0s':
return '1day';
return '1d';
default:
return '5min';
return '5m';
}
};

View File

@@ -1,6 +1,7 @@
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -81,8 +82,13 @@ function GridCardGraph({
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime);
const relativeTime = searchParams.get(
QueryParams.relativeTime,
) as CustomTimeType;
if (startTime && endTime && startTime !== endTime) {
if (relativeTime) {
dispatch(UpdateTimeInterval(relativeTime));
} else if (startTime && endTime && startTime !== endTime) {
dispatch(
UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10),
@@ -146,6 +152,16 @@ function GridCardGraph({
widget?.panelTypes,
widget.timePreferance,
],
retry(failureCount, error): boolean {
if (
String(error).includes('status: error') &&
String(error).includes('i/o timeout')
) {
return false;
}
return failureCount < 2;
},
keepPreviousData: true,
enabled: queryEnabledCondition,
refetchOnMount: false,

View File

@@ -53,6 +53,19 @@
background: rgba(171, 189, 255, 0.04);
padding: 8px;
.ant-collapse-extra {
display: flex;
align-items: center;
.action-btn {
display: flex;
.ant-btn {
background: rgba(113, 144, 249, 0.08);
}
}
}
}
.ant-collapse-content {

View File

@@ -5,12 +5,13 @@
.ant-table-row:hover {
.ant-table-cell {
.value-field {
display: flex;
justify-content: space-between;
align-items: center;
.action-btn {
display: flex;
gap: 4px;
position: absolute;
top: 50%;
right: 16px;
transform: translateY(-50%);
gap: 8px;
}
}
}
@@ -28,6 +29,30 @@
}
}
.attribute-pin {
cursor: pointer;
padding: 0;
vertical-align: middle;
text-align: center;
.log-attribute-pin {
padding: 8px;
display: flex;
justify-content: center;
align-items: center;
.pin-attribute-icon {
border: none;
&.pinned svg {
fill: var(--bg-robin-500);
}
}
}
}
.value-field-container {
background: rgba(22, 25, 34, 0.4);
@@ -70,6 +95,10 @@
.value-field-container {
background: var(--bg-vanilla-300);
&.attribute-pin {
background: var(--bg-vanilla-100);
}
.action-btn {
.filter-btn {
background: var(--bg-vanilla-300);

View File

@@ -1,22 +1,29 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './TableView.styles.scss';
import { LinkOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Space, Spin, Tooltip, Tree, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import cx from 'classnames';
import AddToQueryHOC, {
AddToQueryHOCProps,
} from 'components/Logs/AddToQueryHOC';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { ResizeTable } from 'components/ResizeTable';
import { LOCALSTORAGE } from 'constants/localStorage';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
import { isEmpty } from 'lodash-es';
import { ArrowDownToDot, ArrowUpFromDot } from 'lucide-react';
import { useMemo, useState } from 'react';
import { ArrowDownToDot, ArrowUpFromDot, Pin } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { Dispatch } from 'redux';
@@ -57,6 +64,28 @@ function TableView({
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
const [isfilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
const [pinnedAttributes, setPinnedAttributes] = useState<
Record<string, boolean>
>({});
useEffect(() => {
const pinnedAttributesFromLocalStorage = getLocalStorageApi(
LOCALSTORAGE.PINNED_ATTRIBUTES,
);
if (pinnedAttributesFromLocalStorage) {
try {
const parsedPinnedAttributes = JSON.parse(pinnedAttributesFromLocalStorage);
setPinnedAttributes(parsedPinnedAttributes);
} catch (e) {
console.error('Error parsing pinned attributes from local storgage');
}
} else {
setPinnedAttributes({});
}
}, []);
const flattenLogData: Record<string, string> | null = useMemo(
() => (logData ? flattenObject(logData) : null),
@@ -74,6 +103,19 @@ function TableView({
}
};
const togglePinAttribute = (record: DataType): void => {
if (record) {
const newPinnedAttributes = { ...pinnedAttributes };
newPinnedAttributes[record.key] = !newPinnedAttributes[record.key];
setPinnedAttributes(newPinnedAttributes);
setLocalStorageApi(
LOCALSTORAGE.PINNED_ATTRIBUTES,
JSON.stringify(newPinnedAttributes),
);
}
};
const onClickHandler = (
operator: string,
fieldKey: string,
@@ -138,6 +180,37 @@ function TableView({
}
const columns: ColumnsType<DataType> = [
{
title: '',
dataIndex: 'pin',
key: 'pin',
width: 5,
align: 'left',
className: 'attribute-pin value-field-container',
render: (fieldData: Record<string, string>, record): JSX.Element => {
let pinColor = isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500;
if (pinnedAttributes[record?.key]) {
pinColor = Color.BG_ROBIN_500;
}
return (
<div className="log-attribute-pin value-field">
<div
className={cx(
'pin-attribute-icon',
pinnedAttributes[record?.key] ? 'pinned' : '',
)}
onClick={(): void => {
togglePinAttribute(record);
}}
>
<Pin size={14} color={pinColor} />
</div>
</div>
);
},
},
{
title: 'Field',
dataIndex: 'field',
@@ -264,12 +337,34 @@ function TableView({
},
},
];
function sortPinnedAttributes(
data: Record<string, string>[],
sortingObj: Record<string, boolean>,
): Record<string, string>[] {
const sortingKeys = Object.keys(sortingObj);
return data.sort((a, b) => {
const aKey = a.key;
const bKey = b.key;
const aSortIndex = sortingKeys.indexOf(aKey);
const bSortIndex = sortingKeys.indexOf(bKey);
if (sortingObj[aKey] && !sortingObj[bKey]) {
return -1;
}
if (!sortingObj[aKey] && sortingObj[bKey]) {
return 1;
}
return aSortIndex - bSortIndex;
});
}
const sortedAttributes = sortPinnedAttributes(dataSource, pinnedAttributes);
return (
<ResizeTable
columns={columns}
tableLayout="fixed"
dataSource={dataSource}
dataSource={sortedAttributes}
pagination={false}
showHeader={false}
className="attribute-table-container"

View File

@@ -2,6 +2,7 @@ import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query';
import { themeColors } from 'constants/theme';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import useUrlQuery from 'hooks/useUrlQuery';
import getChartData, { GetChartDataProps } from 'lib/getChartData';
import GetMinMax from 'lib/getMinMax';
@@ -65,8 +66,13 @@ function LogsExplorerChart({
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime);
const relativeTime = searchParams.get(
QueryParams.relativeTime,
) as CustomTimeType;
if (startTime && endTime && startTime !== endTime) {
if (relativeTime) {
dispatch(UpdateTimeInterval(relativeTime));
} else if (startTime && endTime && startTime !== endTime) {
dispatch(
UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10),

View File

@@ -1,9 +1,12 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { GetMinMaxPayload } from 'lib/getMinMax';
export const getGlobalTime = (
selectedTime: Time | TimeV2,
selectedTime: Time | TimeV2 | CustomTimeType,
globalTime: GetMinMaxPayload,
): GetMinMaxPayload | undefined => {
if (selectedTime === 'custom') {

View File

@@ -3,6 +3,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -97,8 +98,13 @@ function WidgetGraph({
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime);
const relativeTime = searchParams.get(
QueryParams.relativeTime,
) as CustomTimeType;
if (startTime && endTime && startTime !== endTime) {
if (relativeTime) {
dispatch(UpdateTimeInterval(relativeTime));
} else if (startTime && endTime && startTime !== endTime) {
dispatch(
UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10),

View File

@@ -1,3 +1,5 @@
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { OptionsQuery } from './types';
export const URL_OPTIONS = 'options';
@@ -7,3 +9,46 @@ export const defaultOptionsQuery: OptionsQuery = {
maxLines: 2,
format: 'list',
};
export const defaultTraceSelectedColumns = [
{
key: 'serviceName',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
},
{
key: 'name',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
},
{
key: 'durationNano',
dataType: DataTypes.Float64,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
},
{
key: 'httpMethod',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
},
{
key: 'responseStatusCode',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
},
];

View File

@@ -16,7 +16,11 @@ import {
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { defaultOptionsQuery, URL_OPTIONS } from './constants';
import {
defaultOptionsQuery,
defaultTraceSelectedColumns,
URL_OPTIONS,
} from './constants';
import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types';
import { getOptionsFromKeys } from './utils';
@@ -124,20 +128,29 @@ const useOptionsMenu = ({
{ queryKey: [debouncedSearchText, isFocused], enabled: isFocused },
);
const searchedAttributeKeys = useMemo(
() => searchedAttributesData?.payload?.attributeKeys || [],
[searchedAttributesData?.payload?.attributeKeys],
);
const searchedAttributeKeys = useMemo(() => {
if (searchedAttributesData?.payload?.attributeKeys?.length) {
return searchedAttributesData.payload.attributeKeys;
}
if (dataSource === DataSource.TRACES) {
return defaultTraceSelectedColumns;
}
return [];
}, [dataSource, searchedAttributesData?.payload?.attributeKeys]);
const initialOptionsQuery: OptionsQuery = useMemo(
() => ({
...defaultOptionsQuery,
...initialOptions,
// eslint-disable-next-line no-nested-ternary
selectColumns: initialOptions?.selectColumns
? initialSelectedColumns
: dataSource === DataSource.TRACES
? defaultTraceSelectedColumns
: defaultOptionsQuery.selectColumns,
}),
[initialOptions, initialSelectedColumns],
[dataSource, initialOptions, initialSelectedColumns],
);
const selectedColumnKeys = useMemo(

View File

@@ -8,6 +8,10 @@
.resource-attributes-selector {
flex: 1;
border-radius: 3px;
background-color: var(--bg-ink-400);
border: 1px solid #454c58;
}
.environment-selector {
@@ -18,3 +22,12 @@
margin-bottom: 0;
}
}
.lightMode {
.resourceAttributesFilter-container {
.resource-attributes-selector {
border: 1px solid #d9d9d9;
background: var(--bg-vanilla-100);
}
}
}

View File

@@ -8,7 +8,6 @@ export const SearchContainer = styled.div`
align-items: center;
gap: 0.2rem;
padding: 0 0.2rem;
border: 1px solid #454c58;
box-sizing: border-box;
border-radius: 3px;
`;

View File

@@ -1,6 +1,9 @@
import { ServiceDataProps } from 'api/metrics/getTopLevelOperations';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
@@ -25,7 +28,7 @@ export interface GetQueryRangeRequestDataProps {
topLevelOperations: [keyof ServiceDataProps, string[]][];
maxTime: number;
minTime: number;
globalSelectedInterval: Time | TimeV2;
globalSelectedInterval: Time | TimeV2 | CustomTimeType;
}
export interface GetServiceListFromQueryProps {

View File

@@ -1,7 +1,7 @@
import GetMinMax, { GetMinMaxPayload } from 'lib/getMinMax';
import { Time } from '../DateTimeSelection/config';
import { Time as TimeV2 } from '../DateTimeSelectionV2/config';
import { CustomTimeType, Time as TimeV2 } from '../DateTimeSelectionV2/config';
export const options: IOptions[] = [
{
@@ -68,7 +68,7 @@ export interface IOptions {
}
export const getMinMax = (
selectedTime: Time | TimeV2,
selectedTime: Time | TimeV2 | CustomTimeType,
minTime: number,
maxTime: number,
): GetMinMaxPayload =>

View File

@@ -1,7 +1,7 @@
import GetMinMax, { GetMinMaxPayload } from 'lib/getMinMax';
import { Time } from '../DateTimeSelection/config';
import { Time as TimeV2 } from '../DateTimeSelectionV2/config';
import { CustomTimeType, Time as TimeV2 } from '../DateTimeSelectionV2/config';
export const options: IOptions[] = [
{
@@ -68,7 +68,7 @@ export interface IOptions {
}
export const getMinMax = (
selectedTime: Time | TimeV2,
selectedTime: Time | TimeV2 | CustomTimeType,
minTime: number,
maxTime: number,
): GetMinMaxPayload =>

View File

@@ -1,16 +1,18 @@
import ROUTES from 'constants/routes';
type FiveMin = '5min';
type TenMin = '10min';
type FifteenMin = '15min';
type ThirtyMin = '30min';
type OneMin = '1min';
type SixHour = '6hr';
type OneHour = '1hr';
type FourHour = '4hr';
type OneDay = '1day';
type ThreeDay = '3days';
type OneWeek = '1week';
type FiveMin = '5m';
type TenMin = '10m';
type FifteenMin = '15m';
type ThirtyMin = '30m';
type OneMin = '1m';
type SixHour = '6h';
type OneHour = '1h';
type FourHour = '4h';
type ThreeHour = '3h';
type TwelveHour = '12h';
type OneDay = '1d';
type ThreeDay = '3d';
type OneWeek = '1w';
type Custom = 'custom';
export type Time =
@@ -22,37 +24,62 @@ export type Time =
| FourHour
| SixHour
| OneHour
| ThreeHour
| Custom
| OneWeek
| OneDay
| TwelveHour
| ThreeDay;
export const Options: Option[] = [
{ value: '5min', label: 'Last 5 min' },
{ value: '15min', label: 'Last 15 min' },
{ value: '30min', label: 'Last 30 min' },
{ value: '1hr', label: 'Last 1 hour' },
{ value: '6hr', label: 'Last 6 hour' },
{ value: '1day', label: 'Last 1 day' },
{ value: '3days', label: 'Last 3 days' },
{ value: '1week', label: 'Last 1 week' },
{ value: '5m', label: 'Last 5 min' },
{ value: '15m', label: 'Last 15 min' },
{ value: '30m', label: 'Last 30 min' },
{ value: '1h', label: 'Last 1 hour' },
{ value: '6h', label: 'Last 6 hour' },
{ value: '1d', label: 'Last 1 day' },
{ value: '3d', label: 'Last 3 days' },
{ value: '1w', label: 'Last 1 week' },
{ value: 'custom', label: 'Custom' },
];
type TimeFrame = {
'5min': string;
'15min': string;
'30min': string;
'1hr': string;
'6hr': string;
'1day': string;
'3days': string;
'1week': string;
[key: string]: string; // Index signature to allow any string as index
};
export const RelativeTimeMap: TimeFrame = {
'5min': '5m',
'15min': '15m',
'30min': '30m',
'1hr': '1h',
'6hr': '6h',
'1day': '1d',
'3days': '3d',
'1week': '1w',
};
export interface Option {
value: Time;
label: string;
}
export const RelativeDurationOptions: Option[] = [
{ value: '5min', label: 'Last 5 min' },
{ value: '15min', label: 'Last 15 min' },
{ value: '30min', label: 'Last 30 min' },
{ value: '1hr', label: 'Last 1 hour' },
{ value: '6hr', label: 'Last 6 hour' },
{ value: '1day', label: 'Last 1 day' },
{ value: '3days', label: 'Last 3 days' },
{ value: '1week', label: 'Last 1 week' },
{ value: '5m', label: 'Last 5 min' },
{ value: '15m', label: 'Last 15 min' },
{ value: '30m', label: 'Last 30 min' },
{ value: '1h', label: 'Last 1 hour' },
{ value: '6h', label: 'Last 6 hour' },
{ value: '1d', label: 'Last 1 day' },
{ value: '3d', label: 'Last 3 days' },
{ value: '1w', label: 'Last 1 week' },
];
export const getDefaultOption = (route: string): Time => {

View File

@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import AutoRefresh from '../AutoRefresh';
import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal';
import { Time as TimeV2 } from '../DateTimeSelectionV2/config';
import { CustomTimeType, Time as TimeV2 } from '../DateTimeSelectionV2/config';
import {
getDefaultOption,
getOptions,
@@ -122,7 +122,7 @@ function DateTimeSelection({
const getInputLabel = (
startTime?: Dayjs,
endTime?: Dayjs,
timeInterval: Time | TimeV2 = '15min',
timeInterval: Time | TimeV2 | CustomTimeType = '15m',
): string | Time => {
if (startTime && endTime && timeInterval === 'custom') {
const format = 'YYYY/MM/DD HH:mm';
@@ -225,7 +225,7 @@ function DateTimeSelection({
[location.pathname],
);
const onSelectHandler = (value: Time | TimeV2): void => {
const onSelectHandler = (value: Time | TimeV2 | CustomTimeType): void => {
if (value !== 'custom') {
updateTimeInterval(value);
updateLocalStorageForRoutes(value);
@@ -358,7 +358,7 @@ function DateTimeSelection({
}}
selectedTime={selectedTime}
onValidCustomDateChange={(dateTime): void =>
onCustomDateHandler(dateTime as DateTimeRangeType)
onCustomDateHandler(dateTime.time as DateTimeRangeType)
}
selectedValue={getInputLabel(
dayjs(minTime / 1000000),
@@ -406,7 +406,7 @@ function DateTimeSelection({
interface DispatchProps {
updateTimeInterval: (
interval: Time | TimeV2,
interval: Time | TimeV2 | CustomTimeType,
dateTimeRange?: [number, number],
) => (dispatch: Dispatch<AppActions>) => void;
globalTimeLoading: () => void;

View File

@@ -54,9 +54,61 @@
}
}
}
.share-link-btn {
height: 34px;
}
.shareable-link-popover {
margin-left: 8px;
}
}
.date-time-root {
.share-modal-content {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
width: 420px;
.absolute-relative-time-toggler-container {
display: flex;
gap: 8px;
align-items: center;
}
.absolute-relative-time-toggler {
display: flex;
gap: 4px;
align-items: center;
}
.absolute-relative-time-error {
font-size: 12px;
color: var(--bg-amber-600);
}
.share-link {
display: flex;
align-items: center;
.share-url {
flex: 1;
border: 1px solid var(--bg-slate-400);
border-radius: 2px;
background: var(--bg-ink-300);
height: 32px;
padding: 6px 8px;
}
.copy-url-btn {
width: 32px;
}
}
}
.date-time-root,
.shareable-link-popover-root {
.ant-popover-inner {
border-radius: 4px !important;
border: 1px solid var(--bg-slate-400);
@@ -185,7 +237,8 @@
}
}
.date-time-root {
.date-time-root,
.shareable-link-popover-root {
.ant-popover-inner {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100) !important;
@@ -234,4 +287,13 @@
}
}
}
.share-modal-content {
.share-link {
.share-url {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
}

View File

@@ -1,24 +1,24 @@
/* eslint-disable sonarjs/no-duplicate-string */
import ROUTES from 'constants/routes';
type FiveMin = '5min';
type TenMin = '10min';
type FifteenMin = '15min';
type ThirtyMin = '30min';
type FortyFiveMin = '45min';
type OneMin = '1min';
type ThreeHour = '3hr';
type SixHour = '6hr';
type OneHour = '1hr';
type FourHour = '4hr';
type TwelveHour = '12hr';
type OneDay = '1day';
type ThreeDay = '3days';
type FourDay = '4days';
type TenDay = '10days';
type OneWeek = '1week';
type TwoWeek = '2weeks';
type SixWeek = '6weeks';
type FiveMin = '5m';
type TenMin = '10m';
type FifteenMin = '15m';
type ThirtyMin = '30m';
type FortyFiveMin = '45m';
type OneMin = '1m';
type ThreeHour = '3h';
type SixHour = '6h';
type OneHour = '1h';
type FourHour = '4h';
type TwelveHour = '12h';
type OneDay = '1d';
type ThreeDay = '3d';
type FourDay = '4d';
type TenDay = '10d';
type OneWeek = '1w';
type TwoWeek = '2w';
type SixWeek = '6w';
type TwoMonths = '2months';
type Custom = 'custom';
@@ -44,15 +44,19 @@ export type Time =
| TwoWeek
| TwoMonths;
export type TimeUnit = 'm' | 'h' | 'd' | 'w';
export type CustomTimeType = `${string}${TimeUnit}`;
export const Options: Option[] = [
{ value: '5min', label: 'Last 5 minutes' },
{ value: '15min', label: 'Last 15 minutes' },
{ value: '30min', label: 'Last 30 minutes' },
{ value: '1hr', label: 'Last 1 hour' },
{ value: '6hr', label: 'Last 6 hours' },
{ value: '1day', label: 'Last 1 day' },
{ value: '3days', label: 'Last 3 days' },
{ value: '1week', label: 'Last 1 week' },
{ value: '5m', label: 'Last 5 minutes' },
{ value: '15m', label: 'Last 15 minutes' },
{ value: '30m', label: 'Last 30 minutes' },
{ value: '1h', label: 'Last 1 hour' },
{ value: '6h', label: 'Last 6 hours' },
{ value: '1d', label: 'Last 1 day' },
{ value: '3d', label: 'Last 3 days' },
{ value: '1w', label: 'Last 1 week' },
{ value: 'custom', label: 'Custom' },
];
@@ -61,36 +65,92 @@ export interface Option {
label: string;
}
export const OLD_RELATIVE_TIME_VALUES = [
'1min',
'10min',
'15min',
'1hr',
'30min',
'45min',
'5min',
'1day',
'3days',
'4days',
'10days',
'1week',
'2weeks',
'6weeks',
'3hr',
'4hr',
'6hr',
'12hr',
];
export const RelativeDurationOptions: Option[] = [
{ value: '5min', label: 'Last 5 minutes' },
{ value: '15min', label: 'Last 15 minutes' },
{ value: '30min', label: 'Last 30 minutes' },
{ value: '1hr', label: 'Last 1 hour' },
{ value: '6hr', label: 'Last 6 hour' },
{ value: '1day', label: 'Last 1 day' },
{ value: '3days', label: 'Last 3 days' },
{ value: '1week', label: 'Last 1 week' },
{ value: '5m', label: 'Last 5 minutes' },
{ value: '15m', label: 'Last 15 minutes' },
{ value: '30m', label: 'Last 30 minutes' },
{ value: '1h', label: 'Last 1 hour' },
{ value: '6h', label: 'Last 6 hour' },
{ value: '1d', label: 'Last 1 day' },
{ value: '3d', label: 'Last 3 days' },
{ value: '1w', label: 'Last 1 week' },
];
export const RelativeDurationSuggestionOptions: Option[] = [
{ value: '3hr', label: '3h' },
{ value: '4days', label: '4d' },
{ value: '6weeks', label: '6w' },
{ value: '12hr', label: '12 hours' },
{ value: '10days', label: '10d' },
{ value: '2weeks', label: '2 weeks' },
{ value: '3h', label: 'Last 3 hours' },
{ value: '4d', label: 'Last 4 days' },
{ value: '6w', label: 'Last 6 weeks' },
{ value: '12h', label: 'Last 12 hours' },
{ value: '10d', label: 'Last 10 days' },
{ value: '2w', label: 'Last 2 weeks' },
{ value: '2months', label: 'Last 2 months' },
{ value: '1day', label: 'today' },
{ value: '1d', label: 'today' },
];
export const FixedDurationSuggestionOptions: Option[] = [
{ value: '45min', label: '45m' },
{ value: '12hr', label: '12 hours' },
{ value: '10days', label: '10d' },
{ value: '2weeks', label: '2 weeks' },
{ value: '45m', label: 'Last 45 mins' },
{ value: '12h', label: 'Last 12 hours' },
{ value: '10d', label: 'Last 10 days' },
{ value: '2w', label: 'Last 2 weeks' },
{ value: '2months', label: 'Last 2 months' },
{ value: '1day', label: 'today' },
{ value: '1d', label: 'today' },
];
export const convertOldTimeToNewValidCustomTimeFormat = (
time: string,
): CustomTimeType => {
const regex = /^(\d+)([a-zA-Z]+)/;
const match = regex.exec(time);
if (match) {
let unit = 'm';
switch (match[2]) {
case 'min':
unit = 'm';
break;
case 'hr':
unit = 'h';
break;
case 'day':
case 'days':
unit = 'd';
break;
case 'week':
case 'weeks':
unit = 'w';
break;
default:
break;
}
return `${match[1]}${unit}` as CustomTimeType;
}
return '30m';
};
export const getDefaultOption = (route: string): Time => {
if (route === ROUTES.SERVICE_MAP) {
return RelativeDurationOptions[2].value;

View File

@@ -1,7 +1,8 @@
import './DateTimeSelectionV2.styles.scss';
import { SyncOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { Color } from '@signozhq/design-tokens';
import { Button, Popover, Switch, Typography } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import CustomTimePicker from 'components/CustomTimePicker/CustomTimePicker';
@@ -26,10 +27,12 @@ import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { isObject } from 'lodash-es';
import { Check, Copy, Info, Send } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { connect, useSelector } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions';
@@ -42,9 +45,12 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import AutoRefresh from '../AutoRefreshV2';
import { DateTimeRangeType } from '../CustomDateTimeModal';
import {
convertOldTimeToNewValidCustomTimeFormat,
CustomTimeType,
getDefaultOption,
getOptions,
LocalStorageTimeRange,
OLD_RELATIVE_TIME_VALUES,
Time,
TimeRange,
} from './config';
@@ -66,6 +72,10 @@ function DateTimeSelection({
const searchStartTime = urlQuery.get('startTime');
const searchEndTime = urlQuery.get('endTime');
const queryClient = useQueryClient();
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(false);
const [isValidteRelativeTime, setIsValidteRelativeTime] = useState(false);
const [, handleCopyToClipboard] = useCopyToClipboard();
const [isURLCopied, setIsURLCopied] = useState(false);
const {
localstorageStartTime,
@@ -178,7 +188,7 @@ function DateTimeSelection({
const getInputLabel = (
startTime?: Dayjs,
endTime?: Dayjs,
timeInterval: Time = '15min',
timeInterval: Time | CustomTimeType = '15m',
): string | Time => {
if (startTime && endTime && timeInterval === 'custom') {
const format = 'DD/MM/YYYY HH:mm';
@@ -284,28 +294,38 @@ function DateTimeSelection({
[location.pathname],
);
const onSelectHandler = (value: Time): void => {
const onSelectHandler = (value: Time | CustomTimeType): void => {
if (value !== 'custom') {
setIsOpen(false);
updateTimeInterval(value);
updateLocalStorageForRoutes(value);
setIsValidteRelativeTime(true);
if (refreshButtonHidden) {
setRefreshButtonHidden(false);
}
} else {
setRefreshButtonHidden(true);
setCustomDTPickerVisible(true);
setIsValidteRelativeTime(false);
setEnableAbsoluteTime(false);
return;
}
const { maxTime, minTime } = GetMinMax(value, getTime());
if (!isLogsExplorerPage) {
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
urlQuery.delete('startTime');
urlQuery.delete('endTime');
urlQuery.set(QueryParams.relativeTime, value);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
// For logs explorer - time range handling is managed in useCopyLogLink.ts:52
if (!stagedQuery) {
return;
}
@@ -319,18 +339,22 @@ function DateTimeSelection({
};
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
// console.log('dateTimeRange', dateTimeRange);
if (dateTimeRange !== null) {
const [startTimeMoment, endTimeMoment] = dateTimeRange;
if (startTimeMoment && endTimeMoment) {
const startTime = startTimeMoment;
const endTime = endTimeMoment;
setCustomDTPickerVisible(false);
updateTimeInterval('custom', [
startTime.toDate().getTime(),
endTime.toDate().getTime(),
]);
setLocalStorageKey('startTime', startTime.toString());
setLocalStorageKey('endTime', endTime.toString());
updateLocalStorageForRoutes(JSON.stringify({ startTime, endTime }));
if (!isLogsExplorerPage) {
@@ -339,6 +363,7 @@ function DateTimeSelection({
startTime?.toDate().getTime().toString(),
);
urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString());
urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
@@ -346,6 +371,57 @@ function DateTimeSelection({
}
};
const onValidCustomDateHandler = (dateTimeStr: CustomTimeType): void => {
setIsOpen(false);
updateTimeInterval(dateTimeStr);
updateLocalStorageForRoutes(dateTimeStr);
urlQuery.delete('startTime');
urlQuery.delete('endTime');
setIsValidteRelativeTime(true);
const { maxTime, minTime } = GetMinMax(dateTimeStr, getTime());
if (!isLogsExplorerPage) {
urlQuery.delete('startTime');
urlQuery.delete('endTime');
urlQuery.set(QueryParams.relativeTime, dateTimeStr);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
if (!stagedQuery) {
return;
}
// the second boolean param directs the qb about the time change so to merge the query and retain the current state
initQueryBuilderData(updateStepInterval(stagedQuery, maxTime, minTime), true);
};
const getCustomOrIntervalTime = (
time: Time,
currentRoute: string,
): Time | CustomTimeType => {
if (searchEndTime !== null && searchStartTime !== null) {
return 'custom';
}
if (
(localstorageEndTime === null || localstorageStartTime === null) &&
time === 'custom'
) {
return getDefaultOption(currentRoute);
}
if (OLD_RELATIVE_TIME_VALUES.indexOf(time) > -1) {
return convertOldTimeToNewValidCustomTimeFormat(time);
}
return time;
};
// this is triggred when we change the routes and based on that we are changing the default options
useEffect(() => {
const metricsTimeDuration = getLocalStorageKey(
@@ -365,21 +441,9 @@ function DateTimeSelection({
const currentOptions = getOptions(currentRoute);
setOptions(currentOptions);
const getCustomOrIntervalTime = (time: Time): Time => {
if (searchEndTime !== null && searchStartTime !== null) {
return 'custom';
}
if (
(localstorageEndTime === null || localstorageStartTime === null) &&
time === 'custom'
) {
return getDefaultOption(currentRoute);
}
const updatedTime = getCustomOrIntervalTime(time, currentRoute);
return time;
};
const updatedTime = getCustomOrIntervalTime(time);
setIsValidteRelativeTime(updatedTime !== 'custom');
const [preStartTime = 0, preEndTime = 0] = getTime() || [];
@@ -388,18 +452,113 @@ function DateTimeSelection({
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
if (updatedTime !== 'custom') {
const { minTime, maxTime } = GetMinMax(updatedTime);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
urlQuery.delete('startTime');
urlQuery.delete('endTime');
urlQuery.set(QueryParams.relativeTime, updatedTime);
} else {
urlQuery.set(QueryParams.startTime, preStartTime.toString());
urlQuery.set(QueryParams.endTime, preEndTime.toString());
const startTime = preStartTime.toString();
const endTime = preEndTime.toString();
urlQuery.set(QueryParams.startTime, startTime);
urlQuery.set(QueryParams.endTime, endTime);
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname, updateTimeInterval, globalTimeLoading]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const shareModalContent = (): JSX.Element => {
let currentUrl = window.location.href;
const startTime = urlQuery.get(QueryParams.startTime);
const endTime = urlQuery.get(QueryParams.endTime);
const isCustomTime = !!(startTime && endTime && selectedTime === 'custom');
if (enableAbsoluteTime || isCustomTime) {
if (selectedTime === 'custom') {
if (searchStartTime && searchEndTime) {
urlQuery.set(QueryParams.startTime, searchStartTime.toString());
urlQuery.set(QueryParams.endTime, searchEndTime.toString());
}
} else {
const { minTime, maxTime } = GetMinMax(selectedTime);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
}
urlQuery.delete(QueryParams.relativeTime);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
} else {
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
urlQuery.set(QueryParams.relativeTime, selectedTime);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
}
return (
<div className="share-modal-content">
<div className="absolute-relative-time-toggler-container">
<div className="absolute-relative-time-toggler">
{(selectedTime === 'custom' || !isValidteRelativeTime) && (
<Info size={14} color={Color.BG_AMBER_600} />
)}
<Switch
checked={enableAbsoluteTime || isCustomTime}
disabled={selectedTime === 'custom' || !isValidteRelativeTime}
size="small"
onChange={(): void => {
setEnableAbsoluteTime(!enableAbsoluteTime);
}}
/>
</div>
<Typography.Text>Enable Absolute Time</Typography.Text>
</div>
{(selectedTime === 'custom' || !isValidteRelativeTime) && (
<div className="absolute-relative-time-error">
Please select / enter valid relative time to toggle.
</div>
)}
<div className="share-link">
<Typography.Text ellipsis className="share-url">
{currentUrl}
</Typography.Text>
<Button
className="periscope-btn copy-url-btn"
onClick={(): void => {
handleCopyToClipboard(currentUrl);
setIsURLCopied(true);
setTimeout(() => {
setIsURLCopied(false);
}, 1000);
}}
icon={
isURLCopied ? (
<Check size={14} color={Color.BG_FOREST_500} />
) : (
<Copy size={14} color={Color.BG_ROBIN_500} />
)
}
/>
</div>
</div>
);
};
return (
<div className="date-time-selector">
{!hasSelectedTimeError && !refreshButtonHidden && (
@@ -426,9 +585,12 @@ function DateTimeSelection({
setHasSelectedTimeError(hasError);
}}
selectedTime={selectedTime}
onValidCustomDateChange={(dateTime): void =>
onCustomDateHandler(dateTime as DateTimeRangeType)
}
onValidCustomDateChange={(dateTime): void => {
onValidCustomDateHandler(dateTime.timeStr as CustomTimeType);
}}
onCustomTimeStatusUpdate={(isValid: boolean): void => {
setIsValidteRelativeTime(isValid);
}}
selectedValue={getInputLabel(
dayjs(minTime / 1000000),
dayjs(maxTime / 1000000),
@@ -457,6 +619,22 @@ function DateTimeSelection({
</FormItem>
</div>
)}
<Popover
rootClassName="shareable-link-popover-root"
className="shareable-link-popover"
placement="bottomRight"
content={shareModalContent}
arrow={false}
trigger={['hover']}
>
<Button
className="share-link-btn periscope-btn"
icon={<Send size={14} />}
>
Share
</Button>
</Popover>
</FormContainer>
</Form>
</div>
@@ -468,7 +646,7 @@ interface DateTimeSelectionV2Props {
}
interface DispatchProps {
updateTimeInterval: (
interval: Time,
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => (dispatch: Dispatch<AppActions>) => void;
globalTimeLoading: () => void;

View File

@@ -11,8 +11,11 @@ import {
useMemo,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { HIGHLIGHTED_DELAY } from './configs';
import { LogTimeRange, UseCopyLogLink } from './types';
@@ -33,15 +36,30 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
null,
);
const { selectedTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const onTimeRangeChange = useCallback(
(newTimeRange: LogTimeRange | null): void => {
urlQuery.set(QueryParams.timeRange, JSON.stringify(newTimeRange));
urlQuery.set(QueryParams.startTime, newTimeRange?.start.toString() || '');
urlQuery.set(QueryParams.endTime, newTimeRange?.end.toString() || '');
if (selectedTime !== 'custom') {
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
urlQuery.set(QueryParams.relativeTime, selectedTime);
} else {
urlQuery.set(QueryParams.startTime, newTimeRange?.start.toString() || '');
urlQuery.set(QueryParams.endTime, newTimeRange?.end.toString() || '');
urlQuery.delete(QueryParams.relativeTime);
}
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
},
[pathname, urlQuery],
[pathname, urlQuery, selectedTime],
);
const isActiveLog = useMemo(() => activeLogId === logId, [activeLogId, logId]);

View File

@@ -1,7 +1,10 @@
import getService from 'api/metrics/getService';
import { AxiosError } from 'axios';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import {
QueryKey,
useQuery,
@@ -27,7 +30,7 @@ export const useQueryService = ({
interface UseQueryServiceProps {
minTime: number;
maxTime: number;
selectedTime: Time | TimeV2;
selectedTime: Time | TimeV2 | CustomTimeType;
selectedTags: Tags[];
options?: UseQueryOptions<PayloadProps, AxiosError, PayloadProps, QueryKey>;
}

View File

@@ -6,7 +6,10 @@ import { getMetricsQueryRange } from 'api/metrics/getQueryRange';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { Pagination } from 'hooks/queryPagination';
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
import { isEmpty } from 'lodash-es';
@@ -31,7 +34,7 @@ export async function GetMetricQueryRange(
if (response.statusCode >= 400) {
throw new Error(
`API responded with ${response.statusCode} - ${response.error}`,
`API responded with ${response.statusCode} - ${response.error} status: ${response.message}, errors: ${response?.body}`,
);
}
@@ -67,7 +70,7 @@ export interface GetQueryResultsProps {
query: Query;
graphType: PANEL_TYPES;
selectedTime: timePreferenceType;
globalSelectedInterval: Time | TimeV2;
globalSelectedInterval: Time | TimeV2 | CustomTimeType;
variables?: Record<string, unknown>;
params?: Record<string, unknown>;
tableParams?: {

View File

@@ -1,63 +1,101 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import { isString } from 'lodash-es';
import { GlobalReducer } from 'types/reducer/globalTime';
import getMinAgo from './getStartAndEndTime/getMinAgo';
const validCustomTimeRegex = /^(\d+)([mhdw])$/;
export const isValidTimeFormat = (time: string): boolean =>
validCustomTimeRegex.test(time);
const extractTimeAndUnit = (time: string): { time: number; unit: string } => {
// Match the pattern
const match = /^(\d+)([mhdw])$/.exec(time);
if (match) {
return { time: parseInt(match[1], 10), unit: match[2] };
}
return {
time: 30,
unit: 'm',
};
};
export const getMinTimeForRelativeTimes = (
time: number,
unit: string,
): number => {
switch (unit) {
case 'm':
return getMinAgo({ minutes: 1 * time }).getTime();
case 'h':
return getMinAgo({ minutes: 60 * time }).getTime();
case 'd':
return getMinAgo({ minutes: 24 * 60 * time }).getTime();
case 'w':
return getMinAgo({ minutes: 24 * 60 * 7 * time }).getTime();
default:
return getMinAgo({ minutes: 1 }).getTime();
}
};
const GetMinMax = (
interval: Time | TimeV2,
interval: Time | TimeV2 | string,
dateTimeRange?: [number, number],
// eslint-disable-next-line sonarjs/cognitive-complexity
): GetMinMaxPayload => {
let maxTime = new Date().getTime();
let minTime = 0;
if (interval === '1min') {
if (interval === '1m') {
const minTimeAgo = getMinAgo({ minutes: 1 }).getTime();
minTime = minTimeAgo;
} else if (interval === '10min') {
} else if (interval === '10m') {
const minTimeAgo = getMinAgo({ minutes: 10 }).getTime();
minTime = minTimeAgo;
} else if (interval === '15min') {
} else if (interval === '15m') {
const minTimeAgo = getMinAgo({ minutes: 15 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1hr') {
} else if (interval === '1h') {
const minTimeAgo = getMinAgo({ minutes: 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === '30min') {
} else if (interval === '30m') {
const minTimeAgo = getMinAgo({ minutes: 30 }).getTime();
minTime = minTimeAgo;
} else if (interval === '45min') {
} else if (interval === '45m') {
const minTimeAgo = getMinAgo({ minutes: 45 }).getTime();
minTime = minTimeAgo;
} else if (interval === '5min') {
} else if (interval === '5m') {
const minTimeAgo = getMinAgo({ minutes: 5 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1day') {
} else if (interval === '1d') {
// one day = 24*60(min)
const minTimeAgo = getMinAgo({ minutes: 24 * 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === '3days') {
} else if (interval === '3d') {
// three day = one day * 3
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 3 }).getTime();
minTime = minTimeAgo;
} else if (interval === '4days') {
} else if (interval === '4d') {
// four day = one day * 4
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 4 }).getTime();
minTime = minTimeAgo;
} else if (interval === '10days') {
} else if (interval === '10d') {
// ten day = one day * 10
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 10 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1week') {
} else if (interval === '1w') {
// one week = one day * 7
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 7 }).getTime();
minTime = minTimeAgo;
} else if (interval === '2weeks') {
} else if (interval === '2w') {
// two week = one day * 14
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 14 }).getTime();
minTime = minTimeAgo;
} else if (interval === '6weeks') {
} else if (interval === '6w') {
// six week = one day * 42
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 42 }).getTime();
minTime = minTimeAgo;
@@ -65,13 +103,17 @@ const GetMinMax = (
// two months = one day * 60
const minTimeAgo = getMinAgo({ minutes: 24 * 60 * 60 }).getTime();
minTime = minTimeAgo;
} else if (['3hr', '4hr', '6hr', '12hr'].includes(interval)) {
const h = parseInt(interval.replace('hr', ''), 10);
} else if (['3h', '4h', '6h', '12h'].includes(interval)) {
const h = parseInt(interval.replace('h', ''), 10);
const minTimeAgo = getMinAgo({ minutes: h * 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === 'custom') {
maxTime = (dateTimeRange || [])[1] || 0;
minTime = (dateTimeRange || [])[0] || 0;
} else if (isString(interval) && isValidTimeFormat(interval)) {
const { time, unit } = extractTimeAndUnit(interval);
minTime = getMinTimeForRelativeTimes(time, unit);
} else {
throw new Error('invalid time type');
}

View File

@@ -1,7 +1,10 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import store from 'store';
import getMaxMinTime from './getMaxMinTime';
@@ -38,7 +41,7 @@ const getStartEndRangeTime = ({
interface GetStartEndRangeTimesProps {
type?: timePreferenceType;
graphType?: PANEL_TYPES | null;
interval?: Time | TimeV2;
interval?: Time | TimeV2 | CustomTimeType;
}
interface GetStartEndRangeTimesPayload {

View File

@@ -7,7 +7,7 @@ import {
import { FORMULA_REGEXP } from 'constants/regExp';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
import { isObject } from 'lodash-es';
import { isEqual, isObject } from 'lodash-es';
import { ReactNode } from 'react';
import {
IBuilderFormula,
@@ -258,12 +258,7 @@ const findSeriaValueFromAnotherQuery = (
const localLabelEntries = Object.entries(seria.labels);
if (localLabelEntries.length !== labelEntries.length) return;
const isExistLabels = localLabelEntries.find(([key, value]) =>
labelEntries.find(
([currentKey, currentValue]) =>
currentKey === key && currentValue === value,
),
);
const isExistLabels = isEqual(localLabelEntries, labelEntries);
if (isExistLabels) {
value = seria;
@@ -304,10 +299,9 @@ const fillRestAggregationData = (
if (targetSeria) {
const isEqual = isEqualQueriesByLabel(equalQueriesByLabels, column.field);
if (!isEqual) {
// This line is crucial. It ensures that no additional rows are added to the table for similar labels across all formulas here is how this check is applied: signoz/frontend/src/lib/query/createTableColumnsFromQuery.ts line number 370
equalQueriesByLabels.push(column.field);
}
column.data.push(parseFloat(targetSeria.values[0].value).toFixed(2));
} else {
column.data.push('N/A');
}
@@ -357,6 +351,7 @@ const fillDataFromSeries = (
}
if (column.type !== 'field' && column.field !== queryName) {
// This code is executed only when there are multiple formulas. It checks if there are similar labels present in other formulas and, if found, adds them to the corresponding column data in the table.
fillRestAggregationData(
column,
queryTableData,

View File

@@ -1,12 +1,15 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import GetMinMax from 'lib/getMinMax';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
export const UpdateTimeInterval = (
interval: Time | TimeV2,
interval: Time | TimeV2 | CustomTimeType,
dateTimeRange: [number, number] = [0, 0],
): ((dispatch: Dispatch<AppActions>) => void) => (
dispatch: Dispatch<AppActions>,

View File

@@ -88,4 +88,7 @@ export const getFilter = (data: GetFilterPayload): TraceReducer['filter'] => {
};
export const stripTimestampsFromQuery = (query: string): string =>
query.replace(/(\?|&)startTime=\d+/, '').replace(/&endTime=\d+/, '');
query
.replace(/(\?|&)startTime=\d+/, '')
.replace(/&endTime=\d+/, '')
.replace(/[?&]relativeTime=[^&]+/g, '');

View File

@@ -1,5 +1,8 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { ResetIdStartAndEnd, SetSearchQueryString } from './logs';
@@ -14,7 +17,7 @@ export type GlobalTime = {
};
interface UpdateTime extends GlobalTime {
selectedTime: Time | TimeV2;
selectedTime: Time | TimeV2 | CustomTimeType;
}
interface UpdateTimeInterval {

View File

@@ -6,7 +6,8 @@ export interface ErrorResponse {
statusCode: ErrorStatusCode;
payload: null;
error: string;
message: null;
message: string | null;
body?: string | null;
}
export interface SuccessResponse<T, P = unknown> {

View File

@@ -1,12 +1,15 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { GlobalTime } from 'types/actions/globalTime';
export interface GlobalReducer {
maxTime: GlobalTime['maxTime'];
minTime: GlobalTime['minTime'];
loading: boolean;
selectedTime: Time | TimeV2;
selectedTime: Time | TimeV2 | CustomTimeType;
isAutoRefreshDisabled: boolean;
selectedAutoRefreshInterval: string;
}

View File

@@ -64,7 +64,7 @@ if (process.env.BUNDLE_ANALYSER === 'true') {
*/
const config = {
mode: 'development',
devtool: 'source-map',
devtool: 'eval-source-map',
entry: resolve(__dirname, './src/index.tsx'),
devServer: {
historyApiFallback: {

View File

@@ -79,7 +79,7 @@ if (process.env.BUNDLE_ANALYSER === 'true') {
const config = {
mode: 'production',
devtool: 'source-map',
devtool: 'eval-source-map',
entry: resolve(__dirname, './src/index.tsx'),
output: {
path: resolve(__dirname, './build'),

View File

@@ -162,7 +162,14 @@ func NewReaderFromClickhouseConnection(
os.Exit(1)
}
wrap := clickhouseConnWrapper{conn: db}
wrap := clickhouseConnWrapper{
conn: db,
settings: ClickhouseQuerySettings{
MaxExecutionTimeLeaf: os.Getenv("ClickHouseMaxExecutionTimeLeaf"),
TimeoutBeforeCheckingExecutionSpeed: os.Getenv("ClickHouseTimeoutBeforeCheckingExecutionSpeed"),
MaxBytesToRead: os.Getenv("ClickHouseMaxBytesToRead"),
},
}
return &ClickHouseReader{
db: wrap,
@@ -3674,7 +3681,7 @@ func isSelectedField(tableStatement string, field model.LogField) bool {
// in case of attributes and resources, if there is a materialized column present then it is selected
// TODO: handle partial change complete eg:- index is removed but materialized column is still present
name := utils.GetClickhouseColumnName(field.Type, field.DataType, field.Name)
return strings.Contains(tableStatement, fmt.Sprintf("`%s`", name))
return strings.Contains(tableStatement, fmt.Sprintf("%s", name))
}
func (r *ClickHouseReader) UpdateLogField(ctx context.Context, field *model.UpdateField) *model.ApiError {
@@ -3708,10 +3715,10 @@ func (r *ClickHouseReader) UpdateLogField(ctx context.Context, field *model.Upda
return &model.ApiError{Err: err, Typ: model.ErrorInternal}
}
query = fmt.Sprintf("ALTER TABLE %s.%s ON CLUSTER %s ADD COLUMN IF NOT EXISTS %s_exists bool DEFAULT if(indexOf(%s, '%s') != 0, true, false) CODEC(ZSTD(1))",
query = fmt.Sprintf("ALTER TABLE %s.%s ON CLUSTER %s ADD COLUMN IF NOT EXISTS %s_exists` bool DEFAULT if(indexOf(%s, '%s') != 0, true, false) CODEC(ZSTD(1))",
r.logsDB, table,
r.cluster,
colname,
strings.TrimSuffix(colname, "`"),
keyColName,
field.Name,
)
@@ -3733,10 +3740,10 @@ func (r *ClickHouseReader) UpdateLogField(ctx context.Context, field *model.Upda
if field.IndexGranularity == 0 {
field.IndexGranularity = constants.DefaultLogSkipIndexGranularity
}
query := fmt.Sprintf("ALTER TABLE %s.%s ON CLUSTER %s ADD INDEX IF NOT EXISTS %s_idx (%s) TYPE %s GRANULARITY %d",
query := fmt.Sprintf("ALTER TABLE %s.%s ON CLUSTER %s ADD INDEX IF NOT EXISTS %s_idx` (%s) TYPE %s GRANULARITY %d",
r.logsDB, r.logsLocalTable,
r.cluster,
colname,
strings.TrimSuffix(colname, "`"),
colname,
field.IndexType,
field.IndexGranularity,
@@ -3748,7 +3755,7 @@ func (r *ClickHouseReader) UpdateLogField(ctx context.Context, field *model.Upda
} else {
// Delete the index first
query := fmt.Sprintf("ALTER TABLE %s.%s ON CLUSTER %s DROP INDEX IF EXISTS %s_idx", r.logsDB, r.logsLocalTable, r.cluster, colname)
query := fmt.Sprintf("ALTER TABLE %s.%s ON CLUSTER %s DROP INDEX IF EXISTS %s_idx`", r.logsDB, r.logsLocalTable, r.cluster, strings.TrimSuffix(colname, "`"))
err := r.db.Exec(ctx, query)
if err != nil {
return &model.ApiError{Err: err, Typ: model.ErrorInternal}
@@ -3768,11 +3775,11 @@ func (r *ClickHouseReader) UpdateLogField(ctx context.Context, field *model.Upda
}
// drop exists column on logs table
query = "ALTER TABLE %s.%s ON CLUSTER %s DROP COLUMN IF EXISTS %s_exists "
query = "ALTER TABLE %s.%s ON CLUSTER %s DROP COLUMN IF EXISTS %s_exists` "
err = r.db.Exec(ctx, fmt.Sprintf(query,
r.logsDB, table,
r.cluster,
colname,
strings.TrimSuffix(colname, "`"),
),
)
if err != nil {
@@ -3802,7 +3809,7 @@ func (r *ClickHouseReader) GetLogs(ctx context.Context, params *model.LogsFilter
if lenFilters != 0 {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, userEmail)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, userEmail, true, false)
}
}
@@ -3844,7 +3851,7 @@ func (r *ClickHouseReader) TailLogs(ctx context.Context, client *model.LogsTailC
if lenFilters != 0 {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, userEmail)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, userEmail, true, false)
}
}
@@ -3936,7 +3943,7 @@ func (r *ClickHouseReader) AggregateLogs(ctx context.Context, params *model.Logs
if lenFilters != 0 {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, userEmail)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_LOGS_FILTERS, data, userEmail, true, false)
}
}
@@ -4329,7 +4336,7 @@ func isColumn(tableStatement, attrType, field, datType string) bool {
// value of attrType will be `resource` or `tag`, if `tag` change it to `attribute`
name := utils.GetClickhouseColumnName(attrType, datType, field)
return strings.Contains(tableStatement, fmt.Sprintf("`%s` ", name))
return strings.Contains(tableStatement, fmt.Sprintf("%s ", name))
}
func (r *ClickHouseReader) GetLogAggregateAttributes(ctx context.Context, req *v3.AggregateAttributeRequest) (*v3.AggregateAttributeResponse, error) {
@@ -4732,7 +4739,7 @@ func readRowsForTimeSeriesResult(rows driver.Rows, vars []interface{}, columnNam
series := v3.Series{Labels: seriesToAttrs[key], Points: points, GroupingSetsPoint: groupingSetsPoint, LabelsArray: labelsArray[key]}
seriesList = append(seriesList, &series)
}
return seriesList, nil
return seriesList, getPersonalisedError(rows.Err())
}
func logComment(ctx context.Context) string {
@@ -4823,10 +4830,24 @@ func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([
rowList = append(rowList, &v3.Row{Timestamp: t, Data: row})
}
return rowList, nil
return rowList, getPersonalisedError(rows.Err())
}
func getPersonalisedError(err error) error {
if err == nil {
return nil
}
if strings.Contains(err.Error(), "code: 307") {
return errors.New("query is consuming too much resources, please reach out to the team")
}
if strings.Contains(err.Error(), "code: 159") {
return errors.New("Query is taking too long to run, please reach out to the team")
}
return err
}
func removeDuplicateUnderscoreAttributes(row map[string]interface{}) {
if val, ok := row["attributes_int64"]; ok {
attributes := val.(*map[string]int64)

View File

@@ -9,8 +9,15 @@ import (
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
)
type ClickhouseQuerySettings struct {
MaxExecutionTimeLeaf string
TimeoutBeforeCheckingExecutionSpeed string
MaxBytesToRead string
}
type clickhouseConnWrapper struct {
conn clickhouse.Conn
conn clickhouse.Conn
settings ClickhouseQuerySettings
}
func (c clickhouseConnWrapper) Close() error {
@@ -25,16 +32,46 @@ func (c clickhouseConnWrapper) Stats() driver.Stats {
return c.conn.Stats()
}
func (c clickhouseConnWrapper) logComment(ctx context.Context) context.Context {
func (c clickhouseConnWrapper) addClickHouseSettings(ctx context.Context, query string) context.Context {
settings := clickhouse.Settings{}
logComment := c.getLogComment(ctx)
if logComment != "" {
settings["log_comment"] = logComment
}
// don't add resource restrictions for metrics and traces
if !strings.Contains(query, "signoz_logs") {
ctx = clickhouse.Context(ctx, clickhouse.WithSettings(settings))
return ctx
}
if c.settings.MaxBytesToRead != "" {
settings["max_bytes_to_read"] = c.settings.MaxBytesToRead
}
if c.settings.MaxExecutionTimeLeaf != "" {
settings["max_execution_time_leaf"] = c.settings.MaxExecutionTimeLeaf
}
if c.settings.TimeoutBeforeCheckingExecutionSpeed != "" {
settings["timeout_before_checking_execution_speed"] = c.settings.TimeoutBeforeCheckingExecutionSpeed
}
ctx = clickhouse.Context(ctx, clickhouse.WithSettings(settings))
return ctx
}
func (c clickhouseConnWrapper) getLogComment(ctx context.Context) string {
// Get the key-value pairs from context for log comment
kv := ctx.Value("log_comment")
if kv == nil {
return ctx
return ""
}
logCommentKVs, ok := kv.(map[string]string)
if !ok {
return ctx
return ""
}
logComment := ""
@@ -43,34 +80,31 @@ func (c clickhouseConnWrapper) logComment(ctx context.Context) context.Context {
}
logComment = strings.TrimSuffix(logComment, ", ")
ctx = clickhouse.Context(ctx, clickhouse.WithSettings(clickhouse.Settings{
"log_comment": logComment,
}))
return ctx
return logComment
}
func (c clickhouseConnWrapper) Query(ctx context.Context, query string, args ...interface{}) (driver.Rows, error) {
return c.conn.Query(c.logComment(ctx), query, args...)
return c.conn.Query(c.addClickHouseSettings(ctx, query), query, args...)
}
func (c clickhouseConnWrapper) QueryRow(ctx context.Context, query string, args ...interface{}) driver.Row {
return c.conn.QueryRow(c.logComment(ctx), query, args...)
return c.conn.QueryRow(c.addClickHouseSettings(ctx, query), query, args...)
}
func (c clickhouseConnWrapper) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
return c.conn.Select(c.logComment(ctx), dest, query, args...)
return c.conn.Select(c.addClickHouseSettings(ctx, query), dest, query, args...)
}
func (c clickhouseConnWrapper) Exec(ctx context.Context, query string, args ...interface{}) error {
return c.conn.Exec(c.logComment(ctx), query, args...)
return c.conn.Exec(c.addClickHouseSettings(ctx, query), query, args...)
}
func (c clickhouseConnWrapper) AsyncInsert(ctx context.Context, query string, wait bool, args ...interface{}) error {
return c.conn.AsyncInsert(c.logComment(ctx), query, wait, args...)
return c.conn.AsyncInsert(c.addClickHouseSettings(ctx, query), query, wait, args...)
}
func (c clickhouseConnWrapper) PrepareBatch(ctx context.Context, query string, opts ...driver.PrepareBatchOption) (driver.Batch, error) {
return c.conn.PrepareBatch(c.logComment(ctx), query, opts...)
return c.conn.PrepareBatch(c.addClickHouseSettings(ctx, query), query, opts...)
}
func (c clickhouseConnWrapper) ServerVersion() (*driver.ServerVersion, error) {

View File

@@ -401,6 +401,8 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
router.HandleFunc("/api/v1/explorer/views/{viewId}", am.EditAccess(aH.deleteSavedView)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/feedback", am.OpenAccess(aH.submitFeedback)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/events", am.ViewAccess(aH.registerEvent)).Methods(http.MethodPost)
// router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet)
router.HandleFunc("/api/v1/services", am.ViewAccess(aH.getServices)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/services/list", am.ViewAccess(aH.getServicesList)).Methods(http.MethodGet)
@@ -1502,7 +1504,22 @@ func (aH *APIHandler) submitFeedback(w http.ResponseWriter, r *http.Request) {
}
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_INPRODUCT_FEEDBACK, data, userEmail)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_INPRODUCT_FEEDBACK, data, userEmail, true, false)
}
}
func (aH *APIHandler) registerEvent(w http.ResponseWriter, r *http.Request) {
request, err := parseRegisterEventRequest(r)
if aH.HandleError(w, err, http.StatusBadRequest) {
return
}
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(request.EventName, request.Attributes, userEmail, true, true)
aH.WriteJSON(w, r, map[string]string{"data": "Event Processed Successfully"})
} else {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
}
}
@@ -1585,7 +1602,7 @@ func (aH *APIHandler) getServices(w http.ResponseWriter, r *http.Request) {
}
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_NUMBER_OF_SERVICES, data, userEmail)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_NUMBER_OF_SERVICES, data, userEmail, true, false)
}
if (data["number"] != 0) && (data["number"] != telemetry.DEFAULT_NUMBER_OF_SERVICES) {
@@ -2310,7 +2327,7 @@ func (aH *APIHandler) editOrg(w http.ResponseWriter, r *http.Request) {
"organizationName": req.Name,
}
userEmail, err := auth.GetEmailFromJwt(r.Context())
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_ORG_SETTINGS, data, userEmail)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_ORG_SETTINGS, data, userEmail, true, false)
aH.WriteJSON(w, r, map[string]string{"data": "org updated successfully"})
}
@@ -3525,7 +3542,7 @@ func sendQueryResultEvents(r *http.Request, result []*v3.Result, queryRangeParam
"metricsUsed": signozMetricsUsed,
"dashboardId": dashboardID,
"widgetId": widgetID,
}, userEmail)
}, userEmail, true, false)
}
if alertMatched {
var alertID string
@@ -3547,7 +3564,7 @@ func sendQueryResultEvents(r *http.Request, result []*v3.Result, queryRangeParam
"logsUsed": signozLogsUsed,
"metricsUsed": signozMetricsUsed,
"alertId": alertID,
}, userEmail)
}, userEmail, true, false)
}
}
}

View File

@@ -1,33 +0,0 @@
{
"id": "parse-default-mongo-access-log",
"name": "Parse default mongo access log",
"alias": "parse-default-mongo-access-log",
"description": "Parse standard mongo access log",
"enabled": true,
"filter": {
"op": "AND",
"items": [
{
"key": {
"type": "tag",
"key": "source",
"dataType": "string"
},
"op": "=",
"value": "mongo"
}
]
},
"config": [
{
"type": "grok_parser",
"id": "parse-body-grok",
"enabled": true,
"orderId": 1,
"name": "Parse Body",
"parse_to": "attributes",
"pattern": "%{GREEDYDATA}",
"parse_from": "body"
}
]
}

View File

@@ -0,0 +1,117 @@
### Collect MongoDB Logs
You can configure MongoDB logs collection by providing the required collector config to your collector.
#### Create collector config file
Save the following config for collecting MongoDB logs in a file named `mongodb-logs-collection-config.yaml`
```yaml
receivers:
filelog/mongodb:
include: ["${env:MONGODB_LOG_FILE}"]
operators:
# Parse structured mongodb logs
# For more details, see https://www.mongodb.com/docs/manual/reference/log-messages/#structured-logging
- type: json_parser
if: body matches '^\\s*{\\s*".*}\\s*$'
parse_from: body
parse_to: attributes
timestamp:
parse_from: attributes.t.$$date
layout: '2006-01-02T15:04:05.000-07:00'
layout_type: gotime
severity:
parse_from: attributes.s
overwrite_text: true
mapping:
debug:
- D1
- D2
- D3
- D4
- D5
info: I
warn: W
error: E
fatal: F
- type: flatten
if: attributes.attr != nil
field: attributes.attr
- type: move
if: attributes.msg != nil
from: attributes.msg
to: body
- type: move
if: attributes.c != nil
from: attributes.c
to: attributes.component
- type: move
if: attributes.id != nil
from: attributes.id
to: attributes.mongo_log_id
- type: remove
if: attributes.t != nil
field: attributes.t
- type: remove
if: attributes.s != nil
field: attributes.s
- type: add
field: attributes.source
value: mongodb
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
exporters:
# export to SigNoz cloud
otlp/mongodb-logs:
endpoint: "${env:OTLP_DESTINATION_ENDPOINT}"
tls:
insecure: false
headers:
"signoz-access-token": "${env:SIGNOZ_INGESTION_KEY}"
# export to local collector
# otlp/mongodb-logs:
# endpoint: "localhost:4317"
# tls:
# insecure: true
service:
pipelines:
logs/mongodb:
receivers: [filelog/mongodb]
processors: [batch]
exporters: [otlp/mongodb-logs]
```
#### Set Environment Variables
Set the following environment variables in your otel-collector environment:
```bash
# path of MongoDB server log file. must be accessible by the otel collector
export MONGODB_LOG_FILE=/var/log/mongodb.log
# region specific SigNoz cloud ingestion endpoint
export OTLP_DESTINATION_ENDPOINT="ingest.us.signoz.cloud:443"
# your SigNoz ingestion key
export SIGNOZ_INGESTION_KEY="signoz-ingestion-key"
```
#### Use collector config file
Make the collector config file available to your otel collector and use it by adding the following flag to the command for running your collector
```bash
--config mongodb-logs-collection-config.yaml
```
Note: the collector can use multiple config files, specified by multiple occurrences of the --config flag.

View File

@@ -1,19 +1,21 @@
### Configure otel collector
### Collect MongoDB Metrics
#### Save collector config file
You can configure MongoDB metrics collection by providing the required collector config to your collector.
Save the following collector config in a file named `mongo-collector-config.yaml`
#### Create collector config file
```bash
Save the following config for collecting mongodb metrics in a file named `mongodb-metrics-collection-config.yaml`
```yaml
receivers:
mongodb:
# - For standalone MongoDB deployments this is the hostname and port of the mongod instance
# - For replica sets specify the hostnames and ports of the mongod instances that are in the replica set configuration. If the replica_set field is specified, nodes will be autodiscovered.
# - For a sharded MongoDB deployment, please specify a list of the mongos hosts.
hosts:
- endpoint: 127.0.0.1:27017
- endpoint: ${env:MONGODB_ENDPOINT}
# If authentication is required, the user can with clusterMonitor permissions can be provided here
username: monitoring
username: ${env:MONGODB_USERNAME}
# If authentication is required, the password can be provided here.
password: ${env:MONGODB_PASSWORD}
collection_interval: 60s
@@ -46,18 +48,19 @@ processors:
hostname_sources: ["os"]
exporters:
# export to local collector
otlp/local:
endpoint: "localhost:4317"
tls:
insecure: true
# export to SigNoz cloud
otlp/signoz:
endpoint: "ingest.{region}.signoz.cloud:443"
otlp/mongodb:
endpoint: "${env:OTLP_DESTINATION_ENDPOINT}"
tls:
insecure: false
headers:
"signoz-access-token": "<SIGNOZ_INGESTION_KEY>"
"signoz-access-token": "${env:SIGNOZ_INGESTION_KEY}"
# export to local collector
# otlp/mongodb:
# endpoint: "localhost:4317"
# tls:
# insecure: true
service:
pipelines:
@@ -65,10 +68,37 @@ service:
receivers: [mongodb]
# note: remove this processor if the collector host is not running on the same host as the mongo instance
processors: [resourcedetection/system]
exporters: [otlp/local]
exporters: [otlp/mongodb]
```
#### Set Environment Variables
Set the following environment variables in your otel-collector environment:
```bash
# MongoDB endpoint reachable from the otel collector"
export MONGODB_ENDPOINT="host:port"
# password for MongoDB monitoring user"
export MONGODB_USERNAME="monitoring"
# password for MongoDB monitoring user"
export MONGODB_PASSWORD="<PASSWORD>"
# region specific SigNoz cloud ingestion endpoint
export OTLP_DESTINATION_ENDPOINT="ingest.us.signoz.cloud:443"
# your SigNoz ingestion key
export SIGNOZ_INGESTION_KEY="signoz-ingestion-key"
```
#### Use collector config file
Run your collector with the added flag `--config mongo-collector-config.yaml`
Make the collector config file available to your otel collector and use it by adding the following flag to the command for running your collector
```bash
--config mongodb-metrics-collection-config.yaml
```
Note: the collector can use multiple config files, specified by multiple occurrences of the --config flag.

View File

@@ -1,22 +1,41 @@
### Prepare mongo for monitoring
## Before You Begin
- Have a running mongodb instance
- Have the monitoring user created
- Have the monitoring user granted the necessary permissions
To configure metrics and logs collection for MongoDB, you need the following.
Mongodb recommends to set up a least privilege user (LPU) with a `clusterMonitor` role in order to collect.
### Ensure MongoDB server is prepared for monitoring
Run the following command to create a user with the necessary permissions.
- **Ensure that the MongoDB server is running a supported version**
MongoDB versions 4.4+ are supported.
You can use the following statement to determine server version
```js
db.version()
```
```bash
use admin
db.createUser(
{
user: "monitoring",
pwd: "<PASSWORD>",
roles: ["clusterMonitor"]
}
);
```
- **If collecting metrics, ensure that there is a MongoDB user with required permissions**
Mongodb recommends to set up a least privilege user (LPU) with a clusterMonitor role in order to collect metrics
Replace `<PASSWORD>` with a strong password and set is as env var `MONGODB_PASSWORD`.
To create a monitoring user, run:
```js
use admin
db.createUser(
{
user: "monitoring",
pwd: "<PASSWORD>",
roles: ["clusterMonitor"]
}
);
```
### Ensure OTEL Collector is running with access to the MongoDB server
- **Ensure that an OTEL collector is running in your deployment environment**
If needed, please [install an OTEL Collector](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/)
If already installed, ensure that the collector version is v0.88.0 or newer.
Also ensure that you can provide config files to the collector and that you can set environment variables and command line flags used for running it.
- **Ensure that the OTEL collector can access the MongoDB server**
In order to collect metrics, the collector must be able to access the MongoDB server as a client using the monitoring user.
In order to collect logs, the collector must be able to read the MongoDB server log file.

View File

@@ -18,18 +18,20 @@
"instructions": "file://config/prerequisites.md"
},
{
"title": "Configure Otel Collector",
"instructions": "file://config/configure-otel-collector.md"
"title": "Collect Metrics",
"instructions": "file://config/collect-metrics.md"
},
{
"title": "Collect Logs",
"instructions": "file://config/collect-logs.md"
}
],
"assets": {
"logs": {
"pipelines": [
"file://assets/pipelines/log-parser.json"
]
"pipelines": []
},
"dashboards": [
"file://assets/dashboards/overview.json"
"file://assets/dashboards/overview.json"
],
"alerts": []
},
@@ -52,37 +54,207 @@
"data_collected": {
"logs": [
{
"name": "Request Method",
"path": "attributes[\"http.request.method\"]",
"type": "string",
"description": "HTTP method"
"name": "Timestamp",
"path": "timestamp",
"type": "timestamp"
},
{
"name": "Request Path",
"path": "attributes[\"url.path\"]",
"type": "string",
"description": "path requested"
"name": "Severity Text",
"path": "severity_text",
"type": "string"
},
{
"name": "Response Status Code",
"path": "attributes[\"http.response.status_code\"]",
"type": "int",
"description": "HTTP response code"
"name": "Severity Number",
"path": "severity_number",
"type": "number"
},
{
"name": "MongoDB Component",
"path": "attributes.component",
"type": "string"
}
],
"metrics": [
{
"name": "http.server.request.duration",
"type": "Histogram",
"unit": "s",
"description": "Duration of HTTP server requests"
"description": "The number of cache operations of the instance.",
"unit": "number",
"type": "Sum",
"name": "mongodb_cache_operations"
},
{
"name": "http.server.active_requests",
"type": "UpDownCounter",
"unit": "{ request }",
"description": "Number of active HTTP server requests"
"description": "The number of collections.",
"unit": "number",
"type": "Sum",
"name": "mongodb_collection_count"
},
{
"description": "The size of the collection. Data compression does not affect this value.",
"unit": "Bytes",
"type": "Sum",
"name": "mongodb_data_size"
},
{
"description": "The number of connections.",
"unit": "number",
"type": "Sum",
"name": "mongodb_connection_count"
},
{
"description": "The number of extents.",
"unit": "number",
"type": "Sum",
"name": "mongodb_extent_count"
},
{
"description": "The time the global lock has been held.",
"unit": "ms",
"type": "Sum",
"name": "mongodb_global_lock_time"
},
{
"description": "The number of indexes.",
"unit": "number",
"type": "Sum",
"name": "mongodb_index_count"
},
{
"description": "Sum of the space allocated to all indexes in the database, including free index space.",
"unit": "Bytes",
"type": "Sum",
"name": "mongodb_index_size"
},
{
"description": "The amount of memory used.",
"unit": "Bytes",
"type": "Sum",
"name": "mongodb_memory_usage"
},
{
"description": "The number of objects.",
"unit": "number",
"type": "Sum",
"name": "mongodb_object_count"
},
{
"description": "The latency of operations.",
"unit": "us",
"type": "Gauge",
"name": "mongodb_operation_latency_time"
},
{
"description": "The number of operations executed.",
"unit": "number",
"type": "Sum",
"name": "mongodb_operation_count"
},
{
"description": "The number of replicated operations executed.",
"unit": "number",
"type": "Sum",
"name": "mongodb_operation_repl_count"
},
{
"description": "The total amount of storage allocated to this collection.",
"unit": "Bytes",
"type": "Sum",
"name": "mongodb_storage_size"
},
{
"description": "The number of existing databases.",
"unit": "number",
"type": "Sum",
"name": "mongodb_database_count"
},
{
"description": "The number of times an index has been accessed.",
"unit": "number",
"type": "Sum",
"name": "mongodb_index_access_count"
},
{
"description": "The number of document operations executed.",
"unit": "number",
"type": "Sum",
"name": "mongodb_document_operation_count"
},
{
"description": "The number of bytes received.",
"unit": "Bytes",
"type": "Sum",
"name": "mongodb_network_io_receive"
},
{
"description": "The number of by transmitted.",
"unit": "Bytes",
"type": "Sum",
"name": "mongodb_network_io_transmit"
},
{
"description": "The number of requests received by the server.",
"unit": "number",
"type": "Sum",
"name": "mongodb_network_request_count"
},
{
"description": "The total time spent performing operations.",
"unit": "ms",
"type": "Sum",
"name": "mongodb_operation_time"
},
{
"description": "The total number of active sessions.",
"unit": "number",
"type": "Sum",
"name": "mongodb_session_count"
},
{
"description": "The number of open cursors maintained for clients.",
"unit": "number",
"type": "Sum",
"name": "mongodb_cursor_count"
},
{
"description": "The number of cursors that have timed out.",
"unit": "number",
"type": "Sum",
"name": "mongodb_cursor_timeout_count"
},
{
"description": "Number of times the lock was acquired in the specified mode.",
"unit": "number",
"type": "Sum",
"name": "mongodb_lock_acquire_count"
},
{
"description": "Number of times the lock acquisitions encountered waits because the locks were held in a conflicting mode.",
"unit": "number",
"type": "Sum",
"name": "mongodb_lock_acquire_wait_count"
},
{
"description": "Cumulative wait time for the lock acquisitions.",
"unit": "microseconds",
"type": "Sum",
"name": "mongodb_lock_acquire_time"
},
{
"description": "Number of times the lock acquisitions encountered deadlocks.",
"unit": "number",
"type": "Sum",
"name": "mongodb_lock_deadlock_count"
},
{
"description": "The health status of the server.",
"unit": "number",
"type": "Gauge",
"name": "mongodb_health"
},
{
"description": "The amount of time that the server has been running.",
"unit": "ms",
"type": "Sum",
"name": "mongodb_uptime"
}
]
}
}
}

View File

@@ -1,3 +1,6 @@
### Monitor MongoDB with SigNoz
Collect key MongoDB metrics and parse your MongoDB logs
Collect key MongoDB metrics and view them with an out of the box dashboard.
Collect and parse MongoDB logs to populate timestamp, severity, and other log attributes for better querying and aggregation.

View File

@@ -1,62 +0,0 @@
{
"id": "parse-default-nginx-access-log",
"name": "Parse default nginx access log",
"alias": "parse-default-nginx-access-log",
"description": "Parse standard nginx access log",
"enabled": true,
"filter": {
"op": "AND",
"items": [
{
"key": {
"type": "tag",
"key": "source",
"dataType": "string"
},
"op": "=",
"value": "nginx"
}
]
},
"config": [
{
"type": "grok_parser",
"id": "parse-body-grok",
"enabled": true,
"orderId": 1,
"name": "Parse Body",
"parse_to": "attributes",
"pattern": "%{IP:client.address} - %{USERNAME:enduser.id} \\[%{HTTPDATE:time.local}\\] \"((%{WORD:http.method} %{DATA:http.path}(\\?%{DATA:http.query})? %{WORD:network.protocol.name}/%{NOTSPACE:network.protocol.version})|%{DATA})\" %{INT:http.response.status_code:int} %{INT:http.request.body.bytes:int} \"%{NOTSPACE:http.referer}\" \"%{DATA:http.user.agent}\" %{INT:http.request.bytes:int} %{NUMBER:http.request.time:float} \\[%{DATA:proxy.upstream.name}?\\] \\[%{DATA:proxy.alternative.upstream.name}?\\] ((%{IP:network.peer.address}:%{INT:network.peer.port:int})|%{DATA})? (%{INT:http.response.bytes:int}|-)? (%{NUMBER:http.response.time:float}|-)? (%{NUMBER:network.peer.status.code:int}|-)? %{NOTSPACE:request.id}",
"parse_from": "body"
},
{
"type": "severity_parser",
"id": "parse-sev",
"enabled": true,
"orderId": 2,
"name": "Set Severity",
"parse_from": "attributes[\"http.response.status_code\"]",
"mapping": {
"debug": [
"1xx"
],
"error": [
"4xx"
],
"fatal": [
"5xx"
],
"info": [
"2xx"
],
"trace": [
"trace"
],
"warn": [
"3xx"
]
},
"overwrite_text": true
}
]
}

View File

@@ -0,0 +1,139 @@
### Collect Nginx Logs
You can configure Nginx logs collection by providing the required collector config to your collector.
#### Create collector config file
Save the following config for collecting Nginx logs in a file named `nginx-logs-collection-config.yaml`
```yaml
receivers:
filelog/nginx-access-logs:
include: ["${env:NGINX_ACCESS_LOG_FILE}"]
operators:
# Parse the default nginx access log format. Nginx defaults to the "combined" log format
# $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"
# For more details, see https://nginx.org/en/docs/http/ngx_http_log_module.html
- type: regex_parser
if: body matches '^(?P<remote_addr>[0-9\\.]+) - (?P<remote_user>[^\\s]+) \\[(?P<ts>.+)\\] "(?P<request_method>\\w+?) (?P<request_path>.+?)" (?P<status>[0-9]+) (?P<body_bytes_sent>[0-9]+) "(?P<http_referrer>.+?)" "(?P<http_user_agent>.+?)"$'
parse_from: body
parse_to: attributes
regex: '^(?P<remote_addr>[0-9\.]+) - (?P<remote_user>[^\s]+) \[(?P<ts>.+)\] "(?P<request_method>\w+?) (?P<request_path>.+?)" (?P<status>[0-9]+) (?P<body_bytes_sent>[0-9]+) "(?P<http_referrer>.+?)" "(?P<http_user_agent>.+?)"$'
timestamp:
parse_from: attributes.ts
layout: "02/Jan/2006:15:04:05 -0700"
layout_type: gotime
severity:
parse_from: attributes.status
overwrite_text: true
mapping:
debug: "1xx"
info:
- "2xx"
- "3xx"
warn: "4xx"
error: "5xx"
- type: remove
if: attributes.ts != nil
field: attributes.ts
- type: add
field: attributes.source
value: nginx
filelog/nginx-error-logs:
include: ["${env:NGINX_ERROR_LOG_FILE}"]
operators:
# Parse the default nginx error log format.
# YYYY/MM/DD HH:MM:SS [LEVEL] PID#TID: *CID MESSAGE
# For more details, see https://github.com/phusion/nginx/blob/master/src/core/ngx_log.c
- type: regex_parser
if: body matches '^(?P<ts>.+?) \\[(?P<log_level>\\w+)\\] (?P<pid>\\d+)#(?P<tid>\\d+). \\*(?P<cid>\\d+) (?P<message>.+)$'
parse_from: body
parse_to: attributes
regex: '^(?P<ts>.+?) \[(?P<log_level>\w+)\] (?P<pid>\d+)#(?P<tid>\d+). \*(?P<cid>\d+) (?P<message>.+)$'
timestamp:
parse_from: attributes.ts
layout: "2006/01/02 15:04:05"
layout_type: gotime
severity:
parse_from: attributes.log_level
overwrite_text: true
mapping:
debug: "debug"
info:
- "info"
- "notice"
warn: "warn"
error:
- "error"
- "crit"
- "alert"
fatal: "emerg"
- type: remove
if: attributes.ts != nil
field: attributes.ts
- type: move
if: attributes.message != nil
from: attributes.message
to: body
- type: add
field: attributes.source
value: nginx
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
exporters:
# export to SigNoz cloud
otlp/nginx-logs:
endpoint: "${env:OTLP_DESTINATION_ENDPOINT}"
tls:
insecure: false
headers:
"signoz-access-token": "${env:SIGNOZ_INGESTION_KEY}"
# export to local collector
# otlp/nginx-logs:
# endpoint: "localhost:4317"
# tls:
# insecure: true
service:
pipelines:
logs/nginx:
receivers: [filelog/nginx-access-logs, filelog/nginx-error-logs]
processors: [batch]
exporters: [otlp/nginx-logs]
```
#### Set Environment Variables
Set the following environment variables in your otel-collector environment:
```bash
# path of Nginx access log file. must be accessible by the otel collector
export NGINX_ACCESS_LOG_FILE=/var/log/nginx/access.log;
# path of Nginx error log file. must be accessible by the otel collector
export NGINX_ERROR_LOG_FILE=/var/log/nginx/error.log
# region specific SigNoz cloud ingestion endpoint
export OTLP_DESTINATION_ENDPOINT="ingest.us.signoz.cloud:443"
# your SigNoz ingestion key
export SIGNOZ_INGESTION_KEY="signoz-ingestion-key"
```
#### Use collector config file
Make the collector config file available to your otel collector and use it by adding the following flag to the command for running your collector
```bash
--config nginx-logs-collection-config.yaml
```
Note: the collector can use multiple config files, specified by multiple occurrences of the --config flag.

View File

@@ -1 +0,0 @@
### Prepare nginx for observability

View File

@@ -0,0 +1,19 @@
## Before You Begin
To configure logs collection for Nginx, you need the following.
### Ensure Nginx server is running a supported version
Ensure that your Nginx server is running a version newer than 1.0.0
### Ensure OTEL Collector is running with access to the Nginx server
- **Ensure that an OTEL collector is running in your deployment environment**
If needed, please [install an OTEL Collector](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/)
If already installed, ensure that the collector version is v0.88.0 or newer.
Also ensure that you can provide config files to the collector and that you can set environment variables and command line flags used for running it.
- **Ensure that the OTEL collector can access the Nginx server**
In order to collect logs, the collector must be able to read Nginx server log files.

View File

@@ -15,19 +15,17 @@
"overview": "file://overview.md",
"configuration": [
{
"title": "Prepare Nginx",
"instructions": "file://config/prepare-nginx.md"
"title": "Prerequisites",
"instructions": "file://config/prerequisites.md"
},
{
"title": "Configure Otel Collector",
"instructions": "file://config/configure-otel-collector.md"
"title": "Collect Logs",
"instructions": "file://config/collect-logs.md"
}
],
"assets": {
"logs": {
"pipelines": [
"file://assets/pipelines/log-parser.json"
]
"pipelines": []
},
"dashboards": null,
"alerts": null
@@ -50,38 +48,57 @@
},
"data_collected": {
"logs": [
{
"name": "Timestamp",
"path": "timestamp",
"type": "timestamp"
},
{
"name": "Severity Text",
"path": "severity_text",
"type": "string"
},
{
"name": "Severity Number",
"path": "severity_number",
"type": "number"
},
{
"name": "Body Bytes Sent",
"path": "attributes.body_bytes_sent",
"type": "string"
},
{
"name": "Referrer",
"path": "attributes.http_referrer",
"type": "string"
},
{
"name": "User Agent",
"path": "attributes.http_user_agent",
"type": "string"
},
{
"name": "Request Method",
"path": "attributes[\"http.request.method\"]",
"type": "string",
"description": "HTTP method"
"path": "attributes.request_method",
"type": "string"
},
{
"name": "Request Path",
"path": "attributes[\"url.path\"]",
"type": "string",
"description": "path requested"
"path": "attributes.request_path",
"type": "string"
},
{
"name": "Response Status Code",
"path": "attributes[\"http.response.status_code\"]",
"type": "int",
"description": "HTTP response code"
}
],
"metrics": [
{
"name": "http.server.request.duration",
"type": "Histogram",
"unit": "s",
"description": "Duration of HTTP server requests"
"path": "attributes.status",
"type": "string"
},
{
"name": "http.server.active_requests",
"type": "UpDownCounter",
"unit": "{ request }",
"description": "Number of active HTTP server requests"
"name": "Remote Address",
"path": "attributes.remote_addr",
"type": "string"
}
]
],
"metrics": []
}
}
}

View File

@@ -1,3 +1,3 @@
### Monitor Nginx with SigNoz
Parse your Nginx logs and collect key metrics.
Collect and parse Nginx logs to populate timestamp, severity, and other log attributes for better querying and aggregation.

View File

@@ -35,7 +35,7 @@ receivers:
- LOG
- NOTICE
- DETAIL
warning: WARNING
warn: WARNING
error: ERROR
fatal:
- FATAL

View File

@@ -1,33 +0,0 @@
{
"id": "parse-default-redis-access-log",
"name": "Parse default redis access log",
"alias": "parse-default-redis-access-log",
"description": "Parse standard redis access log",
"enabled": true,
"filter": {
"op": "AND",
"items": [
{
"key": {
"type": "tag",
"key": "source",
"dataType": "string"
},
"op": "=",
"value": "redis"
}
]
},
"config": [
{
"type": "grok_parser",
"id": "parse-body-grok",
"enabled": true,
"orderId": 1,
"name": "Parse Body",
"parse_to": "attributes",
"pattern": "%{GREEDYDATA}",
"parse_from": "body"
}
]
}

View File

@@ -29,7 +29,7 @@ receivers:
info:
- '-'
- '*'
warning: '#'
warn: '#'
on_error: send
- type: move
if: attributes.message != nil

View File

@@ -28,9 +28,7 @@
],
"assets": {
"logs": {
"pipelines": [
"file://assets/pipelines/log-parser.json"
]
"pipelines": []
},
"dashboards": [
"file://assets/dashboards/overview.json"

View File

@@ -252,7 +252,7 @@ func TestReplaceInterestingFields(t *testing.T) {
},
}
expectedTokens := []string{"attributes_int64_value[indexOf(attributes_int64_key, 'id.userid')] IN (100) ", "and attribute_int64_id_key >= 50 ", `AND body ILIKE '%searchstring%'`}
expectedTokens := []string{"attributes_int64_value[indexOf(attributes_int64_key, 'id.userid')] IN (100) ", "and `attribute_int64_id_key` >= 50 ", `AND body ILIKE '%searchstring%'`}
Convey("testInterestingFields", t, func() {
tokens, err := replaceInterestingFields(&allFields, queryTokens)
So(err, ShouldBeNil)
@@ -374,7 +374,7 @@ var generateSQLQueryTestCases = []struct {
IdGt: "2BsKLKv8cZrLCn6rkOcRGkdjBdM",
IdLT: "2BsKG6tRpFWjYMcWsAGKfSxoQdU",
},
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' and id > '2BsKLKv8cZrLCn6rkOcRGkdjBdM' and id < '2BsKG6tRpFWjYMcWsAGKfSxoQdU' ) and ( attribute_int64_field1 < 100 and attribute_int64_field1 > 50 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ",
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' and id > '2BsKLKv8cZrLCn6rkOcRGkdjBdM' and id < '2BsKG6tRpFWjYMcWsAGKfSxoQdU' ) and ( `attribute_int64_field1` < 100 and `attribute_int64_field1` > 50 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ",
},
{
Name: "second query with only timestamp range",
@@ -383,7 +383,7 @@ var generateSQLQueryTestCases = []struct {
TimestampStart: uint64(1657689292000),
TimestampEnd: uint64(1657689294000),
},
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( attribute_int64_field1 < 100 and attribute_int64_field1 > 50 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ",
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( `attribute_int64_field1` < 100 and `attribute_int64_field1` > 50 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ",
},
{
Name: "generate case sensitive query",
@@ -392,7 +392,7 @@ var generateSQLQueryTestCases = []struct {
TimestampStart: uint64(1657689292000),
TimestampEnd: uint64(1657689294000),
},
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( attribute_int64_field1 < 100 and attributes_int64_value[indexOf(attributes_int64_key, 'FielD1')] > 50 and attribute_double64_Field2 > 10 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ",
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( `attribute_int64_field1` < 100 and attributes_int64_value[indexOf(attributes_int64_key, 'FielD1')] > 50 and `attribute_double64_Field2` > 10 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ",
},
{
Name: "Check exists and not exists",
@@ -401,7 +401,7 @@ var generateSQLQueryTestCases = []struct {
TimestampStart: uint64(1657689292000),
TimestampEnd: uint64(1657689294000),
},
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( has(attributes_int64_key, 'field1') and NOT has(attributes_double64_key, 'Field2') and attribute_double64_Field2 > 10 ) ",
SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( has(attributes_int64_key, 'field1') and NOT has(attributes_double64_key, 'Field2') and `attribute_double64_Field2` > 10 ) ",
},
{
Name: "Check top level key filter",

View File

@@ -74,6 +74,12 @@ func isEnriched(field v3.AttributeKey) bool {
if field.Type == v3.AttributeKeyTypeUnspecified || field.DataType == v3.AttributeKeyDataTypeUnspecified {
return false
}
// try to enrich all attributes which doesn't have isColumn = true
if !field.IsColumn {
return false
}
return true
}
@@ -148,8 +154,12 @@ func enrichFieldWithMetadata(field v3.AttributeKey, fields map[string]v3.Attribu
}
// enrich with default values if metadata is not found
field.Type = v3.AttributeKeyTypeTag
field.DataType = v3.AttributeKeyDataTypeString
if field.Type == "" {
field.Type = v3.AttributeKeyTypeTag
}
if field.DataType == "" {
field.DataType = v3.AttributeKeyDataTypeString
}
return field
}

View File

@@ -30,7 +30,7 @@ var testEnrichmentRequiredData = []struct {
},
},
},
EnrichmentRequired: false,
EnrichmentRequired: true,
},
{
Name: "attribute enrichment required",
@@ -66,7 +66,7 @@ var testEnrichmentRequiredData = []struct {
},
},
},
EnrichmentRequired: false,
EnrichmentRequired: true,
},
{
Name: "filter enrichment required",
@@ -118,7 +118,7 @@ var testEnrichmentRequiredData = []struct {
},
},
},
EnrichmentRequired: false,
EnrichmentRequired: true,
},
{
Name: "groupBy enrichment required",
@@ -151,7 +151,7 @@ var testEnrichmentRequiredData = []struct {
},
},
},
EnrichmentRequired: false,
EnrichmentRequired: true,
},
{
Name: "orderBy enrichment required",
@@ -200,7 +200,7 @@ var testEnrichmentRequiredData = []struct {
},
},
},
EnrichmentRequired: false,
EnrichmentRequired: true,
},
}
@@ -255,6 +255,7 @@ var testEnrichParamsData = []struct {
Key: "response_time",
Type: v3.AttributeKeyTypeTag,
DataType: v3.AttributeKeyDataTypeInt64,
IsColumn: true,
},
},
Result: v3.QueryRangeParamsV3{
@@ -273,7 +274,7 @@ var testEnrichParamsData = []struct {
{Key: v3.AttributeKey{Key: "user_name", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeString}, Value: "john", Operator: "="},
}},
GroupBy: []v3.AttributeKey{{Key: "trace_id", Type: v3.AttributeKeyTypeUnspecified, DataType: v3.AttributeKeyDataTypeString, IsColumn: true}},
OrderBy: []v3.OrderBy{{ColumnName: "response_time", Key: "response_time", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeInt64}},
OrderBy: []v3.OrderBy{{ColumnName: "response_time", Key: "response_time", Type: v3.AttributeKeyTypeTag, DataType: v3.AttributeKeyDataTypeInt64, IsColumn: true}},
},
},
},

View File

@@ -150,7 +150,7 @@ func GetExistsNexistsFilter(op v3.FilterOperator, item v3.FilterItem) string {
if op == v3.FilterOperatorNotExists {
val = false
}
return fmt.Sprintf("%s_exists=%v", getClickhouseColumnName(item.Key), val)
return fmt.Sprintf("%s_exists`=%v", strings.TrimSuffix(getClickhouseColumnName(item.Key), "`"), val)
}
columnType := getClickhouseLogsColumnType(item.Key.Type)
columnDataType := getClickhouseLogsColumnDataType(item.Key.DataType)
@@ -212,7 +212,7 @@ func buildLogsTimeSeriesFilterQuery(fs *v3.FilterSet, groupBy []v3.AttributeKey,
conditions = append(conditions, fmt.Sprintf("has(%s_%s_key, '%s')", columnType, columnDataType, attr.Key))
} else if attr.Type != v3.AttributeKeyTypeUnspecified {
// for materialzied columns
conditions = append(conditions, fmt.Sprintf("%s_exists=true", getClickhouseColumnName(attr)))
conditions = append(conditions, fmt.Sprintf("%s_exists`=true", strings.TrimSuffix(getClickhouseColumnName(attr), "`")))
}
}

View File

@@ -26,17 +26,17 @@ var testGetClickhouseColumnNameData = []struct {
{
Name: "selected field",
AttributeKey: v3.AttributeKey{Key: "servicename", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true},
ExpectedColumnName: "attribute_string_servicename",
ExpectedColumnName: "`attribute_string_servicename`",
},
{
Name: "selected field resource",
AttributeKey: v3.AttributeKey{Key: "sdk_version", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeResource, IsColumn: true},
ExpectedColumnName: "resource_int64_sdk_version",
ExpectedColumnName: "`resource_int64_sdk_version`",
},
{
Name: "selected field float",
AttributeKey: v3.AttributeKey{Key: "sdk_version", DataType: v3.AttributeKeyDataTypeFloat64, Type: v3.AttributeKeyTypeTag, IsColumn: true},
ExpectedColumnName: "attribute_float64_sdk_version",
ExpectedColumnName: "`attribute_float64_sdk_version`",
},
{
Name: "same name as top level column",
@@ -48,6 +48,11 @@ var testGetClickhouseColumnNameData = []struct {
AttributeKey: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true},
ExpectedColumnName: "trace_id",
},
{
Name: "name with - ",
AttributeKey: v3.AttributeKey{Key: "test-attr", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true},
ExpectedColumnName: "`attribute_string_test-attr`",
},
}
func TestGetClickhouseColumnName(t *testing.T) {
@@ -131,7 +136,7 @@ var timeSeriesFilterQueryData = []struct {
{Key: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}, Value: "john", Operator: "="},
{Key: v3.AttributeKey{Key: "k8s_namespace", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "my_service", Operator: "!="},
}},
ExpectedFilter: "attribute_string_user_name = 'john' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] != 'my_service'",
ExpectedFilter: "`attribute_string_user_name` = 'john' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] != 'my_service'",
},
{
Name: "Test like",
@@ -194,7 +199,7 @@ var timeSeriesFilterQueryData = []struct {
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}, Value: "host: \"(?P<host>\\S+)\"", Operator: "regex"},
}},
ExpectedFilter: "match(attribute_string_host, 'host: \"(?P<host>\\\\S+)\"')",
ExpectedFilter: "match(`attribute_string_host`, 'host: \"(?P<host>\\\\S+)\"')",
},
{
Name: "Test not regex",
@@ -217,7 +222,7 @@ var timeSeriesFilterQueryData = []struct {
{Key: v3.AttributeKey{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "102.", Operator: "ncontains"},
}},
GroupBy: []v3.AttributeKey{{Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}},
ExpectedFilter: "attributes_string_value[indexOf(attributes_string_key, 'host')] NOT ILIKE '%102.%' AND attribute_string_host_exists=true",
ExpectedFilter: "attributes_string_value[indexOf(attributes_string_key, 'host')] NOT ILIKE '%102.%' AND `attribute_string_host_exists`=true",
},
{
Name: "Wrong data",
@@ -266,14 +271,14 @@ var timeSeriesFilterQueryData = []struct {
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
{Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true}, Operator: "exists"},
}},
ExpectedFilter: "attribute_string_method_exists=true",
ExpectedFilter: "`attribute_string_method_exists`=true",
},
{
Name: "Test nexists on materiazlied column",
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
{Key: v3.AttributeKey{Key: "status", DataType: v3.AttributeKeyDataTypeInt64, Type: v3.AttributeKeyTypeTag, IsColumn: true}, Operator: "nexists"},
}},
ExpectedFilter: "attribute_int64_status_exists=false",
ExpectedFilter: "`attribute_int64_status_exists`=false",
},
// add new tests
}
@@ -368,7 +373,7 @@ var testBuildLogsQueryData = []struct {
OrderBy: []v3.OrderBy{{ColumnName: "#SIGNOZ_VALUE", Order: "ASC"}},
},
TableName: "logs",
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attribute_string_name))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND attribute_string_name_exists=true group by ts order by value ASC",
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(`attribute_string_name`))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND `attribute_string_name_exists`=true group by ts order by value ASC",
},
{
Name: "Test aggregate count distinct on non selected field",
@@ -421,9 +426,9 @@ var testBuildLogsQueryData = []struct {
},
TableName: "logs",
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attribute_string_host$$name as `host.name`, toFloat64(count(distinct(attribute_string_method$$name))) as value" +
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, `attribute_string_host$$name` as `host.name`, toFloat64(count(distinct(`attribute_string_method$$name`))) as value" +
" from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
"AND attribute_string_host$$name_exists=true AND attribute_string_method$$name_exists=true " +
"AND `attribute_string_host$$name_exists`=true AND `attribute_string_method$$name_exists`=true " +
"group by `host.name`,ts " +
"order by `host.name` ASC",
},
@@ -449,11 +454,11 @@ var testBuildLogsQueryData = []struct {
TableName: "logs",
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
" attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, " +
"toFloat64(count(distinct(attribute_string_name))) as value from signoz_logs.distributed_logs " +
"toFloat64(count(distinct(`attribute_string_name`))) as value from signoz_logs.distributed_logs " +
"where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
"AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND resources_string_value[indexOf(resources_string_key, 'x')] != 'abc' " +
"AND has(attributes_string_key, 'method') " +
"AND attribute_string_name_exists=true " +
"AND `attribute_string_name_exists`=true " +
"group by `method`,ts " +
"order by `method` ASC",
},
@@ -480,12 +485,12 @@ var testBuildLogsQueryData = []struct {
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
" attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, " +
"resources_string_value[indexOf(resources_string_key, 'x')] as `x`, " +
"toFloat64(count(distinct(attribute_string_name))) as value from signoz_logs.distributed_logs " +
"toFloat64(count(distinct(`attribute_string_name`))) as value from signoz_logs.distributed_logs " +
"where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
"AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND resources_string_value[indexOf(resources_string_key, 'x')] != 'abc' " +
"AND has(attributes_string_key, 'method') " +
"AND has(resources_string_key, 'x') " +
"AND attribute_string_name_exists=true " +
"AND `attribute_string_name_exists`=true " +
"group by `method`,`x`,ts " +
"order by `method` ASC,`x` ASC",
},
@@ -540,12 +545,12 @@ var testBuildLogsQueryData = []struct {
TableName: "logs",
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
" attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, " +
"sum(attribute_float64_bytes) as value " +
"sum(`attribute_float64_bytes`) as value " +
"from signoz_logs.distributed_logs " +
"where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
"AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' " +
"AND has(attributes_string_key, 'method') " +
"AND attribute_float64_bytes_exists=true " +
"AND `attribute_float64_bytes_exists`=true " +
"group by `method`,ts " +
"order by `method` ASC",
},
@@ -570,12 +575,12 @@ var testBuildLogsQueryData = []struct {
TableName: "logs",
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
" attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, " +
"min(attribute_float64_bytes) as value " +
"min(`attribute_float64_bytes`) as value " +
"from signoz_logs.distributed_logs " +
"where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
"AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' " +
"AND has(attributes_string_key, 'method') " +
"AND attribute_float64_bytes_exists=true " +
"AND `attribute_float64_bytes_exists`=true " +
"group by `method`,ts " +
"order by `method` ASC",
},
@@ -600,12 +605,12 @@ var testBuildLogsQueryData = []struct {
TableName: "logs",
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
" attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, " +
"max(attribute_float64_bytes) as value " +
"max(`attribute_float64_bytes`) as value " +
"from signoz_logs.distributed_logs " +
"where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
"AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' " +
"AND has(attributes_string_key, 'method') " +
"AND attribute_float64_bytes_exists=true " +
"AND `attribute_float64_bytes_exists`=true " +
"group by `method`,ts " +
"order by `method` ASC",
},
@@ -627,11 +632,11 @@ var testBuildLogsQueryData = []struct {
TableName: "logs",
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts," +
" attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`, " +
"quantile(0.05)(attribute_float64_bytes) as value " +
"quantile(0.05)(`attribute_float64_bytes`) as value " +
"from signoz_logs.distributed_logs " +
"where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
"AND has(attributes_string_key, 'method') " +
"AND attribute_float64_bytes_exists=true " +
"AND `attribute_float64_bytes_exists`=true " +
"group by `method`,ts " +
"order by `method` ASC",
},
@@ -653,10 +658,10 @@ var testBuildLogsQueryData = []struct {
TableName: "logs",
PreferRPM: true,
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as `method`" +
", sum(attribute_float64_bytes)/1.000000 as value from signoz_logs.distributed_logs " +
", sum(`attribute_float64_bytes`)/1.000000 as value from signoz_logs.distributed_logs " +
"where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) " +
"AND has(attributes_string_key, 'method') " +
"AND attribute_float64_bytes_exists=true " +
"AND `attribute_float64_bytes_exists`=true " +
"group by `method`,ts order by `method` ASC",
},
{

View File

@@ -66,6 +66,19 @@ func parseGetTopOperationsRequest(r *http.Request) (*model.GetTopOperationsParam
return postData, nil
}
func parseRegisterEventRequest(r *http.Request) (*model.RegisterEventParams, error) {
var postData *model.RegisterEventParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
if postData.EventName == "" {
return nil, errors.New("eventName param missing in query")
}
return postData, nil
}
func parseMetricsTime(s string) (time.Time, error) {
if t, err := strconv.ParseFloat(s, 64); err == nil {
s, ns := math.Modf(t)

View File

@@ -554,10 +554,10 @@ var testLogsWithFormula = []struct {
},
},
ExpectedQuery: "SELECT A.`key1.1` as `key1.1`, A.`ts` as `ts`, A.value - B.value as value FROM (SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, " +
"attribute_bool_key1$$1 as `key1.1`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1702980884000000000 AND timestamp <= 1702984484000000000) AND " +
"attribute_bool_key_2 = true AND attribute_bool_key1$$1_exists=true group by `key1.1`,ts order by value DESC) as A INNER JOIN (SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), " +
"INTERVAL 60 SECOND) AS ts, attribute_bool_key1$$1 as `key1.1`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1702980884000000000 AND " +
"timestamp <= 1702984484000000000) AND attributes_bool_value[indexOf(attributes_bool_key, 'key_1')] = true AND attribute_bool_key1$$1_exists=true group by `key1.1`,ts order by value DESC) as B " +
"`attribute_bool_key1$$1` as `key1.1`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1702980884000000000 AND timestamp <= 1702984484000000000) AND " +
"`attribute_bool_key_2` = true AND `attribute_bool_key1$$1_exists`=true group by `key1.1`,ts order by value DESC) as A INNER JOIN (SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), " +
"INTERVAL 60 SECOND) AS ts, `attribute_bool_key1$$1` as `key1.1`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1702980884000000000 AND " +
"timestamp <= 1702984484000000000) AND attributes_bool_value[indexOf(attributes_bool_key, 'key_1')] = true AND `attribute_bool_key1$$1_exists`=true group by `key1.1`,ts order by value DESC) as B " +
"ON A.`key1.1` = B.`key1.1` AND A.`ts` = B.`ts`",
},
}

View File

@@ -452,7 +452,7 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
data["tracesUsed"] = signozTracesUsed
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail, true, false)
}
}
return data, true
@@ -496,7 +496,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
if _, ok := telemetry.EnabledPaths()[path]; ok {
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail, true, false)
}
}
// }

View File

@@ -89,7 +89,7 @@ func Invite(ctx context.Context, req *model.InviteRequest) (*model.InviteRespons
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER_INVITATION_SENT, map[string]interface{}{
"invited user email": req.Email,
}, au.Email)
}, au.Email, true, false)
// send email if SMTP is enabled
if os.Getenv("SMTP_ENABLED") == "true" && req.FrontendBaseUrl != "" {
@@ -404,7 +404,7 @@ func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword b
}
telemetry.GetInstance().IdentifyUser(user)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER_INVITATION_ACCEPTED, nil, req.Email)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER_INVITATION_ACCEPTED, nil, req.Email, true, false)
return user, nil
}

View File

@@ -203,7 +203,7 @@ func (mds *ModelDaoSqlite) CreateUser(ctx context.Context,
}
telemetry.GetInstance().IdentifyUser(user)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER, data, user.Email)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER, data, user.Email, true, false)
return user, nil
}

View File

@@ -164,6 +164,11 @@ type GetTopOperationsParams struct {
Limit int `json:"limit"`
}
type RegisterEventParams struct {
EventName string `json:"eventName"`
Attributes map[string]interface{} `json:"attributes"`
}
type GetUsageParams struct {
StartTime string
EndTime string

View File

@@ -197,7 +197,7 @@ func createTelemetry() {
data := map[string]interface{}{}
telemetry.SetTelemetryEnabled(constants.IsTelemetryEnabled())
telemetry.SendEvent(TELEMETRY_EVENT_HEART_BEAT, data, "")
telemetry.SendEvent(TELEMETRY_EVENT_HEART_BEAT, data, "", true, false)
ticker := time.NewTicker(HEART_BEAT_DURATION)
activeUserTicker := time.NewTicker(ACTIVE_USER_DURATION)
@@ -231,7 +231,12 @@ func createTelemetry() {
if (telemetry.activeUser["traces"] != 0) || (telemetry.activeUser["metrics"] != 0) || (telemetry.activeUser["logs"] != 0) {
telemetry.activeUser["any"] = 1
}
telemetry.SendEvent(TELEMETRY_EVENT_ACTIVE_USER, map[string]interface{}{"traces": telemetry.activeUser["traces"], "metrics": telemetry.activeUser["metrics"], "logs": telemetry.activeUser["logs"], "any": telemetry.activeUser["any"]}, "")
telemetry.SendEvent(TELEMETRY_EVENT_ACTIVE_USER, map[string]interface{}{
"traces": telemetry.activeUser["traces"],
"metrics": telemetry.activeUser["metrics"],
"logs": telemetry.activeUser["logs"],
"any": telemetry.activeUser["any"]},
"", true, false)
telemetry.activeUser = map[string]int8{"traces": 0, "metrics": 0, "logs": 0, "any": 0}
case <-ticker.C:
@@ -239,17 +244,23 @@ func createTelemetry() {
tagsInfo, _ := telemetry.reader.GetTagsInfoInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION)
if len(tagsInfo.Env) != 0 {
telemetry.SendEvent(TELEMETRY_EVENT_ENVIRONMENT, map[string]interface{}{"value": tagsInfo.Env}, "")
telemetry.SendEvent(TELEMETRY_EVENT_ENVIRONMENT, map[string]interface{}{"value": tagsInfo.Env}, "", true, false)
}
for language, _ := range tagsInfo.Languages {
telemetry.SendEvent(TELEMETRY_EVENT_LANGUAGE, map[string]interface{}{"language": language}, "")
languages := []string{}
for language := range tagsInfo.Languages {
languages = append(languages, language)
}
for service, _ := range tagsInfo.Services {
telemetry.SendEvent(TELEMETRY_EVENT_SERVICE, map[string]interface{}{"serviceName": service}, "")
if len(languages) > 0 {
telemetry.SendEvent(TELEMETRY_EVENT_LANGUAGE, map[string]interface{}{"language": languages}, "", true, false)
}
services := []string{}
for service := range tagsInfo.Services {
services = append(services, service)
}
if len(services) > 0 {
telemetry.SendEvent(TELEMETRY_EVENT_SERVICE, map[string]interface{}{"serviceName": services}, "", true, false)
}
totalSpans, _ := telemetry.reader.GetTotalSpans(context.Background())
totalLogs, _ := telemetry.reader.GetTotalLogs(context.Background())
spansInLastHeartBeatInterval, _ := telemetry.reader.GetSpansInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION)
@@ -280,7 +291,7 @@ func createTelemetry() {
for key, value := range tsInfo {
data[key] = value
}
telemetry.SendEvent(TELEMETRY_EVENT_HEART_BEAT, data, "")
telemetry.SendEvent(TELEMETRY_EVENT_HEART_BEAT, data, "", true, false)
alertsInfo, err := telemetry.reader.GetAlertsInfo(context.Background())
if err == nil {
@@ -307,18 +318,18 @@ func createTelemetry() {
}
// send event only if there are dashboards or alerts or channels
if dashboardsInfo.TotalDashboards > 0 || alertsInfo.TotalAlerts > 0 || len(*channels) > 0 || savedViewsInfo.TotalSavedViews > 0 {
telemetry.SendEvent(TELEMETRY_EVENT_DASHBOARDS_ALERTS, dashboardsAlertsData, "")
telemetry.SendEvent(TELEMETRY_EVENT_DASHBOARDS_ALERTS, dashboardsAlertsData, "", true, false)
}
}
}
}
}
if err != nil {
telemetry.SendEvent(TELEMETRY_EVENT_DASHBOARDS_ALERTS, map[string]interface{}{"error": err.Error()}, "")
telemetry.SendEvent(TELEMETRY_EVENT_DASHBOARDS_ALERTS, map[string]interface{}{"error": err.Error()}, "", true, false)
}
getDistributedInfoInLastHeartBeatInterval, _ := telemetry.reader.GetDistributedInfoInLastHeartBeatInterval(context.Background())
telemetry.SendEvent(TELEMETRY_EVENT_DISTRIBUTED, getDistributedInfoInLastHeartBeatInterval, "")
telemetry.SendEvent(TELEMETRY_EVENT_DISTRIBUTED, getDistributedInfoInLastHeartBeatInterval, "", true, false)
}
}
}()
@@ -426,7 +437,7 @@ func (a *Telemetry) checkEvents(event string) bool {
return sendEvent
}
func (a *Telemetry) SendEvent(event string, data map[string]interface{}, userEmail string, opts ...bool) {
func (a *Telemetry) SendEvent(event string, data map[string]interface{}, userEmail string, rateLimitFlag bool, viaEventsAPI bool) {
// ignore telemetry for default user
if userEmail == DEFAULT_CLOUD_EMAIL || a.GetUserEmail() == DEFAULT_CLOUD_EMAIL {
@@ -436,10 +447,6 @@ func (a *Telemetry) SendEvent(event string, data map[string]interface{}, userEma
if userEmail != "" {
a.SetUserEmail(userEmail)
}
rateLimitFlag := true
if len(opts) > 0 {
rateLimitFlag = opts[0]
}
if !a.isTelemetryEnabled() {
return
@@ -485,7 +492,7 @@ func (a *Telemetry) SendEvent(event string, data map[string]interface{}, userEma
// check if event is part of SAAS_EVENTS_LIST
_, isSaaSEvent := SAAS_EVENTS_LIST[event]
if a.saasOperator != nil && a.GetUserEmail() != "" && isSaaSEvent {
if a.saasOperator != nil && a.GetUserEmail() != "" && (isSaaSEvent || viaEventsAPI) {
a.saasOperator.Enqueue(analytics.Track{
Event: event,
UserId: a.GetUserEmail(),

View File

@@ -141,9 +141,14 @@ func TestLogPipelinesForInstalledSignozIntegrations(t *testing.T) {
break
}
}
require.NotNil(testAvailableIntegration)
if testAvailableIntegration == nil {
// None of the built in integrations include a pipeline right now.
return
}
// Installing an integration should add its pipelines to pipelines list
require.NotNil(testAvailableIntegration)
require.False(testAvailableIntegration.IsInstalled)
integrationsTB.RequestQSToInstallIntegration(
testAvailableIntegration.Id, map[string]interface{}{},

View File

@@ -243,7 +243,7 @@ func GetClickhouseColumnName(typeName string, dataType, field string) string {
// if name contains . replace it with `$$`
field = strings.ReplaceAll(field, ".", "$$")
colName := fmt.Sprintf("%s_%s_%s", strings.ToLower(typeName), strings.ToLower(dataType), field)
colName := fmt.Sprintf("`%s_%s_%s`", strings.ToLower(typeName), strings.ToLower(dataType), field)
return colName
}