mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-19 23:10:25 +01:00
Compare commits
22 Commits
v0.42.0
...
v0.42.3-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c99a112dc7 | ||
|
|
e4d9c4e239 | ||
|
|
781732f25a | ||
|
|
77e55a0ec9 | ||
|
|
a34c59762b | ||
|
|
397da5857f | ||
|
|
43ceb052d8 | ||
|
|
6eced60bf5 | ||
|
|
7c2f5352d2 | ||
|
|
e6e377beff | ||
|
|
6da9de6591 | ||
|
|
7549aee656 | ||
|
|
da4a6266c5 | ||
|
|
6ac938f2a6 | ||
|
|
990fc83269 | ||
|
|
5d5ff47d5e | ||
|
|
9f30bba9a8 | ||
|
|
6014bb76b6 | ||
|
|
e25b54f86a | ||
|
|
5959963b9d | ||
|
|
5c2a9e8362 | ||
|
|
aad840da59 |
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -29,4 +29,5 @@ export enum QueryParams {
|
||||
expandedWidgetId = 'expandedWidgetId',
|
||||
integration = 'integration',
|
||||
pagination = 'pagination',
|
||||
relativeTime = 'relativeTime',
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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, '');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -1 +0,0 @@
|
||||
### Configure otel collector
|
||||
@@ -1 +0,0 @@
|
||||
### Prepare nginx for observability
|
||||
@@ -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.
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -35,7 +35,7 @@ receivers:
|
||||
- LOG
|
||||
- NOTICE
|
||||
- DETAIL
|
||||
warning: WARNING
|
||||
warn: WARNING
|
||||
error: ERROR
|
||||
fatal:
|
||||
- FATAL
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -29,7 +29,7 @@ receivers:
|
||||
info:
|
||||
- '-'
|
||||
- '*'
|
||||
warning: '#'
|
||||
warn: '#'
|
||||
on_error: send
|
||||
- type: move
|
||||
if: attributes.message != nil
|
||||
|
||||
@@ -28,9 +28,7 @@
|
||||
],
|
||||
"assets": {
|
||||
"logs": {
|
||||
"pipelines": [
|
||||
"file://assets/pipelines/log-parser.json"
|
||||
]
|
||||
"pipelines": []
|
||||
},
|
||||
"dashboards": [
|
||||
"file://assets/dashboards/overview.json"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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), "`")))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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{}{},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user