mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-08 02:39:55 +00:00
Compare commits
26 Commits
help-suppo
...
issue_2705
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f43292c10b | ||
|
|
a2492b0135 | ||
|
|
eb8ca5a7ca | ||
|
|
80133240ca | ||
|
|
7d7d112f40 | ||
|
|
add2d19614 | ||
|
|
adfe20e88a | ||
|
|
d3b83f5a41 | ||
|
|
77eba9a558 | ||
|
|
43e73e06fe | ||
|
|
840d8b2e49 | ||
|
|
df751c7f38 | ||
|
|
cd07c743b6 | ||
|
|
46e6c34e51 | ||
|
|
42f7905b3b | ||
|
|
a6e68c6519 | ||
|
|
c7e3e6dc4e | ||
|
|
9194ab08b6 | ||
|
|
ea51320e4f | ||
|
|
436009ec20 | ||
|
|
785a872ee0 | ||
|
|
9ab3a03bfe | ||
|
|
8a3688b74d | ||
|
|
5a686f3be1 | ||
|
|
da31b2f7df | ||
|
|
a25189eca7 |
3
.github/workflows/staging-deployment.yaml
vendored
3
.github/workflows/staging-deployment.yaml
vendored
@@ -30,6 +30,7 @@ jobs:
|
||||
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
|
||||
run: |
|
||||
read -r -d '' COMMAND <<EOF || true
|
||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||
@@ -51,4 +52,4 @@ jobs:
|
||||
make build-frontend-amd64
|
||||
make run-testing
|
||||
EOF
|
||||
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||
|
||||
3
.github/workflows/testing-deployment.yaml
vendored
3
.github/workflows/testing-deployment.yaml
vendored
@@ -30,6 +30,7 @@ jobs:
|
||||
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
|
||||
run: |
|
||||
read -r -d '' COMMAND <<EOF || true
|
||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||
@@ -52,4 +53,4 @@ jobs:
|
||||
make build-frontend-amd64
|
||||
make run-testing
|
||||
EOF
|
||||
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||
|
||||
@@ -110,6 +110,8 @@
|
||||
"react-syntax-highlighter": "15.5.0",
|
||||
"react-use": "^17.3.2",
|
||||
"react-virtuoso": "4.0.3",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rehype-raw": "7.0.0",
|
||||
|
||||
@@ -3,7 +3,7 @@ const apiV1 = '/api/v1/';
|
||||
export const apiV2 = '/api/v2/';
|
||||
export const apiV3 = '/api/v3/';
|
||||
export const apiV4 = '/api/v4/';
|
||||
export const gatewayApiV1 = '/api/gateway/v1';
|
||||
export const apiAlertManager = '/api/alertmanager';
|
||||
export const gatewayApiV1 = '/api/gateway/v1/';
|
||||
export const apiAlertManager = '/api/alertmanager/';
|
||||
|
||||
export default apiV1;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
|
||||
import VirtuosoOverlayScrollbar from 'components/VirtuosoOverlayScrollbar/VirtuosoOverlayScrollbar';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { PartialOptions } from 'overlayscrollbars';
|
||||
import { CSSProperties, ReactElement, useMemo } from 'react';
|
||||
|
||||
type Props = {
|
||||
children: ReactElement;
|
||||
isVirtuoso?: boolean;
|
||||
style?: CSSProperties;
|
||||
options?: PartialOptions;
|
||||
};
|
||||
|
||||
function OverlayScrollbar({
|
||||
children,
|
||||
isVirtuoso,
|
||||
style,
|
||||
options: customOptions,
|
||||
}: Props): any {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const options = useMemo(
|
||||
() =>
|
||||
({
|
||||
scrollbars: {
|
||||
autoHide: 'scroll',
|
||||
theme: isDarkMode ? 'os-theme-light' : 'os-theme-dark',
|
||||
},
|
||||
...(customOptions || {}),
|
||||
} as PartialOptions),
|
||||
[customOptions, isDarkMode],
|
||||
);
|
||||
|
||||
if (isVirtuoso) {
|
||||
return (
|
||||
<VirtuosoOverlayScrollbar style={style} options={options}>
|
||||
{children}
|
||||
</VirtuosoOverlayScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TypicalOverlayScrollbar style={style} options={options}>
|
||||
{children}
|
||||
</TypicalOverlayScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
OverlayScrollbar.defaultProps = {
|
||||
isVirtuoso: false,
|
||||
style: {},
|
||||
options: {},
|
||||
};
|
||||
|
||||
export default OverlayScrollbar;
|
||||
@@ -0,0 +1,31 @@
|
||||
import './typicalOverlayScrollbar.scss';
|
||||
|
||||
import { PartialOptions } from 'overlayscrollbars';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import { CSSProperties, ReactElement } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactElement;
|
||||
style?: CSSProperties;
|
||||
options?: PartialOptions;
|
||||
}
|
||||
|
||||
export default function TypicalOverlayScrollbar({
|
||||
children,
|
||||
style,
|
||||
options,
|
||||
}: Props): ReturnType<typeof OverlayScrollbarsComponent> {
|
||||
return (
|
||||
<OverlayScrollbarsComponent
|
||||
defer
|
||||
options={options}
|
||||
style={style}
|
||||
className="overlay-scrollbar"
|
||||
data-overlayscrollbars-initialize
|
||||
>
|
||||
{children}
|
||||
</OverlayScrollbarsComponent>
|
||||
);
|
||||
}
|
||||
|
||||
TypicalOverlayScrollbar.defaultProps = { style: {}, options: {} };
|
||||
@@ -0,0 +1,3 @@
|
||||
.overlay-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import './virtuosoOverlayScrollbar.scss';
|
||||
|
||||
import useInitializeOverlayScrollbar from 'hooks/useInitializeOverlayScrollbar/useInitializeOverlayScrollbar';
|
||||
import { PartialOptions } from 'overlayscrollbars';
|
||||
import React, { CSSProperties, ReactElement } from 'react';
|
||||
|
||||
interface VirtuosoOverlayScrollbarProps {
|
||||
children: ReactElement;
|
||||
style?: CSSProperties;
|
||||
options: PartialOptions;
|
||||
}
|
||||
|
||||
export default function VirtuosoOverlayScrollbar({
|
||||
children,
|
||||
style,
|
||||
options,
|
||||
}: VirtuosoOverlayScrollbarProps): JSX.Element {
|
||||
const { rootRef, setScroller } = useInitializeOverlayScrollbar(options);
|
||||
|
||||
const enhancedChild = React.cloneElement(children, {
|
||||
scrollerRef: setScroller,
|
||||
'data-overlayscrollbars-initialize': true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
data-overlayscrollbars-initialize
|
||||
ref={rootRef}
|
||||
className="overlay-scroll-wrapper"
|
||||
style={style}
|
||||
>
|
||||
{enhancedChild}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
VirtuosoOverlayScrollbar.defaultProps = { style: {} };
|
||||
@@ -0,0 +1,5 @@
|
||||
.overlay-scroll-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { ColumnType, TablePaginationConfig } from 'antd/es/table';
|
||||
import { FilterValue, SorterResult } from 'antd/es/table/interface';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { FilterConfirmProps } from 'antd/lib/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import getAll from 'api/errors/getAll';
|
||||
import getErrorCounts from 'api/errors/getErrorCounts';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
@@ -23,7 +24,8 @@ import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -410,6 +412,26 @@ function AllErrors(): JSX.Element {
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (
|
||||
!logEventCalledRef.current &&
|
||||
!isUndefined(errorCountResponse.data?.payload)
|
||||
) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === 'resource_deployment_environment',
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('Exception: List page visited', {
|
||||
numberOfExceptions: errorCountResponse?.data?.payload,
|
||||
selectedEnvironments,
|
||||
resourceAttributeUsed: !!queries?.length,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [errorCountResponse.data?.payload]);
|
||||
|
||||
return (
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
.app-content {
|
||||
width: calc(100% - 64px);
|
||||
overflow: auto;
|
||||
z-index: 0;
|
||||
|
||||
.content-container {
|
||||
|
||||
@@ -9,6 +9,7 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||
import getUserVersion from 'api/user/getVersion';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
||||
import ROUTES from 'constants/routes';
|
||||
import SideNav from 'container/SideNav';
|
||||
@@ -303,24 +304,29 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
)}
|
||||
<div className={cx('app-content', collapsed ? 'collapsed' : '')}>
|
||||
<div
|
||||
className={cx('app-content', collapsed ? 'collapsed' : '')}
|
||||
data-overlayscrollbars-initialize
|
||||
>
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<LayoutContent>
|
||||
<ChildrenContainer
|
||||
style={{
|
||||
margin:
|
||||
isLogsView() ||
|
||||
isTracesView() ||
|
||||
isDashboardView() ||
|
||||
isDashboardWidgetView() ||
|
||||
isDashboardListView()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
}}
|
||||
>
|
||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||
{children}
|
||||
</ChildrenContainer>
|
||||
<LayoutContent data-overlayscrollbars-initialize>
|
||||
<OverlayScrollbar>
|
||||
<ChildrenContainer
|
||||
style={{
|
||||
margin:
|
||||
isLogsView() ||
|
||||
isTracesView() ||
|
||||
isDashboardView() ||
|
||||
isDashboardWidgetView() ||
|
||||
isDashboardListView()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
}}
|
||||
>
|
||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||
{children}
|
||||
</ChildrenContainer>
|
||||
</OverlayScrollbar>
|
||||
</LayoutContent>
|
||||
</Sentry.ErrorBoundary>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@ export const Layout = styled(LayoutComponent)`
|
||||
`;
|
||||
|
||||
export const LayoutContent = styled(LayoutComponent.Content)`
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
|
||||
@@ -449,8 +449,8 @@ function CreateAlertChannels({
|
||||
const result = await functionToCall();
|
||||
logEvent('Alert Channel: Save channel', {
|
||||
type: value,
|
||||
sendResolvedAlert: selectedConfig.send_resolved,
|
||||
name: selectedConfig.name,
|
||||
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||
name: selectedConfig?.name,
|
||||
new: 'true',
|
||||
status: result?.status,
|
||||
statusMessage: result?.statusMessage,
|
||||
@@ -530,8 +530,8 @@ function CreateAlertChannels({
|
||||
|
||||
logEvent('Alert Channel: Test notification', {
|
||||
type: channelType,
|
||||
sendResolvedAlert: selectedConfig.send_resolved,
|
||||
name: selectedConfig.name,
|
||||
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||
name: selectedConfig?.name,
|
||||
new: 'true',
|
||||
status:
|
||||
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
|
||||
|
||||
@@ -370,8 +370,8 @@ function EditAlertChannels({
|
||||
}
|
||||
logEvent('Alert Channel: Save channel', {
|
||||
type: value,
|
||||
sendResolvedAlert: selectedConfig.send_resolved,
|
||||
name: selectedConfig.name,
|
||||
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||
name: selectedConfig?.name,
|
||||
new: 'false',
|
||||
status: result?.status,
|
||||
statusMessage: result?.statusMessage,
|
||||
@@ -441,8 +441,8 @@ function EditAlertChannels({
|
||||
}
|
||||
logEvent('Alert Channel: Test notification', {
|
||||
type: channelType,
|
||||
sendResolvedAlert: selectedConfig.send_resolved,
|
||||
name: selectedConfig.name,
|
||||
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||
name: selectedConfig?.name,
|
||||
new: 'false',
|
||||
status:
|
||||
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
|
||||
|
||||
@@ -1,8 +1,34 @@
|
||||
import './EmptyLogsSearch.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { DataSource, PanelTypeKeys } from 'types/common/queryBuilder';
|
||||
|
||||
export default function EmptyLogsSearch({
|
||||
dataSource,
|
||||
panelType,
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
panelType: PanelTypeKeys;
|
||||
}): JSX.Element {
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
logEvent('Traces Explorer: No results', {
|
||||
panelType,
|
||||
});
|
||||
} else if (dataSource === DataSource.LOGS) {
|
||||
logEvent('Logs Explorer: No results', {
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
export default function EmptyLogsSearch(): JSX.Element {
|
||||
return (
|
||||
<div className="empty-logs-search-container">
|
||||
<div className="empty-logs-search-container-content">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import './styles.scss';
|
||||
|
||||
import { Button, Divider, Space, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import getNextPrevId from 'api/errors/getNextPrevId';
|
||||
import Editor from 'components/Editor';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
@@ -9,8 +10,9 @@ import dayjs from 'dayjs';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -111,9 +113,29 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
}));
|
||||
|
||||
const onClickTraceHandler = (): void => {
|
||||
logEvent('Exception: Navigate to trace detail page', {
|
||||
groupId: errorDetail?.groupID,
|
||||
spanId: errorDetail.spanID,
|
||||
traceId: errorDetail.traceID,
|
||||
exceptionId: errorDetail?.errorId,
|
||||
});
|
||||
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
||||
};
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||
logEvent('Exception: Detail page visited', {
|
||||
groupId: errorDetail?.groupID,
|
||||
spanId: errorDetail.spanID,
|
||||
traceId: errorDetail.traceID,
|
||||
exceptionId: errorDetail?.errorId,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography>{errorDetail.exceptionType}</Typography>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import axios from 'axios';
|
||||
import cx from 'classnames';
|
||||
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
||||
@@ -93,7 +94,23 @@ function ExplorerOptions({
|
||||
setIsExport(value);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
currentQuery,
|
||||
panelType,
|
||||
isStagedQueryUpdated,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const handleSaveViewModalToggle = (): void => {
|
||||
if (sourcepage === DataSource.TRACES) {
|
||||
logEvent('Traces Explorer: Save view clicked', {
|
||||
panelType,
|
||||
});
|
||||
} else if (sourcepage === DataSource.LOGS) {
|
||||
logEvent('Logs Explorer: Save view clicked', {
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
setIsSaveModalOpen(!isSaveModalOpen);
|
||||
};
|
||||
|
||||
@@ -104,11 +121,21 @@ function ExplorerOptions({
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const onCreateAlertsHandler = useCallback(() => {
|
||||
if (sourcepage === DataSource.TRACES) {
|
||||
logEvent('Traces Explorer: Create alert', {
|
||||
panelType,
|
||||
});
|
||||
} else if (sourcepage === DataSource.LOGS) {
|
||||
logEvent('Logs Explorer: Create alert', {
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
history.push(
|
||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||
JSON.stringify(query),
|
||||
)}`,
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [history, query]);
|
||||
|
||||
const onCancel = (value: boolean) => (): void => {
|
||||
@@ -116,6 +143,15 @@ function ExplorerOptions({
|
||||
};
|
||||
|
||||
const onAddToDashboard = (): void => {
|
||||
if (sourcepage === DataSource.TRACES) {
|
||||
logEvent('Traces Explorer: Add to dashboard clicked', {
|
||||
panelType,
|
||||
});
|
||||
} else if (sourcepage === DataSource.LOGS) {
|
||||
logEvent('Logs Explorer: Add to dashboard clicked', {
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
setIsExport(true);
|
||||
};
|
||||
|
||||
@@ -127,13 +163,6 @@ function ExplorerOptions({
|
||||
refetch: refetchAllView,
|
||||
} = useGetAllViews(sourcepage);
|
||||
|
||||
const {
|
||||
currentQuery,
|
||||
panelType,
|
||||
isStagedQueryUpdated,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
||||
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
@@ -224,6 +253,17 @@ function ExplorerOptions({
|
||||
onMenuItemSelectHandler({
|
||||
key: option.key,
|
||||
});
|
||||
if (sourcepage === DataSource.TRACES) {
|
||||
logEvent('Traces Explorer: Select view', {
|
||||
panelType,
|
||||
viewName: option?.value,
|
||||
});
|
||||
} else if (sourcepage === DataSource.LOGS) {
|
||||
logEvent('Logs Explorer: Select view', {
|
||||
panelType,
|
||||
viewName: option?.value,
|
||||
});
|
||||
}
|
||||
if (ref.current) {
|
||||
ref.current.blur();
|
||||
}
|
||||
@@ -259,6 +299,17 @@ function ExplorerOptions({
|
||||
viewName: newViewName,
|
||||
setNewViewName,
|
||||
});
|
||||
if (sourcepage === DataSource.TRACES) {
|
||||
logEvent('Traces Explorer: Save view successful', {
|
||||
panelType,
|
||||
viewName: newViewName,
|
||||
});
|
||||
} else if (sourcepage === DataSource.LOGS) {
|
||||
logEvent('Logs Explorer: Save view successful', {
|
||||
panelType,
|
||||
viewName: newViewName,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Remove this and move this to scss file
|
||||
@@ -499,7 +550,7 @@ function ExplorerOptions({
|
||||
|
||||
export interface ExplorerOptionsProps {
|
||||
isLoading?: boolean;
|
||||
onExport: (dashboard: Dashboard | null) => void;
|
||||
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
|
||||
query: Query | null;
|
||||
disabled: boolean;
|
||||
sourcepage: DataSource;
|
||||
|
||||
@@ -41,7 +41,7 @@ function ExportPanelContainer({
|
||||
} = useMutation(createDashboard, {
|
||||
onSuccess: (data) => {
|
||||
if (data.payload) {
|
||||
onExport(data?.payload);
|
||||
onExport(data?.payload, true);
|
||||
}
|
||||
refetch();
|
||||
},
|
||||
@@ -55,7 +55,7 @@ function ExportPanelContainer({
|
||||
({ uuid }) => uuid === selectedDashboardId,
|
||||
);
|
||||
|
||||
onExport(currentSelectedDashboard || null);
|
||||
onExport(currentSelectedDashboard || null, false);
|
||||
}, [data, selectedDashboardId, onExport]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
|
||||
@@ -40,7 +40,7 @@ function ExportPanel({
|
||||
|
||||
export interface ExportPanelProps {
|
||||
isLoading?: boolean;
|
||||
onExport: (dashboard: Dashboard | null) => void;
|
||||
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
|
||||
query: Query | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ function BasicInfo({
|
||||
if (!channels.loading && isNewRule) {
|
||||
logEvent('Alert: New alert creation page visited', {
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||
numberOfChannels: channels.payload?.length,
|
||||
numberOfChannels: channels?.payload?.length,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface ChartPreviewProps {
|
||||
userQueryKey?: string;
|
||||
allowSelectedIntervalForStepGen?: boolean;
|
||||
yAxisUnit: string;
|
||||
setQueryStatus?: (status: string) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
@@ -62,6 +63,7 @@ function ChartPreview({
|
||||
allowSelectedIntervalForStepGen = false,
|
||||
alertDef,
|
||||
yAxisUnit,
|
||||
setQueryStatus,
|
||||
}: ChartPreviewProps): JSX.Element | null {
|
||||
const { t } = useTranslation('alerts');
|
||||
const dispatch = useDispatch();
|
||||
@@ -149,10 +151,10 @@ function ChartPreview({
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
|
||||
if (setQueryStatus) setQueryStatus(queryResponse.status);
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
|
||||
}, [maxTime, minTime, globalSelectedInterval, queryResponse, setQueryStatus]);
|
||||
|
||||
if (queryResponse.data && graphType === PANEL_TYPES.BAR) {
|
||||
const sortedSeriesData = getSortedSeriesData(
|
||||
@@ -284,6 +286,7 @@ ChartPreview.defaultProps = {
|
||||
userQueryKey: '',
|
||||
allowSelectedIntervalForStepGen: false,
|
||||
alertDef: undefined,
|
||||
setQueryStatus: (): void => {},
|
||||
};
|
||||
|
||||
export default ChartPreview;
|
||||
|
||||
@@ -101,6 +101,7 @@ function FormAlertRules({
|
||||
const isNewRule = ruleId === 0;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [queryStatus, setQueryStatus] = useState<string>('');
|
||||
|
||||
// alertDef holds the form values to be posted
|
||||
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
||||
@@ -523,6 +524,7 @@ function FormAlertRules({
|
||||
alertDef={alertDef}
|
||||
yAxisUnit={yAxisUnit || ''}
|
||||
graphType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
setQueryStatus={setQueryStatus}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -540,6 +542,7 @@ function FormAlertRules({
|
||||
selectedInterval={globalSelectedInterval}
|
||||
yAxisUnit={yAxisUnit || ''}
|
||||
graphType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
setQueryStatus={setQueryStatus}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -665,7 +668,8 @@ function FormAlertRules({
|
||||
disabled={
|
||||
isAlertNameMissing ||
|
||||
isAlertAvailableToSave ||
|
||||
!isChannelConfigurationValid
|
||||
!isChannelConfigurationValid ||
|
||||
queryStatus === 'error'
|
||||
}
|
||||
>
|
||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||
@@ -674,7 +678,11 @@ function FormAlertRules({
|
||||
|
||||
<ActionButton
|
||||
loading={loading || false}
|
||||
disabled={isAlertNameMissing || !isChannelConfigurationValid}
|
||||
disabled={
|
||||
isAlertNameMissing ||
|
||||
!isChannelConfigurationValid ||
|
||||
queryStatus === 'error'
|
||||
}
|
||||
type="default"
|
||||
onClick={onTestRuleHandler}
|
||||
>
|
||||
|
||||
@@ -124,6 +124,9 @@ const getSpanWithoutChildren = (
|
||||
value: span.value,
|
||||
event: span.event,
|
||||
hasError: span.hasError,
|
||||
spanKind: span.spanKind,
|
||||
statusCodeString: span.statusCodeString,
|
||||
statusMessage: span.statusMessage,
|
||||
});
|
||||
|
||||
export const isSpanPresentInSearchString = (
|
||||
|
||||
@@ -3,6 +3,7 @@ import './DashboardEmptyState.styles.scss';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import SettingsDrawer from 'container/NewDashboard/DashboardDescription/SettingsDrawer';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
@@ -36,6 +37,12 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(() => {
|
||||
handleToggleDashboardSlider(true);
|
||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [handleToggleDashboardSlider]);
|
||||
return (
|
||||
<section className="dashboard-empty-state">
|
||||
|
||||
@@ -14,6 +14,7 @@ import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||
@@ -113,6 +114,7 @@ function GridCardGraph({
|
||||
};
|
||||
}
|
||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||
const initialDataSource = updatedQuery.builder.queryData[0].dataSource;
|
||||
return {
|
||||
query: updatedQuery,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
@@ -123,6 +125,10 @@ function GridCardGraph({
|
||||
offset: 0,
|
||||
limit: updatedQuery.builder.queryData[0].limit || 0,
|
||||
},
|
||||
selectColumns:
|
||||
initialDataSource === DataSource.LOGS
|
||||
? widget.selectedLogFields
|
||||
: widget.selectedTracesFields,
|
||||
},
|
||||
fillGaps: widget.fillSpans,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.fullscreen-grid-container {
|
||||
overflow: auto;
|
||||
margin: 8px -8px;
|
||||
margin-right: 0;
|
||||
|
||||
.react-grid-layout {
|
||||
border: none !important;
|
||||
@@ -49,7 +50,7 @@
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: -webkit-fill-available;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import './GridCardLayout.styles.scss';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Form, Input, Modal, Typography } from 'antd';
|
||||
import { useForm } from 'antd/es/form/Form';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -15,7 +16,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { defaultTo, isUndefined } from 'lodash-es';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import {
|
||||
Check,
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FullScreen, FullScreenHandle } from 'react-full-screen';
|
||||
import { ItemCallback, Layout } from 'react-grid-layout';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -126,6 +127,18 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
setDashboardLayout(sortLayout(layouts));
|
||||
}, [layouts]);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||
logEvent('Dashboard Detail: Opened', {
|
||||
dashboardId: data.uuid,
|
||||
dashboardName: data.title,
|
||||
numberOfPanels: data.widgets?.length,
|
||||
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, [data]);
|
||||
const onSaveHandler = (): void => {
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
@@ -428,7 +441,11 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
return isDashboardEmpty ? (
|
||||
<DashboardEmptyState />
|
||||
) : (
|
||||
<FullScreen handle={handle} className="fullscreen-grid-container">
|
||||
<FullScreen
|
||||
handle={handle}
|
||||
className="fullscreen-grid-container"
|
||||
data-overlayscrollbars-initialize
|
||||
>
|
||||
<ReactGridLayout
|
||||
cols={12}
|
||||
rowHeight={45}
|
||||
|
||||
@@ -79,7 +79,7 @@ function WidgetHeader({
|
||||
);
|
||||
}, [widget.id, widget.panelTypes, widget.query]);
|
||||
|
||||
const onCreateAlertsHandler = useCreateAlerts(widget);
|
||||
const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView');
|
||||
|
||||
const onDownloadHandler = useCallback((): void => {
|
||||
const csv = unparse(tableProcessedDataRef.current);
|
||||
@@ -234,6 +234,7 @@ function WidgetHeader({
|
||||
)}
|
||||
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
||||
<MoreOutlined
|
||||
data-testid="widget-header-options"
|
||||
className={`widget-header-more-options ${
|
||||
parentHover ? 'widget-header-hover' : ''
|
||||
}`}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { createColumnsAndDataSource, getQueryLegend } from '../utils';
|
||||
import {
|
||||
createColumnsAndDataSource,
|
||||
getQueryLegend,
|
||||
sortFunction,
|
||||
} from '../utils';
|
||||
import {
|
||||
expectedOutputWithLegends,
|
||||
tableDataMultipleQueriesSuccessResponse,
|
||||
@@ -39,4 +43,88 @@ describe('Table Panel utils', () => {
|
||||
// should return undefined when legend not present
|
||||
expect(getQueryLegend(query, 'B')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('sorter function for table sorting', () => {
|
||||
let rowA: {
|
||||
A: string | number;
|
||||
timestamp: number;
|
||||
key: string;
|
||||
} = {
|
||||
A: 22.4,
|
||||
timestamp: 111111,
|
||||
key: '1111',
|
||||
};
|
||||
let rowB: {
|
||||
A: string | number;
|
||||
timestamp: number;
|
||||
key: string;
|
||||
} = {
|
||||
A: 'n/a',
|
||||
timestamp: 111112,
|
||||
key: '1112',
|
||||
};
|
||||
const item = {
|
||||
isValueColumn: true,
|
||||
name: 'A',
|
||||
queryName: 'A',
|
||||
};
|
||||
// A has value and value is considered bigger than n/a hence 1
|
||||
expect(sortFunction(rowA, rowB, item)).toBe(1);
|
||||
|
||||
rowA = {
|
||||
A: 'n/a',
|
||||
timestamp: 111111,
|
||||
key: '1111',
|
||||
};
|
||||
rowB = {
|
||||
A: 22.4,
|
||||
timestamp: 111112,
|
||||
key: '1112',
|
||||
};
|
||||
|
||||
// B has value and value is considered bigger than n/a hence -1
|
||||
expect(sortFunction(rowA, rowB, item)).toBe(-1);
|
||||
|
||||
rowA = {
|
||||
A: 11,
|
||||
timestamp: 111111,
|
||||
key: '1111',
|
||||
};
|
||||
rowB = {
|
||||
A: 22,
|
||||
timestamp: 111112,
|
||||
key: '1112',
|
||||
};
|
||||
|
||||
// A and B has value , since B > A hence A-B
|
||||
expect(sortFunction(rowA, rowB, item)).toBe(-11);
|
||||
|
||||
rowA = {
|
||||
A: 'read',
|
||||
timestamp: 111111,
|
||||
key: '1111',
|
||||
};
|
||||
rowB = {
|
||||
A: 'write',
|
||||
timestamp: 111112,
|
||||
key: '1112',
|
||||
};
|
||||
|
||||
// A and B are strings so A is smaller than B because r comes before w hence -1
|
||||
expect(sortFunction(rowA, rowB, item)).toBe(-1);
|
||||
|
||||
rowA = {
|
||||
A: 'n/a',
|
||||
timestamp: 111111,
|
||||
key: '1111',
|
||||
};
|
||||
rowB = {
|
||||
A: 'n/a',
|
||||
timestamp: 111112,
|
||||
key: '1112',
|
||||
};
|
||||
|
||||
// A and B are strings n/a , since both of them are same hence 0
|
||||
expect(sortFunction(rowA, rowB, item)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { ColumnsType, ColumnType } from 'antd/es/table';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
|
||||
@@ -105,6 +106,39 @@ export function getQueryLegend(
|
||||
return legend;
|
||||
}
|
||||
|
||||
export function sortFunction(
|
||||
a: RowData,
|
||||
b: RowData,
|
||||
item: {
|
||||
name: string;
|
||||
queryName: string;
|
||||
isValueColumn: boolean;
|
||||
},
|
||||
): number {
|
||||
// assumption :- number values is bigger than 'n/a'
|
||||
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
|
||||
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
|
||||
|
||||
// if both the values are numbers then return the difference here
|
||||
if (!isNaN(valueA) && !isNaN(valueB)) {
|
||||
return valueA - valueB;
|
||||
}
|
||||
|
||||
// if valueB is a number then make it bigger value
|
||||
if (isNaN(valueA) && !isNaN(valueB)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// if valueA is number make it the bigger value
|
||||
if (!isNaN(valueA) && isNaN(valueB)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// if both of them are strings do the localecompare
|
||||
return ((a[item.name] as string) || '').localeCompare(
|
||||
(b[item.name] as string) || '',
|
||||
);
|
||||
}
|
||||
export function createColumnsAndDataSource(
|
||||
data: TableData,
|
||||
currentQuery: Query,
|
||||
@@ -123,18 +157,7 @@ export function createColumnsAndDataSource(
|
||||
title: !isEmpty(legend) ? legend : item.name,
|
||||
width: QUERY_TABLE_CONFIG.width,
|
||||
render: renderColumnCell && renderColumnCell[item.name],
|
||||
sorter: (a: RowData, b: RowData): number => {
|
||||
const valueA = Number(a[`${item.name}_without_unit`] ?? a[item.name]);
|
||||
const valueB = Number(b[`${item.name}_without_unit`] ?? b[item.name]);
|
||||
|
||||
if (!isNaN(valueA) && !isNaN(valueB)) {
|
||||
return valueA - valueB;
|
||||
}
|
||||
|
||||
return ((a[item.name] as string) || '').localeCompare(
|
||||
(b[item.name] as string) || '',
|
||||
);
|
||||
},
|
||||
sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item),
|
||||
};
|
||||
|
||||
return [...acc, column];
|
||||
|
||||
@@ -49,9 +49,9 @@ export const alertActionLogEvent = (
|
||||
break;
|
||||
}
|
||||
logEvent('Alert: Action', {
|
||||
ruleId: record.id,
|
||||
ruleId: record?.id,
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[record.alertType as AlertTypes],
|
||||
name: record.alert,
|
||||
name: record?.alert,
|
||||
action: actionValue,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { TableProps } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
@@ -34,7 +35,7 @@ import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { get, isEmpty } from 'lodash-es';
|
||||
import { get, isEmpty, isUndefined } from 'lodash-es';
|
||||
import {
|
||||
ArrowDownWideNarrow,
|
||||
ArrowUpRight,
|
||||
@@ -60,6 +61,7 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -71,7 +73,6 @@ import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import useUrlQuery from '../../hooks/useUrlQuery';
|
||||
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
||||
import ImportJSON from './ImportJSON';
|
||||
import { DeleteButton } from './TableComponents/DeleteButton';
|
||||
@@ -84,7 +85,7 @@ import {
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function DashboardsList(): JSX.Element {
|
||||
const {
|
||||
data: dashboardListResponse = [],
|
||||
data: dashboardListResponse,
|
||||
isLoading: isDashboardListLoading,
|
||||
error: dashboardFetchError,
|
||||
refetch: refetchDashboardList,
|
||||
@@ -97,12 +98,14 @@ function DashboardsList(): JSX.Element {
|
||||
setListSortOrder: setSortOrder,
|
||||
} = useDashboard();
|
||||
|
||||
const [searchString, setSearchString] = useState<string>(
|
||||
sortOrder.search || '',
|
||||
);
|
||||
const [action, createNewDashboard] = useComponentPermission(
|
||||
['action', 'create_new_dashboards'],
|
||||
role,
|
||||
);
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [
|
||||
showNewDashboardTemplatesModal,
|
||||
setShowNewDashboardTemplatesModal,
|
||||
@@ -121,10 +124,6 @@ function DashboardsList(): JSX.Element {
|
||||
false,
|
||||
);
|
||||
|
||||
const params = useUrlQuery();
|
||||
const searchParams = params.get('search');
|
||||
const [searchString, setSearchString] = useState<string>(searchParams || '');
|
||||
|
||||
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
|
||||
const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
|
||||
let dashboardDynamicColumns: DashboardDynamicColumns = {
|
||||
@@ -186,14 +185,6 @@ function DashboardsList(): JSX.Element {
|
||||
setDashboards(sortedDashboards);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
params.set('columnKey', sortOrder.columnKey as string);
|
||||
params.set('order', sortOrder.order as string);
|
||||
params.set('page', sortOrder.pagination || '1');
|
||||
history.replace({ search: params.toString() });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sortOrder]);
|
||||
|
||||
const sortHandle = (key: string): void => {
|
||||
if (!dashboards) return;
|
||||
if (key === 'createdAt') {
|
||||
@@ -202,6 +193,7 @@ function DashboardsList(): JSX.Element {
|
||||
columnKey: 'createdAt',
|
||||
order: 'descend',
|
||||
pagination: sortOrder.pagination || '1',
|
||||
search: sortOrder.search || '',
|
||||
});
|
||||
} else if (key === 'updatedAt') {
|
||||
sortDashboardsByUpdatedAt(dashboards);
|
||||
@@ -209,21 +201,19 @@ function DashboardsList(): JSX.Element {
|
||||
columnKey: 'updatedAt',
|
||||
order: 'descend',
|
||||
pagination: sortOrder.pagination || '1',
|
||||
search: sortOrder.search || '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function handlePageSizeUpdate(page: number): void {
|
||||
setSortOrder((order) => ({
|
||||
...order,
|
||||
pagination: String(page),
|
||||
}));
|
||||
setSortOrder({ ...sortOrder, pagination: String(page) });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const filteredDashboards = filterDashboard(
|
||||
searchString,
|
||||
dashboardListResponse,
|
||||
dashboardListResponse || [],
|
||||
);
|
||||
if (sortOrder.columnKey === 'updatedAt') {
|
||||
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
||||
@@ -234,6 +224,7 @@ function DashboardsList(): JSX.Element {
|
||||
columnKey: 'updatedAt',
|
||||
order: 'descend',
|
||||
pagination: sortOrder.pagination || '1',
|
||||
search: sortOrder.search || '',
|
||||
});
|
||||
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
||||
}
|
||||
@@ -243,6 +234,7 @@ function DashboardsList(): JSX.Element {
|
||||
setSortOrder,
|
||||
sortOrder.columnKey,
|
||||
sortOrder.pagination,
|
||||
sortOrder.search,
|
||||
]);
|
||||
|
||||
const [newDashboardState, setNewDashboardState] = useState({
|
||||
@@ -269,6 +261,7 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
const onNewDashboardHandler = useCallback(async () => {
|
||||
try {
|
||||
logEvent('Dashboard List: Create dashboard clicked', {});
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
loading: true,
|
||||
@@ -305,18 +298,23 @@ function DashboardsList(): JSX.Element {
|
||||
}, [newDashboardState, t]);
|
||||
|
||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||
logEvent('Dashboard List: Import JSON clicked', {});
|
||||
|
||||
setIsImportJSONModalVisible((state) => !state);
|
||||
setUploadedGrafana(uploadedGrafana);
|
||||
};
|
||||
|
||||
const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
setIsFilteringDashboards(true);
|
||||
setSearchValue(event.target.value);
|
||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||
const filteredDashboards = filterDashboard(searchText, dashboardListResponse);
|
||||
const filteredDashboards = filterDashboard(
|
||||
searchText,
|
||||
dashboardListResponse || [],
|
||||
);
|
||||
setDashboards(filteredDashboards);
|
||||
setIsFilteringDashboards(false);
|
||||
setSearchString(searchText);
|
||||
setSortOrder({ ...sortOrder, search: searchText });
|
||||
};
|
||||
|
||||
const [state, setCopy] = useCopyToClipboard();
|
||||
@@ -407,7 +405,7 @@ function DashboardsList(): JSX.Element {
|
||||
{
|
||||
title: 'Dashboards',
|
||||
key: 'dashboard',
|
||||
render: (dashboard: Data): JSX.Element => {
|
||||
render: (dashboard: Data, _, index): JSX.Element => {
|
||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
@@ -441,6 +439,10 @@ function DashboardsList(): JSX.Element {
|
||||
} else {
|
||||
history.push(getLink());
|
||||
}
|
||||
logEvent('Dashboard List: Clicked on dashboard', {
|
||||
dashboardId: dashboard.id,
|
||||
dashboardName: dashboard.name,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -452,7 +454,9 @@ function DashboardsList(): JSX.Element {
|
||||
style={{ height: '14px', width: '14px' }}
|
||||
alt="dashboard-image"
|
||||
/>
|
||||
<Typography.Text>{dashboard.name}</Typography.Text>
|
||||
<Typography.Text data-testid={`dashboard-title-${index}`}>
|
||||
{dashboard.name}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="tags-with-actions">
|
||||
@@ -619,6 +623,21 @@ function DashboardsList(): JSX.Element {
|
||||
hideOnSinglePage: true,
|
||||
};
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (
|
||||
!logEventCalledRef.current &&
|
||||
!isDashboardListLoading &&
|
||||
!isUndefined(dashboardListResponse)
|
||||
) {
|
||||
logEvent('Dashboard List: Page visited', {
|
||||
number: dashboardListResponse?.length,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDashboardListLoading]);
|
||||
|
||||
return (
|
||||
<div className="dashboards-list-container">
|
||||
<div className="dashboards-list-view-content">
|
||||
@@ -677,7 +696,7 @@ function DashboardsList(): JSX.Element {
|
||||
<ArrowUpRight size={16} className="learn-more-arrow" />
|
||||
</section>
|
||||
</div>
|
||||
) : dashboards?.length === 0 && !searchValue ? (
|
||||
) : dashboards?.length === 0 && !searchString ? (
|
||||
<div className="dashboard-empty-state">
|
||||
<img
|
||||
src="/Icons/dashboards.svg"
|
||||
@@ -705,6 +724,9 @@ function DashboardsList(): JSX.Element {
|
||||
type="text"
|
||||
className="new-dashboard"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List: New dashboard clicked', {});
|
||||
}}
|
||||
>
|
||||
New Dashboard
|
||||
</Button>
|
||||
@@ -712,6 +734,7 @@ function DashboardsList(): JSX.Element {
|
||||
<Button
|
||||
type="text"
|
||||
className="learn-more"
|
||||
data-testid="learn-more"
|
||||
onClick={(): void => {
|
||||
window.open(
|
||||
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
|
||||
@@ -731,7 +754,7 @@ function DashboardsList(): JSX.Element {
|
||||
<Input
|
||||
placeholder="Search by name, description, or tags..."
|
||||
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
||||
value={searchValue}
|
||||
value={searchString}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
{createNewDashboard && (
|
||||
@@ -745,6 +768,9 @@ function DashboardsList(): JSX.Element {
|
||||
type="primary"
|
||||
className="periscope-btn primary btn"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List: New dashboard clicked', {});
|
||||
}}
|
||||
>
|
||||
New dashboard
|
||||
</Button>
|
||||
@@ -756,7 +782,7 @@ function DashboardsList(): JSX.Element {
|
||||
<div className="no-search">
|
||||
<img src="/Icons/emptyState.svg" alt="img" className="img" />
|
||||
<Typography.Text className="text">
|
||||
No dashboards found for {searchValue}. Create a new dashboard?
|
||||
No dashboards found for {searchString}. Create a new dashboard?
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
@@ -778,6 +804,7 @@ function DashboardsList(): JSX.Element {
|
||||
type="text"
|
||||
className={cx('sort-btns')}
|
||||
onClick={(): void => sortHandle('createdAt')}
|
||||
data-testid="sort-by-last-created"
|
||||
>
|
||||
Last created
|
||||
{sortOrder.columnKey === 'createdAt' && <Check size={14} />}
|
||||
@@ -786,6 +813,7 @@ function DashboardsList(): JSX.Element {
|
||||
type="text"
|
||||
className={cx('sort-btns')}
|
||||
onClick={(): void => sortHandle('updatedAt')}
|
||||
data-testid="sort-by-last-updated"
|
||||
>
|
||||
Last updated
|
||||
{sortOrder.columnKey === 'updatedAt' && <Check size={14} />}
|
||||
@@ -796,7 +824,7 @@ function DashboardsList(): JSX.Element {
|
||||
placement="bottomRight"
|
||||
arrow={false}
|
||||
>
|
||||
<ArrowDownWideNarrow size={14} />
|
||||
<ArrowDownWideNarrow size={14} data-testid="sort-by" />
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ExclamationCircleTwoTone } from '@ant-design/icons';
|
||||
import MEditor, { Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -67,6 +68,8 @@ function ImportJSON({
|
||||
const onClickLoadJsonHandler = async (): Promise<void> => {
|
||||
try {
|
||||
setDashboardCreating(true);
|
||||
logEvent('Dashboard List: Import and next clicked', {});
|
||||
|
||||
const dashboardData = JSON.parse(editorValue) as DashboardData;
|
||||
|
||||
if (dashboardData?.layout) {
|
||||
@@ -86,6 +89,10 @@ function ImportJSON({
|
||||
dashboardId: response.payload.uuid,
|
||||
}),
|
||||
);
|
||||
logEvent('Dashboard List: New dashboard imported successfully', {
|
||||
dashboardId: response.payload?.uuid,
|
||||
dashboardName: response.payload?.data?.title,
|
||||
});
|
||||
} else if (response.error === 'feature usage exceeded') {
|
||||
setIsFeatureAlert(true);
|
||||
notifications.error({
|
||||
@@ -180,6 +187,9 @@ function ImportJSON({
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
icon={<MonitorDot size={14} />}
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List: Upload JSON file clicked', {});
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{t('upload_json_file')}
|
||||
|
||||
@@ -3,6 +3,7 @@ import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import ListLogView from 'components/Logs/ListLogView';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { CARD_BODY_STYLE } from 'constants/card';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@@ -128,13 +129,15 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
/>
|
||||
) : (
|
||||
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
/>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
)}
|
||||
</InfinityWrapperStyled>
|
||||
|
||||
@@ -2,6 +2,7 @@ import './ContextLogRenderer.styles.scss';
|
||||
|
||||
import { Skeleton } from 'antd';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import ShowButton from 'container/LogsContextList/ShowButton';
|
||||
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@@ -94,13 +95,15 @@ function ContextLogRenderer({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Virtuoso
|
||||
className="virtuoso-list"
|
||||
initialTopMostItemIndex={0}
|
||||
data={logs}
|
||||
itemContent={getItemContent}
|
||||
style={{ height: `calc(${logs.length} * 32px)` }}
|
||||
/>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
className="virtuoso-list"
|
||||
initialTopMostItemIndex={0}
|
||||
data={logs}
|
||||
itemContent={getItemContent}
|
||||
style={{ height: `calc(${logs.length} * 32px)` }}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
{isAfterLogsFetching && (
|
||||
<Skeleton
|
||||
style={{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import './LogsContextList.styles.scss';
|
||||
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -187,14 +188,15 @@ function LogsContextList({
|
||||
<EmptyText>No Data</EmptyText>
|
||||
)}
|
||||
{isFetching && <Spinner size="large" height="10rem" />}
|
||||
|
||||
<Virtuoso
|
||||
className="virtuoso-list"
|
||||
initialTopMostItemIndex={0}
|
||||
data={logs}
|
||||
itemContent={getItemContent}
|
||||
followOutput={order === ORDERBY_FILTERS.DESC}
|
||||
/>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
className="virtuoso-list"
|
||||
initialTopMostItemIndex={0}
|
||||
data={logs}
|
||||
itemContent={getItemContent}
|
||||
followOutput={order === ORDERBY_FILTERS.DESC}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</ListContainer>
|
||||
|
||||
{order === ORDERBY_FILTERS.DESC && (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
// components
|
||||
import ListLogView from 'components/Logs/ListLogView';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { CARD_BODY_STYLE } from 'constants/card';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@@ -133,15 +134,17 @@ function LogsExplorerList({
|
||||
style={{ width: '100%', marginTop: '20px' }}
|
||||
bodyStyle={CARD_BODY_STYLE}
|
||||
>
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
endReached={onEndReached}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
components={components}
|
||||
/>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
endReached={onEndReached}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
components={components}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
);
|
||||
}, [
|
||||
@@ -169,7 +172,9 @@ function LogsExplorerList({
|
||||
!isFetching &&
|
||||
logs.length === 0 &&
|
||||
!isError &&
|
||||
isFilterApplied && <EmptyLogsSearch />}
|
||||
isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={DataSource.LOGS} panelType="LIST" />
|
||||
)}
|
||||
|
||||
{isError && !isLoading && !isFetching && <LogsError />}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import './LogsExplorerViews.styles.scss';
|
||||
|
||||
import { Button } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@@ -37,7 +38,14 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
||||
import { cloneDeep, defaultTo, isEmpty, omit, set } from 'lodash-es';
|
||||
import {
|
||||
cloneDeep,
|
||||
defaultTo,
|
||||
isEmpty,
|
||||
isUndefined,
|
||||
omit,
|
||||
set,
|
||||
} from 'lodash-es';
|
||||
import { Sliders } from 'lucide-react';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
@@ -310,6 +318,19 @@ function LogsExplorerViews({
|
||||
],
|
||||
);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
|
||||
const currentData = data?.payload?.data?.newResult?.data?.result || [];
|
||||
logEvent('Logs Explorer: Page visited', {
|
||||
panelType,
|
||||
isEmpty: !currentData?.[0]?.list,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload]);
|
||||
|
||||
const {
|
||||
mutate: updateDashboard,
|
||||
isLoading: isUpdateDashboardLoading,
|
||||
@@ -324,7 +345,7 @@ function LogsExplorerViews({
|
||||
}, [currentQuery]);
|
||||
|
||||
const handleExport = useCallback(
|
||||
(dashboard: Dashboard | null): void => {
|
||||
(dashboard: Dashboard | null, isNewDashboard?: boolean): void => {
|
||||
if (!dashboard || !panelType) return;
|
||||
|
||||
const panelTypeParam = AVAILABLE_EXPORT_PANEL_TYPES.includes(panelType)
|
||||
@@ -346,6 +367,12 @@ function LogsExplorerViews({
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
logEvent('Logs Explorer: Add to dashboard successful', {
|
||||
panelType,
|
||||
isNewDashboard,
|
||||
dashboardName: dashboard?.data?.title,
|
||||
});
|
||||
|
||||
updateDashboard(updatedDashboard, {
|
||||
onSuccess: (data) => {
|
||||
if (data.error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import './LogsPanelComponent.styles.scss';
|
||||
import { Table } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Controls from 'container/Controls';
|
||||
@@ -207,17 +208,19 @@ function LogsPanelComponent({
|
||||
<>
|
||||
<div className="logs-table">
|
||||
<div className="resize-table">
|
||||
<Table
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: `calc(50vw - 10px)` }}
|
||||
sticky
|
||||
loading={queryResponse.isFetching}
|
||||
style={tableStyles}
|
||||
dataSource={flattenLogData}
|
||||
columns={columns}
|
||||
onRow={handleRow}
|
||||
/>
|
||||
<OverlayScrollbar>
|
||||
<Table
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: `calc(50vw - 10px)` }}
|
||||
sticky
|
||||
loading={queryResponse.isFetching}
|
||||
style={tableStyles}
|
||||
dataSource={flattenLogData}
|
||||
columns={columns}
|
||||
onRow={handleRow}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
{!widget.query.builder.queryData[0].limit && (
|
||||
<div className="controller">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import ListLogView from 'components/Logs/ListLogView';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import LogsTableView from 'components/Logs/TableView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { CARD_BODY_STYLE } from 'constants/card';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
@@ -97,7 +98,9 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<Card className="logs-card" bodyStyle={CARD_BODY_STYLE}>
|
||||
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
);
|
||||
}, [getItemContent, linesPerRow, logs, onSetActiveLog, selected, viewMode]);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Col } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
convertRawQueriesToTraceSelectedTags,
|
||||
resourceAttributesToTagFilterItems,
|
||||
} from 'hooks/useResourceAttribute/utils';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -97,6 +98,24 @@ function DBCall(): JSX.Element {
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === 'resource_deployment_environment',
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('APM: Service detail page visited', {
|
||||
selectedEnvironments,
|
||||
resourceAttributeUsed: !!queries?.length,
|
||||
section: 'dbMetrics',
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const apmToTraceQuery = useGetAPMToTracesQueries({
|
||||
servicename,
|
||||
isDBCall: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Col } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
convertRawQueriesToTraceSelectedTags,
|
||||
resourceAttributesToTagFilterItems,
|
||||
} from 'hooks/useResourceAttribute/utils';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -114,6 +115,23 @@ function External(): JSX.Element {
|
||||
],
|
||||
});
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === 'resource_deployment_environment',
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('APM: Service detail page visited', {
|
||||
selectedEnvironments,
|
||||
resourceAttributeUsed: !!queries?.length,
|
||||
section: 'externalMetrics',
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const externalCallRPSWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import getTopLevelOperations, {
|
||||
ServiceDataProps,
|
||||
} from 'api/metrics/getTopLevelOperations';
|
||||
@@ -17,7 +18,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
@@ -81,6 +82,23 @@ function Application(): JSX.Element {
|
||||
[handleSetTimeStamp],
|
||||
);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === 'resource_deployment_environment',
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('APM: Service detail page visited', {
|
||||
selectedEnvironments,
|
||||
resourceAttributeUsed: !!queries?.length,
|
||||
section: 'overview',
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const {
|
||||
data: topLevelOperations,
|
||||
error: topLevelOperationsError,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import './ComponentSlider.styles.scss';
|
||||
|
||||
import { Card, Modal } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
@@ -20,6 +21,13 @@ function DashboardGraphSlider(): JSX.Element {
|
||||
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
||||
const id = uuid();
|
||||
handleToggleDashboardSlider(false);
|
||||
logEvent('Dashboard Detail: New panel type selected', {
|
||||
// dashboardId: '',
|
||||
// dashboardName: '',
|
||||
// numberOfPanels: 0, // todo - at this point we don't know these attributes
|
||||
panelType: name,
|
||||
widgetId: id,
|
||||
});
|
||||
const queryParamsLog = {
|
||||
graphType: name,
|
||||
widgetId: id,
|
||||
@@ -47,7 +55,6 @@ function DashboardGraphSlider(): JSX.Element {
|
||||
PANEL_TYPES_INITIAL_QUERY[name],
|
||||
),
|
||||
};
|
||||
|
||||
if (name === PANEL_TYPES.LIST) {
|
||||
history.push(
|
||||
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
|
||||
|
||||
@@ -2,6 +2,7 @@ import './Description.styles.scss';
|
||||
|
||||
import { Button } from 'antd';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import DashboardSettingsContent from '../DashboardSettings';
|
||||
@@ -41,7 +42,9 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
|
||||
open={visible}
|
||||
rootClassName="settings-container-root"
|
||||
>
|
||||
<DashboardSettingsContent variableViewModeRef={variableViewModeRef} />
|
||||
<OverlayScrollbar>
|
||||
<DashboardSettingsContent variableViewModeRef={variableViewModeRef} />
|
||||
</OverlayScrollbar>
|
||||
</DrawerContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import './Description.styles.scss';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Input, Modal, Popover, Tag, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
@@ -126,6 +127,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(() => {
|
||||
handleToggleDashboardSlider(true);
|
||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [handleToggleDashboardSlider]);
|
||||
|
||||
const handleLockDashboardToggle = (): void => {
|
||||
@@ -259,6 +266,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
urlQuery.set('columnKey', listSortOrder.columnKey as string);
|
||||
urlQuery.set('order', listSortOrder.order as string);
|
||||
urlQuery.set('page', listSortOrder.pagination as string);
|
||||
urlQuery.set('search', listSortOrder.search as string);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
|
||||
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`;
|
||||
@@ -404,7 +412,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button icon={<Ellipsis size={14} />} type="text" className="icons" />
|
||||
<Button
|
||||
icon={<Ellipsis size={14} />}
|
||||
type="text"
|
||||
className="icons"
|
||||
data-testid="options"
|
||||
/>
|
||||
</Popover>
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
<SettingsDrawer drawerTitle="Dashboard Configuration" />
|
||||
@@ -415,7 +428,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
onClick={onEmptyWidgetHandler}
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
data-testid="add-panel"
|
||||
data-testid="add-panel-header"
|
||||
>
|
||||
New Panel
|
||||
</Button>
|
||||
|
||||
@@ -6,7 +6,7 @@ import GridGraphs from './GridGraphs';
|
||||
function NewDashboard(): JSX.Element {
|
||||
const handle = useFullScreenHandle();
|
||||
return (
|
||||
<div style={{ overflowX: 'hidden' }}>
|
||||
<div>
|
||||
<Description handle={handle} />
|
||||
<GridGraphs handle={handle} />
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import './QuerySection.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -14,7 +15,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { defaultTo, isUndefined } from 'lodash-es';
|
||||
import { Atom, Play, Terminal } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
@@ -122,6 +123,18 @@ function QuerySection({
|
||||
};
|
||||
|
||||
const handleRunQuery = (): void => {
|
||||
const widgetId = urlQuery.get('widgetId');
|
||||
const isNewPanel = isUndefined(widgets?.find((e) => e.id === widgetId));
|
||||
|
||||
logEvent('Panel Edit: Stage and run query', {
|
||||
dataSource: currentQuery.builder?.queryData?.[0]?.dataSource,
|
||||
panelType: selectedWidget.panelTypes,
|
||||
queryType: currentQuery.queryType,
|
||||
widgetId: selectedWidget.id,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
isNewPanel,
|
||||
});
|
||||
handleStageQuery(currentQuery);
|
||||
};
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ function RightContainer({
|
||||
const selectedGraphType =
|
||||
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
|
||||
|
||||
const onCreateAlertsHandler = useCreateAlerts(selectedWidget);
|
||||
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
|
||||
|
||||
const allowThreshold = panelTypeVsThreshold[selectedGraph];
|
||||
const allowSoftMinMax = panelTypeVsSoftMinMax[selectedGraph];
|
||||
@@ -163,6 +163,7 @@ function RightContainer({
|
||||
value={selectedGraph}
|
||||
style={{ width: '100%' }}
|
||||
className="panel-type-select"
|
||||
data-testid="panel-change-select"
|
||||
>
|
||||
{graphTypes.map((item) => (
|
||||
<Option key={item.name} value={item.name}>
|
||||
|
||||
@@ -3,8 +3,10 @@ import './NewWidget.styles.scss';
|
||||
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Modal, Space, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import { chartHelpMessage } from 'components/facingIssueBtn/util';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -30,7 +32,7 @@ import {
|
||||
getPreviousWidgets,
|
||||
getSelectedWidgetIndex,
|
||||
} from 'providers/Dashboard/util';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath, useParams } from 'react-router-dom';
|
||||
@@ -100,6 +102,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const widgetId = query.get('widgetId');
|
||||
const selectedWidget = widgets?.find((e) => e.id === widgetId);
|
||||
@@ -107,6 +111,18 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
if (isWidgetNotPresent) {
|
||||
setIsNewDashboard(true);
|
||||
}
|
||||
|
||||
if (!logEventCalledRef.current) {
|
||||
logEvent('Panel Edit: Page visited', {
|
||||
panelType: selectedWidget?.panelTypes,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
widgetId: selectedWidget?.id,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
isNewPanel: !!isWidgetNotPresent,
|
||||
dataSource: currentQuery.builder.queryData?.[0]?.dataSource,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -481,7 +497,20 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
};
|
||||
|
||||
const onSaveDashboard = useCallback((): void => {
|
||||
const widgetId = query.get('widgetId');
|
||||
const selectWidget = widgets?.find((e) => e.id === widgetId);
|
||||
|
||||
logEvent('Panel Edit: Save changes', {
|
||||
panelType: selectedWidget.panelTypes,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
widgetId: selectedWidget.id,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
queryType: currentQuery.queryType,
|
||||
isNewPanel: isUndefined(selectWidget),
|
||||
dataSource: currentQuery.builder.queryData?.[0]?.dataSource,
|
||||
});
|
||||
setSaveModal(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const isQueryBuilderActive = useIsFeatureDisabled(
|
||||
@@ -530,11 +559,39 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onSaveDashboard]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedGraph === PANEL_TYPES.LIST) {
|
||||
const initialDataSource = currentQuery.builder.queryData[0].dataSource;
|
||||
if (initialDataSource === DataSource.LOGS) {
|
||||
setRequestData((prev) => ({
|
||||
...prev,
|
||||
tableParams: {
|
||||
...prev.tableParams,
|
||||
selectColumns: selectedLogFields,
|
||||
},
|
||||
}));
|
||||
} else if (initialDataSource === DataSource.TRACES) {
|
||||
setRequestData((prev) => ({
|
||||
...prev,
|
||||
tableParams: {
|
||||
...prev.tableParams,
|
||||
selectColumns: selectedTracesFields,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [selectedLogFields, selectedTracesFields, currentQuery, selectedGraph]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="edit-header">
|
||||
<div className="left-header">
|
||||
<X size={14} onClick={onClickDiscardHandler} className="discard-icon" />
|
||||
<X
|
||||
size={14}
|
||||
onClick={onClickDiscardHandler}
|
||||
className="discard-icon"
|
||||
data-testid="discard-button"
|
||||
/>
|
||||
<Flex align="center" gap={24}>
|
||||
<Typography.Text className="configure-panel">
|
||||
Configure panel
|
||||
@@ -586,60 +643,64 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
<PanelContainer>
|
||||
<LeftContainerWrapper isDarkMode={useIsDarkMode()}>
|
||||
{selectedWidget && (
|
||||
<LeftContainer
|
||||
selectedGraph={graphType}
|
||||
selectedLogFields={selectedLogFields}
|
||||
setSelectedLogFields={setSelectedLogFields}
|
||||
selectedTracesFields={selectedTracesFields}
|
||||
setSelectedTracesFields={setSelectedTracesFields}
|
||||
selectedWidget={selectedWidget}
|
||||
selectedTime={selectedTime}
|
||||
requestData={requestData}
|
||||
setRequestData={setRequestData}
|
||||
isLoadingPanelData={isLoadingPanelData}
|
||||
/>
|
||||
)}
|
||||
<OverlayScrollbar>
|
||||
{selectedWidget && (
|
||||
<LeftContainer
|
||||
selectedGraph={graphType}
|
||||
selectedLogFields={selectedLogFields}
|
||||
setSelectedLogFields={setSelectedLogFields}
|
||||
selectedTracesFields={selectedTracesFields}
|
||||
setSelectedTracesFields={setSelectedTracesFields}
|
||||
selectedWidget={selectedWidget}
|
||||
selectedTime={selectedTime}
|
||||
requestData={requestData}
|
||||
setRequestData={setRequestData}
|
||||
isLoadingPanelData={isLoadingPanelData}
|
||||
/>
|
||||
)}
|
||||
</OverlayScrollbar>
|
||||
</LeftContainerWrapper>
|
||||
|
||||
<RightContainerWrapper>
|
||||
<RightContainer
|
||||
setGraphHandler={setGraphHandler}
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
description={description}
|
||||
setDescription={setDescription}
|
||||
stacked={stacked}
|
||||
setStacked={setStacked}
|
||||
stackedBarChart={stackedBarChart}
|
||||
setStackedBarChart={setStackedBarChart}
|
||||
opacity={opacity}
|
||||
yAxisUnit={yAxisUnit}
|
||||
columnUnits={columnUnits}
|
||||
setColumnUnits={setColumnUnits}
|
||||
bucketCount={bucketCount}
|
||||
bucketWidth={bucketWidth}
|
||||
combineHistogram={combineHistogram}
|
||||
setCombineHistogram={setCombineHistogram}
|
||||
setBucketWidth={setBucketWidth}
|
||||
setBucketCount={setBucketCount}
|
||||
setOpacity={setOpacity}
|
||||
selectedNullZeroValue={selectedNullZeroValue}
|
||||
setSelectedNullZeroValue={setSelectedNullZeroValue}
|
||||
selectedGraph={graphType}
|
||||
setSelectedTime={setSelectedTime}
|
||||
selectedTime={selectedTime}
|
||||
setYAxisUnit={setYAxisUnit}
|
||||
thresholds={thresholds}
|
||||
setThresholds={setThresholds}
|
||||
selectedWidget={selectedWidget}
|
||||
isFillSpans={isFillSpans}
|
||||
setIsFillSpans={setIsFillSpans}
|
||||
softMin={softMin}
|
||||
setSoftMin={setSoftMin}
|
||||
softMax={softMax}
|
||||
setSoftMax={setSoftMax}
|
||||
/>
|
||||
<OverlayScrollbar>
|
||||
<RightContainer
|
||||
setGraphHandler={setGraphHandler}
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
description={description}
|
||||
setDescription={setDescription}
|
||||
stacked={stacked}
|
||||
setStacked={setStacked}
|
||||
stackedBarChart={stackedBarChart}
|
||||
setStackedBarChart={setStackedBarChart}
|
||||
opacity={opacity}
|
||||
yAxisUnit={yAxisUnit}
|
||||
columnUnits={columnUnits}
|
||||
setColumnUnits={setColumnUnits}
|
||||
bucketCount={bucketCount}
|
||||
bucketWidth={bucketWidth}
|
||||
combineHistogram={combineHistogram}
|
||||
setCombineHistogram={setCombineHistogram}
|
||||
setBucketWidth={setBucketWidth}
|
||||
setBucketCount={setBucketCount}
|
||||
setOpacity={setOpacity}
|
||||
selectedNullZeroValue={selectedNullZeroValue}
|
||||
setSelectedNullZeroValue={setSelectedNullZeroValue}
|
||||
selectedGraph={graphType}
|
||||
setSelectedTime={setSelectedTime}
|
||||
selectedTime={selectedTime}
|
||||
setYAxisUnit={setYAxisUnit}
|
||||
thresholds={thresholds}
|
||||
setThresholds={setThresholds}
|
||||
selectedWidget={selectedWidget}
|
||||
isFillSpans={isFillSpans}
|
||||
setIsFillSpans={setIsFillSpans}
|
||||
softMin={softMin}
|
||||
setSoftMin={setSoftMin}
|
||||
softMax={softMax}
|
||||
setSoftMax={setSoftMax}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</RightContainerWrapper>
|
||||
</PanelContainer>
|
||||
<Modal
|
||||
|
||||
@@ -54,6 +54,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -72,6 +73,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -88,6 +90,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -107,6 +110,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -125,6 +129,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -141,6 +146,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -157,6 +163,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -172,6 +180,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'orderBy',
|
||||
'functions',
|
||||
'spaceAggregation',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -185,6 +195,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -204,6 +216,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'reduceTo',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -222,6 +236,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'reduceTo',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -238,6 +254,8 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'reduceTo',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -257,6 +275,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -275,6 +294,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -291,6 +311,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -325,6 +346,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -341,6 +363,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -357,6 +380,7 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
'queryName',
|
||||
'expression',
|
||||
'disabled',
|
||||
'legend',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import './NoLogs.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
@@ -21,6 +22,11 @@ export default function NoLogs({
|
||||
e.stopPropagation();
|
||||
|
||||
if (cloudUser) {
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
logEvent('Traces Explorer: Navigate to onboarding', {});
|
||||
} else if (dataSource === DataSource.LOGS) {
|
||||
logEvent('Logs Explorer: Navigate to onboarding', {});
|
||||
}
|
||||
history.push(
|
||||
dataSource === 'traces'
|
||||
? ROUTES.GET_STARTED_APPLICATION_MONITORING
|
||||
|
||||
@@ -3,11 +3,19 @@ import './styles.scss';
|
||||
import { ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Card, Modal, Table, Typography } from 'antd';
|
||||
import { ExpandableConfig } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import savePipeline from 'api/pipeline/post';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -466,6 +474,16 @@ function PipelineListsView({
|
||||
getExpandIcon(expanded, onExpand, record),
|
||||
};
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(currPipelineData)) {
|
||||
logEvent('Logs Pipelines: List page visited', {
|
||||
number: currPipelineData?.length,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, [currPipelineData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
|
||||
@@ -191,6 +191,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
||||
labelInValue
|
||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||
onChange={handleChange}
|
||||
data-testid="group-by"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ export const ReduceToFilter = memo(function ReduceToFilter({
|
||||
style={{ width: '100%' }}
|
||||
options={REDUCE_TO_VALUES}
|
||||
value={currentValue}
|
||||
data-testid="reduce-to"
|
||||
labelInValue
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import localStorageGet from 'api/browser/localstorage/get';
|
||||
import localStorageSet from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { SKIP_ONBOARDING } from 'constants/onboarding';
|
||||
import useErrorNotification from 'hooks/useErrorNotification';
|
||||
import { useQueryService } from 'hooks/useQueryService';
|
||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -45,6 +47,26 @@ function ServiceTraces(): JSX.Element {
|
||||
setSkipOnboarding(true);
|
||||
};
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === 'resource_deployment_environment',
|
||||
)?.tagValue;
|
||||
|
||||
const rps = data.reduce((total, service) => total + service.callRate, 0);
|
||||
|
||||
logEvent('APM: List page visited', {
|
||||
numberOfServices: data?.length,
|
||||
selectedEnvironments,
|
||||
resourceAttributeUsed: !!queries?.length,
|
||||
rps,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
if (
|
||||
services.length === 0 &&
|
||||
isLoading === false &&
|
||||
|
||||
@@ -4,6 +4,7 @@ import './SideNav.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -179,6 +180,11 @@ function SideNav({
|
||||
};
|
||||
|
||||
const onClickShortcuts = (e: MouseEvent): void => {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
logEvent('Sidebar: Menu clicked', {
|
||||
menuRoute: '/shortcuts',
|
||||
menuLabel: 'Keyboard Shortcuts',
|
||||
});
|
||||
if (isCtrlMetaKey(e)) {
|
||||
openInNewTab('/shortcuts');
|
||||
} else {
|
||||
@@ -187,6 +193,10 @@ function SideNav({
|
||||
};
|
||||
|
||||
const onClickGetStarted = (event: MouseEvent): void => {
|
||||
logEvent('Sidebar: Menu clicked', {
|
||||
menuRoute: '/get-started',
|
||||
menuLabel: 'Get Started',
|
||||
});
|
||||
if (isCtrlMetaKey(event)) {
|
||||
openInNewTab('/get-started');
|
||||
} else {
|
||||
@@ -313,6 +323,10 @@ function SideNav({
|
||||
} else if (item) {
|
||||
onClickHandler(item?.key as string, event);
|
||||
}
|
||||
logEvent('Sidebar: Menu clicked', {
|
||||
menuRoute: item?.key,
|
||||
menuLabel: item?.label,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -440,6 +454,10 @@ function SideNav({
|
||||
isActive={activeMenuKey === item?.key}
|
||||
onClick={(event: MouseEvent): void => {
|
||||
handleUserManagentMenuItemClick(item?.key as string, event);
|
||||
logEvent('Sidebar: Menu clicked', {
|
||||
menuRoute: item?.key,
|
||||
menuLabel: item?.label,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
@@ -456,6 +474,10 @@ function SideNav({
|
||||
} else {
|
||||
history.push(`${inviteMemberMenuItem.key}`);
|
||||
}
|
||||
logEvent('Sidebar: Menu clicked', {
|
||||
menuRoute: inviteMemberMenuItem?.key,
|
||||
menuLabel: inviteMemberMenuItem?.label,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -470,6 +492,10 @@ function SideNav({
|
||||
userSettingsMenuItem?.key as string,
|
||||
event,
|
||||
);
|
||||
logEvent('Sidebar: Menu clicked', {
|
||||
menuRoute: userSettingsMenuItem?.key,
|
||||
menuLabel: 'User',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -155,7 +155,9 @@ function TimeSeriesView({
|
||||
chartData[0]?.length === 0 &&
|
||||
!isLoading &&
|
||||
!isError &&
|
||||
isFilterApplied && <EmptyLogsSearch />}
|
||||
isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={dataSource} panelType="TIME_SERIES" />
|
||||
)}
|
||||
|
||||
{chartData &&
|
||||
chartData[0] &&
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Modal, Tabs, Typography } from 'antd';
|
||||
import { Button, Modal, Tabs, Tooltip, Typography } from 'antd';
|
||||
import Editor from 'components/Editor';
|
||||
import { StyledSpace } from 'components/Styled';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -102,8 +102,7 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
||||
marginTop: '16px',
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
Details for selected Span{' '}
|
||||
Details for selected Span
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text style={{ fontWeight: 700 }}>Service</Typography.Text>
|
||||
@@ -114,6 +113,30 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
||||
|
||||
<Typography>{tree.name}</Typography>
|
||||
|
||||
<Typography.Text style={{ fontWeight: 700 }}>SpanKind</Typography.Text>
|
||||
|
||||
<Typography>{tree.spanKind}</Typography>
|
||||
|
||||
<Typography.Text style={{ fontWeight: 700 }}>
|
||||
StatusCodeString
|
||||
</Typography.Text>
|
||||
|
||||
<Tooltip placement="left" title={tree.statusCodeString}>
|
||||
<Typography>{tree.statusCodeString}</Typography>
|
||||
</Tooltip>
|
||||
|
||||
{tree.statusMessage && (
|
||||
<>
|
||||
<Typography.Text style={{ fontWeight: 700 }}>
|
||||
StatusMessage
|
||||
</Typography.Text>
|
||||
|
||||
<Tooltip placement="left" title={tree.statusMessage}>
|
||||
<Typography>{tree.statusMessage}</Typography>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button size="small" style={{ marginTop: '8px' }} onClick={onLogsHandler}>
|
||||
Go to Related logs
|
||||
</Button>
|
||||
|
||||
@@ -13,6 +13,9 @@ describe('traces/getTreeLevelsCount', () => {
|
||||
children,
|
||||
serviceName: '',
|
||||
serviceColour: '',
|
||||
spanKind: '',
|
||||
statusCodeString: '',
|
||||
statusMessage: '',
|
||||
});
|
||||
|
||||
test('should return 0 for empty tree', () => {
|
||||
|
||||
@@ -37,6 +37,9 @@ test('loads and displays greeting', () => {
|
||||
event: [],
|
||||
hasError: false,
|
||||
parent: undefined,
|
||||
spanKind: '',
|
||||
statusCodeString: '',
|
||||
statusMessage: '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -156,7 +156,9 @@ function ListView({ isFilterApplied }: ListViewProps): JSX.Element {
|
||||
<NoLogs dataSource={DataSource.TRACES} />
|
||||
)}
|
||||
|
||||
{isDataPresent && isFilterApplied && <EmptyLogsSearch />}
|
||||
{isDataPresent && isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="LIST" />
|
||||
)}
|
||||
|
||||
{!isError && transformedQueryTableData.length !== 0 && (
|
||||
<ResizeTable
|
||||
|
||||
@@ -105,7 +105,9 @@ function TracesView({ isFilterApplied }: TracesViewProps): JSX.Element {
|
||||
!isFetching &&
|
||||
(tableData || []).length === 0 &&
|
||||
!isError &&
|
||||
isFilterApplied && <EmptyLogsSearch />}
|
||||
isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="TRACE" />
|
||||
)}
|
||||
|
||||
{(tableData || []).length !== 0 && (
|
||||
<ResizeTable
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import './TracesTableComponent.styles.scss';
|
||||
|
||||
import { Table } from 'antd';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import Controls from 'container/Controls';
|
||||
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
||||
@@ -42,6 +43,7 @@ function TracesTableComponent({
|
||||
setRequestData((prev) => ({
|
||||
...prev,
|
||||
tableParams: {
|
||||
...prev.tableParams,
|
||||
pagination,
|
||||
},
|
||||
}));
|
||||
@@ -86,17 +88,19 @@ function TracesTableComponent({
|
||||
return (
|
||||
<div className="traces-table">
|
||||
<div className="resize-table">
|
||||
<Table
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: true }}
|
||||
loading={queryResponse.isFetching}
|
||||
style={tableStyles}
|
||||
dataSource={transformedQueryTableData}
|
||||
columns={columns}
|
||||
onRow={handleRow}
|
||||
sticky
|
||||
/>
|
||||
<OverlayScrollbar>
|
||||
<Table
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: true }}
|
||||
loading={queryResponse.isFetching}
|
||||
style={tableStyles}
|
||||
dataSource={transformedQueryTableData}
|
||||
columns={columns}
|
||||
onRow={handleRow}
|
||||
sticky
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
<div className="controller">
|
||||
<Controls
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getQueryRangeFormat } from 'api/dashboard/queryRangeFormat';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
|
||||
@@ -17,7 +19,7 @@ import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
|
||||
const useCreateAlerts = (widget?: Widgets): VoidFunction => {
|
||||
const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
const queryRangeMutation = useMutation(getQueryRangeFormat);
|
||||
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
@@ -32,6 +34,24 @@ const useCreateAlerts = (widget?: Widgets): VoidFunction => {
|
||||
return useCallback(() => {
|
||||
if (!widget) return;
|
||||
|
||||
if (caller === 'panelView') {
|
||||
logEvent('Panel Edit: Create alert', {
|
||||
panelType: widget.panelTypes,
|
||||
dashboardName: selectedDashboard?.data?.title,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
widgetId: widget.id,
|
||||
queryType: widget.query.queryType,
|
||||
});
|
||||
} else if (caller === 'dashboardView') {
|
||||
logEvent('Dashboard Detail: Panel action', {
|
||||
action: MenuItemKeys.CreateAlerts,
|
||||
panelType: widget.panelTypes,
|
||||
dashboardName: selectedDashboard?.data?.title,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
widgetId: widget.id,
|
||||
queryType: widget.query.queryType,
|
||||
});
|
||||
}
|
||||
const { queryPayload } = prepareQueryRangePayload({
|
||||
query: widget.query,
|
||||
globalSelectedInterval,
|
||||
@@ -57,6 +77,7 @@ const useCreateAlerts = (widget?: Widgets): VoidFunction => {
|
||||
});
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
globalSelectedInterval,
|
||||
notifications,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { PartialOptions } from 'overlayscrollbars';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import {
|
||||
Dispatch,
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
const useInitializeOverlayScrollbar = (
|
||||
options: PartialOptions,
|
||||
): {
|
||||
setScroller: Dispatch<SetStateAction<null>>;
|
||||
rootRef: RefObject<HTMLDivElement>;
|
||||
} => {
|
||||
const rootRef = useRef(null);
|
||||
const [scroller, setScroller] = useState(null);
|
||||
const [initialize, osInstance] = useOverlayScrollbars({
|
||||
defer: true,
|
||||
options,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { current: root } = rootRef;
|
||||
|
||||
if (scroller && root) {
|
||||
initialize({
|
||||
target: root,
|
||||
elements: {
|
||||
viewport: scroller,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (): void => osInstance()?.destroy();
|
||||
}, [scroller, initialize, osInstance]);
|
||||
|
||||
return { setScroller, rootRef };
|
||||
};
|
||||
|
||||
export default useInitializeOverlayScrollbar;
|
||||
@@ -16,6 +16,9 @@ const spans: Span[] = [
|
||||
[''],
|
||||
[''],
|
||||
false,
|
||||
'Server',
|
||||
'Unset',
|
||||
'Lorem Ipsum',
|
||||
],
|
||||
[
|
||||
2,
|
||||
@@ -30,6 +33,9 @@ const spans: Span[] = [
|
||||
[''],
|
||||
[''],
|
||||
false,
|
||||
'Server2',
|
||||
'Unset2',
|
||||
'Lorem Ipsum2',
|
||||
],
|
||||
[
|
||||
3,
|
||||
@@ -44,6 +50,9 @@ const spans: Span[] = [
|
||||
[''],
|
||||
[''],
|
||||
false,
|
||||
'Server3',
|
||||
'Unset3',
|
||||
'Lorem Ipsum3',
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
50
frontend/src/mocks-server/__mockdata__/dashboards.ts
Normal file
50
frontend/src/mocks-server/__mockdata__/dashboards.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export const dashboardSuccessResponse = {
|
||||
status: 'success',
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
uuid: '1',
|
||||
created_at: '2022-11-16T13:29:47.064874419Z',
|
||||
created_by: null,
|
||||
updated_at: '2024-05-21T06:41:30.546630961Z',
|
||||
updated_by: 'thor@avengers.io',
|
||||
isLocked: 0,
|
||||
data: {
|
||||
collapsableRowsMigrated: true,
|
||||
description: '',
|
||||
name: '',
|
||||
panelMap: {},
|
||||
tags: ['linux'],
|
||||
title: 'thor',
|
||||
uploadedGrafana: false,
|
||||
uuid: '',
|
||||
version: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
uuid: '2',
|
||||
created_at: '2022-11-16T13:20:47.064874419Z',
|
||||
created_by: null,
|
||||
updated_at: '2024-05-21T06:42:30.546630961Z',
|
||||
updated_by: 'captain-america@avengers.io',
|
||||
isLocked: 0,
|
||||
data: {
|
||||
collapsableRowsMigrated: true,
|
||||
description: '',
|
||||
name: '',
|
||||
panelMap: {},
|
||||
tags: ['linux'],
|
||||
title: 'captain america',
|
||||
uploadedGrafana: false,
|
||||
uuid: '',
|
||||
version: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const dashboardEmptyState = {
|
||||
status: 'sucsess',
|
||||
data: [],
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { billingSuccessResponse } from './__mockdata__/billing';
|
||||
import { dashboardSuccessResponse } from './__mockdata__/dashboards';
|
||||
import { inviteUser } from './__mockdata__/invite_user';
|
||||
import { licensesSuccessResponse } from './__mockdata__/licenses';
|
||||
import { membersResponse } from './__mockdata__/members';
|
||||
@@ -91,6 +92,10 @@ export const handlers = [
|
||||
res(ctx.status(200), ctx.json(billingSuccessResponse)),
|
||||
),
|
||||
|
||||
rest.get('http://localhost/api/v1/dashboards', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(dashboardSuccessResponse)),
|
||||
),
|
||||
|
||||
rest.get('http://localhost/api/v1/invite', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(inviteUser)),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import ROUTES from 'constants/routes';
|
||||
import DashboardsList from 'container/ListOfDashboard';
|
||||
import { dashboardEmptyState } from 'mocks-server/__mockdata__/dashboards';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import { fireEvent, render, waitFor } from 'tests/test-utils';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
useRouteMatch: jest.fn().mockReturnValue({
|
||||
params: {
|
||||
dashboardId: 4,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockWindowOpen = jest.fn();
|
||||
window.open = mockWindowOpen;
|
||||
|
||||
describe('dashboard list page', () => {
|
||||
// should render on updatedAt and descend when the column key and order is messed up
|
||||
it('should render the list even when the columnKey or the order is mismatched', async () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
|
||||
search: `columnKey=asgard&order=stones&page=1`,
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
const { getByText, getByTestId } = render(
|
||||
<MemoryRouter
|
||||
initialEntries={['/dashbords?columnKey=asgard&order=stones&page=1']}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
|
||||
const firstElement = getByTestId('dashboard-title-0');
|
||||
expect(firstElement.textContent).toBe('captain america');
|
||||
const secondElement = getByTestId('dashboard-title-1');
|
||||
expect(secondElement.textContent).toBe('thor');
|
||||
});
|
||||
|
||||
// should render correctly when the column key is createdAt and order is descend
|
||||
it('should render the list even when the columnKey and the order are given', async () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
|
||||
search: `columnKey=createdAt&order=descend&page=1`,
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
const { getByText, getByTestId } = render(
|
||||
<MemoryRouter
|
||||
initialEntries={['/dashbords?columnKey=createdAt&order=descend&page=1']}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
|
||||
const firstElement = getByTestId('dashboard-title-0');
|
||||
expect(firstElement.textContent).toBe('thor');
|
||||
const secondElement = getByTestId('dashboard-title-1');
|
||||
expect(secondElement.textContent).toBe('captain america');
|
||||
});
|
||||
|
||||
// change the sort by order and dashboards list ot be updated accordingly
|
||||
it('dashboards list should be correctly updated on choosing the different sortBy from dropdown values', async () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||
]}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
|
||||
|
||||
const firstElement = getByTestId('dashboard-title-0');
|
||||
expect(firstElement.textContent).toBe('thor');
|
||||
const secondElement = getByTestId('dashboard-title-1');
|
||||
expect(secondElement.textContent).toBe('captain america');
|
||||
|
||||
// click on the sort button
|
||||
const sortByButton = getByTestId('sort-by');
|
||||
expect(sortByButton).toBeInTheDocument();
|
||||
fireEvent.click(sortByButton!);
|
||||
|
||||
// change the sort order
|
||||
const sortByUpdatedBy = getByTestId('sort-by-last-updated');
|
||||
await waitFor(() => expect(sortByUpdatedBy).toBeInTheDocument());
|
||||
fireEvent.click(sortByUpdatedBy!);
|
||||
|
||||
// expect the new order
|
||||
const updatedFirstElement = getByTestId('dashboard-title-0');
|
||||
expect(updatedFirstElement.textContent).toBe('captain america');
|
||||
const updatedSecondElement = getByTestId('dashboard-title-1');
|
||||
expect(updatedSecondElement.textContent).toBe('thor');
|
||||
});
|
||||
|
||||
// should filter correctly on search string
|
||||
it('should filter dashboards based on search string', async () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
|
||||
search: `columnKey=createdAt&order=descend&page=1&search=tho`,
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
const { getByText, getByTestId, queryByText } = render(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||
]}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
|
||||
const firstElement = getByTestId('dashboard-title-0');
|
||||
expect(firstElement.textContent).toBe('thor');
|
||||
expect(queryByText('captain america')).not.toBeInTheDocument();
|
||||
|
||||
// the pagination item should not be present in the list when number of items are less than one page size
|
||||
expect(
|
||||
document.querySelector('.ant-table-pagination'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('dashboard empty search state', async () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
|
||||
search: `columnKey=createdAt&order=descend&page=1&search=someRandomString`,
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
const { getByText } = render(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||
]}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
getByText(
|
||||
'No dashboards found for someRandomString. Create a new dashboard?',
|
||||
),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('dashboard empty state', async () => {
|
||||
const mockLocation = {
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
|
||||
search: `columnKey=createdAt&order=descend&page=1`,
|
||||
};
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/dashboards', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(dashboardEmptyState)),
|
||||
),
|
||||
);
|
||||
const { getByText, getByTestId } = render(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||
]}
|
||||
>
|
||||
<DashboardProvider>
|
||||
<DashboardsList />
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByText('No dashboards yet.')).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
const learnMoreButton = getByTestId('learn-more');
|
||||
expect(learnMoreButton).toBeInTheDocument();
|
||||
fireEvent.click(learnMoreButton);
|
||||
|
||||
// test the correct link to be added for the dashboards empty state
|
||||
await waitFor(() =>
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
|
||||
'_blank',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
getViewDetailsUsingViewKey,
|
||||
showErrorNotification,
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -143,6 +144,22 @@ function SaveView(): JSX.Element {
|
||||
viewName: newViewName,
|
||||
});
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isLoading) {
|
||||
if (sourcepage === DataSource.TRACES) {
|
||||
logEvent('Traces Views: Views visited', {
|
||||
number: viewsData?.data?.data?.length,
|
||||
});
|
||||
} else if (sourcepage === DataSource.LOGS) {
|
||||
logEvent('Logs Views: Views visited', {
|
||||
number: viewsData?.data?.data?.length,
|
||||
});
|
||||
}
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [viewsData?.data.data, isLoading]);
|
||||
const onUpdateQueryHandler = (): void => {
|
||||
updateViewAsync(
|
||||
{
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import { Calendar } from 'antd';
|
||||
import { Book, Cable, Github, MessageSquare, Slack } from 'lucide-react';
|
||||
import { fireEvent, render } from 'tests/test-utils';
|
||||
|
||||
import Support from './Support';
|
||||
|
||||
const launchChat = 'Launch chat';
|
||||
const useAnalyticsMock = jest.fn();
|
||||
const useHistoryMock = jest.fn();
|
||||
|
||||
jest.mock('hooks/analytics/useAnalytics', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => ({ trackEvent: useAnalyticsMock })),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: useHistoryMock,
|
||||
}));
|
||||
|
||||
const supportChannels = [
|
||||
{
|
||||
key: 'documentation',
|
||||
name: 'Documentation',
|
||||
icon: <Book />,
|
||||
title: 'Find answers in the documentation.',
|
||||
url: 'https://signoz.io/docs/',
|
||||
btnText: 'Visit docs',
|
||||
},
|
||||
{
|
||||
key: 'github',
|
||||
name: 'Github',
|
||||
icon: <Github />,
|
||||
title: 'Create an issue on GitHub to report bugs or request new features.',
|
||||
url: 'https://github.com/SigNoz/signoz/issues',
|
||||
btnText: 'Create issue',
|
||||
},
|
||||
{
|
||||
key: 'slack_community',
|
||||
name: 'Slack Community',
|
||||
icon: <Slack />,
|
||||
title: 'Get support from the SigNoz community on Slack.',
|
||||
url: 'https://signoz.io/slack',
|
||||
btnText: 'Join Slack',
|
||||
},
|
||||
{
|
||||
key: 'chat',
|
||||
name: 'Chat',
|
||||
icon: <MessageSquare />,
|
||||
title: 'Get quick support directly from the team.',
|
||||
url: '',
|
||||
btnText: launchChat,
|
||||
},
|
||||
{
|
||||
key: 'schedule_call',
|
||||
name: 'Schedule a call',
|
||||
icon: <Calendar />,
|
||||
title: 'Schedule a call with the founders.',
|
||||
url: 'https://calendly.com/pranay-signoz/signoz-intro-calls',
|
||||
btnText: 'Schedule call',
|
||||
},
|
||||
{
|
||||
key: 'slack_connect',
|
||||
name: 'Slack Connect',
|
||||
icon: <Cable />,
|
||||
title: 'Get a dedicated support channel for your team.',
|
||||
url: '',
|
||||
btnText: 'Request Slack connect',
|
||||
},
|
||||
];
|
||||
|
||||
describe('Help and Support renders correctly', () => {
|
||||
it('should render the support page with all support channels', () => {
|
||||
const { getByText } = render(<Support />);
|
||||
expect(getByText('Support')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(
|
||||
'We are here to help in case of questions or issues. Pick the channel that is most convenient for you.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
supportChannels.forEach((channel) => {
|
||||
expect(getByText(channel.name)).toBeInTheDocument();
|
||||
expect(getByText(channel.title)).toBeInTheDocument();
|
||||
expect(getByText(channel.btnText)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should trigger correct handler function when channel button is clicked', () => {
|
||||
const { getByText } = render(<Support />);
|
||||
const button = getByText('Visit docs');
|
||||
const windowOpenMock = jest.spyOn(window, 'open').mockImplementation();
|
||||
fireEvent.click(button);
|
||||
expect(windowOpenMock).toHaveBeenCalledWith(
|
||||
'https://signoz.io/docs/',
|
||||
'_blank',
|
||||
);
|
||||
});
|
||||
|
||||
it('should open Intercom chat widget when Chat channel is clicked', () => {
|
||||
window.Intercom = jest.fn();
|
||||
const { getByText } = render(<Support />);
|
||||
const button = getByText(launchChat);
|
||||
fireEvent.click(button);
|
||||
expect(window.Intercom).toHaveBeenCalledWith('show');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Handle channels with null or undefined properties', () => {
|
||||
it('should handle window.Intercom being undefined or null', () => {
|
||||
window.Intercom = null;
|
||||
const { getByText } = render(<Support />);
|
||||
const button = getByText(launchChat);
|
||||
fireEvent.click(button);
|
||||
expect(window.Intercom).toBeNull();
|
||||
});
|
||||
it('should handle missing or undefined history.location.state', () => {
|
||||
const trackEvent = jest.fn();
|
||||
useHistoryMock.mockReturnValue({ location: {} });
|
||||
render(<Support />);
|
||||
expect(trackEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should handle missing or undefined channel.url', () => {
|
||||
const openSpy = jest.spyOn(window, 'open').mockImplementation();
|
||||
const { getByText } = render(<Support />);
|
||||
fireEvent.click(getByText(launchChat));
|
||||
expect(openSpy).not.toHaveBeenCalled();
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
it('should handle missing or undefined channel.name', () => {
|
||||
const trackEvent = jest.fn();
|
||||
const handleChannelClick = jest.fn();
|
||||
const channelWithoutName = { key: 'chat', url: '' };
|
||||
render(<Support />);
|
||||
handleChannelClick(channelWithoutName);
|
||||
expect(trackEvent).not.toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
VerticalAlignTopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Flex, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@@ -197,6 +198,11 @@ export function Filter(props: FilterProps): JSX.Element {
|
||||
})),
|
||||
},
|
||||
};
|
||||
if (selectedFilters) {
|
||||
logEvent('Traces Explorer: Sidebar filter used', {
|
||||
selectedFilters,
|
||||
});
|
||||
}
|
||||
redirectWithQueryBuilderData(preparedQuery);
|
||||
},
|
||||
[currentQuery, redirectWithQueryBuilderData, selectedFilters],
|
||||
|
||||
@@ -3,6 +3,7 @@ import './TracesExplorer.styles.scss';
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Button, Card, Tabs, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import axios from 'axios';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@@ -25,7 +26,7 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { cloneDeep, isEmpty, set } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -135,7 +136,7 @@ function TracesExplorer(): JSX.Element {
|
||||
};
|
||||
|
||||
const handleExport = useCallback(
|
||||
(dashboard: Dashboard | null): void => {
|
||||
(dashboard: Dashboard | null, isNewDashboard?: boolean): void => {
|
||||
if (!dashboard || !panelType) return;
|
||||
|
||||
const panelTypeParam = AVAILABLE_EXPORT_PANEL_TYPES.includes(panelType)
|
||||
@@ -157,6 +158,12 @@ function TracesExplorer(): JSX.Element {
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
logEvent('Traces Explorer: Add to dashboard successful', {
|
||||
panelType,
|
||||
isNewDashboard,
|
||||
dashboardName: dashboard?.data?.title,
|
||||
});
|
||||
|
||||
updateDashboard(updatedDashboard, {
|
||||
onSuccess: (data) => {
|
||||
if (data.error) {
|
||||
@@ -223,6 +230,13 @@ function TracesExplorer(): JSX.Element {
|
||||
currentPanelType,
|
||||
]);
|
||||
const [isOpen, setOpen] = useState<boolean>(true);
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
logEvent('Traces Explorer: Page visited', {});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Modal } from 'antd';
|
||||
import getDashboard from 'api/dashboard/get';
|
||||
import lockDashboardApi from 'api/dashboard/lockDashboard';
|
||||
@@ -11,6 +12,7 @@ import useAxiosError from 'hooks/useAxiosError';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||
import history from 'lib/history';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import isUndefined from 'lodash-es/isUndefined';
|
||||
@@ -38,7 +40,7 @@ import AppReducer from 'types/reducer/app';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
import { IDashboardContext } from './types';
|
||||
import { DashboardSortOrder, IDashboardContext } from './types';
|
||||
import { sortLayout } from './util';
|
||||
|
||||
const DashboardContext = createContext<IDashboardContext>({
|
||||
@@ -52,7 +54,12 @@ const DashboardContext = createContext<IDashboardContext>({
|
||||
layouts: [],
|
||||
panelMap: {},
|
||||
setPanelMap: () => {},
|
||||
listSortOrder: { columnKey: 'createdAt', order: 'descend', pagination: '1' },
|
||||
listSortOrder: {
|
||||
columnKey: 'createdAt',
|
||||
order: 'descend',
|
||||
pagination: '1',
|
||||
search: '',
|
||||
},
|
||||
setListSortOrder: () => {},
|
||||
setLayouts: () => {},
|
||||
setSelectedDashboard: () => {},
|
||||
@@ -68,6 +75,7 @@ interface Props {
|
||||
dashboardId: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function DashboardProvider({
|
||||
children,
|
||||
}: PropsWithChildren): JSX.Element {
|
||||
@@ -82,17 +90,50 @@ export function DashboardProvider({
|
||||
exact: true,
|
||||
});
|
||||
|
||||
const params = useUrlQuery();
|
||||
const orderColumnParam = params.get('columnKey');
|
||||
const orderQueryParam = params.get('order');
|
||||
const paginationParam = params.get('page');
|
||||
|
||||
const [listSortOrder, setListSortOrder] = useState({
|
||||
columnKey: orderColumnParam || 'updatedAt',
|
||||
order: orderQueryParam || 'descend',
|
||||
pagination: paginationParam || '1',
|
||||
const isDashboardListPage = useRouteMatch<Props>({
|
||||
path: ROUTES.ALL_DASHBOARD,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
// added extra checks here in case wrong values appear use the default values rather than empty dashboards
|
||||
const supportedOrderColumnKeys = ['createdAt', 'updatedAt'];
|
||||
|
||||
const supportedOrderKeys = ['ascend', 'descend'];
|
||||
|
||||
const params = useUrlQuery();
|
||||
// since the dashboard provider is wrapped at the very top of the application hence it initialises these values from other pages as well.
|
||||
// pick the below params from URL only if the user is on the dashboards list page.
|
||||
const orderColumnParam = isDashboardListPage && params.get('columnKey');
|
||||
const orderQueryParam = isDashboardListPage && params.get('order');
|
||||
const paginationParam = isDashboardListPage && params.get('page');
|
||||
const searchParam = isDashboardListPage && params.get('search');
|
||||
|
||||
const [listSortOrder, setListOrder] = useState({
|
||||
columnKey: orderColumnParam
|
||||
? supportedOrderColumnKeys.includes(orderColumnParam)
|
||||
? orderColumnParam
|
||||
: 'updatedAt'
|
||||
: 'updatedAt',
|
||||
order: orderQueryParam
|
||||
? supportedOrderKeys.includes(orderQueryParam)
|
||||
? orderQueryParam
|
||||
: 'descend'
|
||||
: 'descend',
|
||||
pagination: paginationParam || '1',
|
||||
search: searchParam || '',
|
||||
});
|
||||
|
||||
function setListSortOrder(sortOrder: DashboardSortOrder): void {
|
||||
if (!isEqual(sortOrder, listSortOrder)) {
|
||||
setListOrder(sortOrder);
|
||||
}
|
||||
params.set('columnKey', sortOrder.columnKey as string);
|
||||
params.set('order', sortOrder.order as string);
|
||||
params.set('page', sortOrder.pagination || '1');
|
||||
params.set('search', sortOrder.search || '');
|
||||
history.replace({ search: params.toString() });
|
||||
}
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface DashboardSortOrder {
|
||||
columnKey: string;
|
||||
order: string;
|
||||
pagination: string;
|
||||
search: string;
|
||||
}
|
||||
|
||||
export interface IDashboardContext {
|
||||
isDashboardSliderOpen: boolean;
|
||||
isDashboardLocked: boolean;
|
||||
@@ -15,18 +21,8 @@ export interface IDashboardContext {
|
||||
layouts: Layout[];
|
||||
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
|
||||
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||
listSortOrder: {
|
||||
columnKey: string;
|
||||
order: string;
|
||||
pagination: string;
|
||||
};
|
||||
setListSortOrder: Dispatch<
|
||||
SetStateAction<{
|
||||
columnKey: string;
|
||||
order: string;
|
||||
pagination: string;
|
||||
}>
|
||||
>;
|
||||
listSortOrder: DashboardSortOrder;
|
||||
setListSortOrder: (sortOrder: DashboardSortOrder) => void;
|
||||
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
|
||||
setSelectedDashboard: React.Dispatch<
|
||||
React.SetStateAction<Dashboard | undefined>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import { parseQuery } from 'lib/logql';
|
||||
import { OrderPreferenceItems } from 'pages/Logs/config';
|
||||
import {
|
||||
@@ -29,6 +30,30 @@ import {
|
||||
} from 'types/actions/logs';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
const supportedLogsOrder = [
|
||||
OrderPreferenceItems.ASC,
|
||||
OrderPreferenceItems.DESC,
|
||||
];
|
||||
|
||||
function getLogsOrder(): OrderPreferenceItems {
|
||||
// set the value of order from the URL only when order query param is present and the user is landing on the old logs explorer page
|
||||
if (window.location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||
const orderParam = new URLSearchParams(window.location.search).get('order');
|
||||
|
||||
if (orderParam) {
|
||||
// check if the order passed is supported else pass the default order
|
||||
if (supportedLogsOrder.includes(orderParam as OrderPreferenceItems)) {
|
||||
return orderParam as OrderPreferenceItems;
|
||||
}
|
||||
|
||||
return OrderPreferenceItems.DESC;
|
||||
}
|
||||
return OrderPreferenceItems.DESC;
|
||||
}
|
||||
|
||||
return OrderPreferenceItems.DESC;
|
||||
}
|
||||
|
||||
const initialState: ILogsReducer = {
|
||||
fields: {
|
||||
interesting: [],
|
||||
@@ -51,10 +76,7 @@ const initialState: ILogsReducer = {
|
||||
liveTailStartRange: 15,
|
||||
selectedLogId: null,
|
||||
detailedLog: null,
|
||||
order:
|
||||
(new URLSearchParams(window.location.search).get(
|
||||
'order',
|
||||
) as ILogsReducer['order']) ?? OrderPreferenceItems.DESC,
|
||||
order: getLogsOrder(),
|
||||
};
|
||||
|
||||
export const LogsReducer = (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import '@signozhq/design-tokens/dist/style.css';
|
||||
@import 'overlayscrollbars/overlayscrollbars.css';
|
||||
|
||||
@import './periscope.scss';
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ export type Span = [
|
||||
string[],
|
||||
string[],
|
||||
boolean,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
];
|
||||
|
||||
export interface ITraceTree {
|
||||
@@ -49,6 +52,9 @@ export interface ITraceTree {
|
||||
hasError?: boolean;
|
||||
event?: ITraceEvents[];
|
||||
isMissing?: boolean;
|
||||
spanKind: string;
|
||||
statusCodeString: string;
|
||||
statusMessage: string;
|
||||
childReferences?: Record<string, string>[];
|
||||
nonChildReferences?: Record<string, string>[];
|
||||
// For internal use
|
||||
|
||||
@@ -49,7 +49,10 @@ Object {
|
||||
"nonChildReferences": Array [],
|
||||
"serviceColour": "",
|
||||
"serviceName": "frontend",
|
||||
"spanKind": "Lorem Ipsum2",
|
||||
"startTime": 1657275433246,
|
||||
"statusCodeString": "Unset2",
|
||||
"statusMessage": "Server2",
|
||||
"tags": Array [
|
||||
Object {
|
||||
"key": "host.name.span3",
|
||||
@@ -78,7 +81,10 @@ Object {
|
||||
"nonChildReferences": Array [],
|
||||
"serviceColour": "",
|
||||
"serviceName": "frontend",
|
||||
"spanKind": "Lorem Ipsum1",
|
||||
"startTime": 1657275433246,
|
||||
"statusCodeString": "Unset1",
|
||||
"statusMessage": "Server1",
|
||||
"tags": Array [
|
||||
Object {
|
||||
"key": "host.name.span2",
|
||||
@@ -106,7 +112,10 @@ Object {
|
||||
"nonChildReferences": Array [],
|
||||
"serviceColour": "",
|
||||
"serviceName": "frontend",
|
||||
"spanKind": "Lorem Ipsum",
|
||||
"startTime": 1657275433246,
|
||||
"statusCodeString": "Unset",
|
||||
"statusMessage": "Server",
|
||||
"tags": Array [
|
||||
Object {
|
||||
"key": "host.name.span1",
|
||||
@@ -152,7 +161,10 @@ Object {
|
||||
"nonChildReferences": Array [],
|
||||
"serviceColour": "",
|
||||
"serviceName": "frontend",
|
||||
"spanKind": "Lorem Ipsum2",
|
||||
"startTime": 1657275433246,
|
||||
"statusCodeString": "Unset2",
|
||||
"statusMessage": "Server2",
|
||||
"tags": Array [
|
||||
Object {
|
||||
"key": "host.name.span3",
|
||||
@@ -168,7 +180,10 @@ Object {
|
||||
"name": "Missing Span (span_2)",
|
||||
"serviceColour": "",
|
||||
"serviceName": "",
|
||||
"spanKind": "",
|
||||
"startTime": null,
|
||||
"statusCodeString": "",
|
||||
"statusMessage": "",
|
||||
"tags": Array [],
|
||||
"time": null,
|
||||
"value": null,
|
||||
@@ -201,7 +216,10 @@ Object {
|
||||
"nonChildReferences": Array [],
|
||||
"serviceColour": "",
|
||||
"serviceName": "frontend",
|
||||
"spanKind": "Lorem Ipsum",
|
||||
"startTime": 1657275433246,
|
||||
"statusCodeString": "Unset",
|
||||
"statusMessage": "Server",
|
||||
"tags": Array [
|
||||
Object {
|
||||
"key": "host.name.span1",
|
||||
|
||||
@@ -16,6 +16,9 @@ export const TraceData: Span[] = [
|
||||
'{"timeUnixNano":1657275433246142000,"attributeMap":{"event":"HTTP request received S1","level":"info","method":"GET","url":"/dispatch?customer=392\\u0026nonse=0.015296363321630757"}}',
|
||||
],
|
||||
false,
|
||||
'Server',
|
||||
'Unset',
|
||||
'Lorem Ipsum',
|
||||
],
|
||||
[
|
||||
1657275433246,
|
||||
@@ -32,6 +35,9 @@ export const TraceData: Span[] = [
|
||||
'{"timeUnixNano":1657275433246142000,"attributeMap":{"event":"HTTP request received S2","level":"info","method":"GET","url":"/dispatch?customer=392\\u0026nonse=0.015296363321630757"}}',
|
||||
],
|
||||
false,
|
||||
'Server1',
|
||||
'Unset1',
|
||||
'Lorem Ipsum1',
|
||||
],
|
||||
[
|
||||
1657275433246,
|
||||
@@ -48,5 +54,8 @@ export const TraceData: Span[] = [
|
||||
'{"timeUnixNano":1657275433246142000,"attributeMap":{"event":"HTTP request received S3","level":"info","method":"GET","url":"/dispatch?customer=392\\u0026nonse=0.015296363321630757"}}',
|
||||
],
|
||||
false,
|
||||
'Server2',
|
||||
'Unset2',
|
||||
'Lorem Ipsum2',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -71,6 +71,9 @@ export const spanToTreeUtil = (inputSpanList: Span[]): ITraceForest => {
|
||||
time: null as never,
|
||||
value: null as never,
|
||||
isMissing: true,
|
||||
statusMessage: '',
|
||||
statusCodeString: '',
|
||||
spanKind: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -93,6 +96,9 @@ export const spanToTreeUtil = (inputSpanList: Span[]): ITraceForest => {
|
||||
event: span[10]?.map((e) => JSON.parse(e || '{}') || {}),
|
||||
childReferences,
|
||||
nonChildReferences,
|
||||
statusMessage: span[12],
|
||||
statusCodeString: span[13],
|
||||
spanKind: span[14],
|
||||
};
|
||||
spanMap[span[1]] = spanObject;
|
||||
});
|
||||
|
||||
@@ -13028,6 +13028,16 @@ outvariant@^1.2.1, outvariant@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.0.tgz#e742e4bda77692da3eca698ef5bfac62d9fba06e"
|
||||
integrity sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==
|
||||
|
||||
overlayscrollbars-react@^0.5.6:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/overlayscrollbars-react/-/overlayscrollbars-react-0.5.6.tgz#e9779f9fc2c1a3288570a45c83f8e42518bfb8c1"
|
||||
integrity sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==
|
||||
|
||||
overlayscrollbars@^2.8.1:
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/overlayscrollbars/-/overlayscrollbars-2.9.2.tgz#056020a3811742b58b754fab6f775d49bd109be9"
|
||||
integrity sha512-iDT84r39i7oWP72diZN2mbJUsn/taCq568aQaIrc84S87PunBT7qtsVltAF2esk7ORTRjQDnfjVYoqqTzgs8QA==
|
||||
|
||||
p-limit@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz"
|
||||
@@ -14678,7 +14688,7 @@ react-use@17.4.0, react-use@^17.3.2:
|
||||
|
||||
react-virtuoso@4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.0.3.tgz"
|
||||
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.0.3.tgz#0dc8b10978095852d985b064157639b9fb9d9b1e"
|
||||
integrity sha512-tyqt8FBWxO+smve/kUgJbhCI2MEOvH2hHgFYPKWBMA2cJmV+cHIDDh1BL/6w4pg/dcCdlHCNVwi6aiztPxWttw==
|
||||
|
||||
react@18.2.0:
|
||||
|
||||
@@ -63,7 +63,7 @@ func EnrichmentRequired(params *v3.QueryRangeParamsV3) bool {
|
||||
|
||||
func isEnriched(field v3.AttributeKey) bool {
|
||||
// if it is timestamp/id dont check
|
||||
if field.Key == "timestamp" || field.Key == "id" || field.Key == constants.SigNozOrderByValue {
|
||||
if field.Key == "timestamp" || field.Key == "id" || field.Key == constants.SigNozOrderByValue || field.Type == v3.AttributeKeyTypeInstrumentationScope {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ func getClickhouseLogsColumnType(columnType v3.AttributeKeyType) string {
|
||||
if columnType == v3.AttributeKeyTypeTag {
|
||||
return "attributes"
|
||||
}
|
||||
if columnType == v3.AttributeKeyTypeInstrumentationScope {
|
||||
return "scope"
|
||||
}
|
||||
return "resources"
|
||||
}
|
||||
|
||||
@@ -477,7 +480,7 @@ type Options struct {
|
||||
}
|
||||
|
||||
func isOrderByTs(orderBy []v3.OrderBy) bool {
|
||||
if len(orderBy) == 1 && orderBy[0].Key == constants.TIMESTAMP {
|
||||
if len(orderBy) == 1 && (orderBy[0].Key == constants.TIMESTAMP || orderBy[0].ColumnName == constants.TIMESTAMP) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -53,6 +53,11 @@ var testGetClickhouseColumnNameData = []struct {
|
||||
AttributeKey: v3.AttributeKey{Key: "test-attr", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag, IsColumn: true},
|
||||
ExpectedColumnName: "`attribute_string_test-attr`",
|
||||
},
|
||||
{
|
||||
Name: "instrumentation scope attribute",
|
||||
AttributeKey: v3.AttributeKey{Key: "version", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeInstrumentationScope, IsColumn: false},
|
||||
ExpectedColumnName: "scope_string_value[indexOf(scope_string_key, 'version')]",
|
||||
},
|
||||
}
|
||||
|
||||
func TestGetClickhouseColumnName(t *testing.T) {
|
||||
@@ -130,6 +135,13 @@ var timeSeriesFilterQueryData = []struct {
|
||||
}},
|
||||
ExpectedFilter: "attributes_string_value[indexOf(attributes_string_key, 'user_name')] = 'john' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] != 'my_service'",
|
||||
},
|
||||
{
|
||||
Name: "Test instrumentation scope attribute",
|
||||
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "version", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeInstrumentationScope}, Value: "v1", Operator: "="},
|
||||
}},
|
||||
ExpectedFilter: "scope_string_value[indexOf(scope_string_key, 'version')] = 'v1'",
|
||||
},
|
||||
{
|
||||
Name: "Test materialized column",
|
||||
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
@@ -734,10 +746,11 @@ var testBuildLogsQueryData = []struct {
|
||||
Expression: "A",
|
||||
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}},
|
||||
},
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string," +
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string," +
|
||||
"CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64," +
|
||||
"CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool," +
|
||||
"CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " +
|
||||
"CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, " +
|
||||
"CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope " +
|
||||
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) order by timestamp DESC",
|
||||
},
|
||||
{
|
||||
@@ -753,10 +766,11 @@ var testBuildLogsQueryData = []struct {
|
||||
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}},
|
||||
OrderBy: []v3.OrderBy{{ColumnName: "method", DataType: v3.AttributeKeyDataTypeString, Order: "ASC", IsColumn: true}},
|
||||
},
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string," +
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string," +
|
||||
"CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64," +
|
||||
"CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool," +
|
||||
"CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " +
|
||||
"CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, " +
|
||||
"CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope " +
|
||||
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) order by `method` ASC",
|
||||
},
|
||||
{
|
||||
@@ -773,12 +787,35 @@ var testBuildLogsQueryData = []struct {
|
||||
{Key: v3.AttributeKey{Key: "severity_number", DataType: v3.AttributeKeyDataTypeInt64, IsColumn: true}, Operator: "!=", Value: 0},
|
||||
}},
|
||||
},
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string," +
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string," +
|
||||
"CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64," +
|
||||
"CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool," +
|
||||
"CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " +
|
||||
"CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, " +
|
||||
"CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope " +
|
||||
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND severity_number != 0 order by timestamp DESC",
|
||||
},
|
||||
{
|
||||
Name: "Test Noop with scope filter",
|
||||
PanelType: v3.PanelTypeList,
|
||||
Start: 1680066360726210000,
|
||||
End: 1680066458000000000,
|
||||
BuilderQuery: &v3.BuilderQuery{
|
||||
SelectColumns: []v3.AttributeKey{},
|
||||
QueryName: "A",
|
||||
AggregateOperator: v3.AggregateOperatorNoOp,
|
||||
Expression: "A",
|
||||
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "scope_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Operator: "=", Value: "app"},
|
||||
{Key: v3.AttributeKey{Key: "scope_version", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Operator: "=", Value: "version"},
|
||||
}},
|
||||
},
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string," +
|
||||
"CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64," +
|
||||
"CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool," +
|
||||
"CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, " +
|
||||
"CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope " +
|
||||
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND scope_name = 'app' AND scope_version = 'version' order by timestamp DESC",
|
||||
},
|
||||
{
|
||||
Name: "Test aggregate with having clause",
|
||||
PanelType: v3.PanelTypeGraph,
|
||||
@@ -1308,9 +1345,13 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
},
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND ",
|
||||
Options: Options{IsLivetailQuery: true},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body," +
|
||||
"CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') " +
|
||||
"as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') " +
|
||||
"as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope " +
|
||||
"from signoz_logs.distributed_logs where attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND ",
|
||||
Options: Options{IsLivetailQuery: true},
|
||||
},
|
||||
{
|
||||
Name: "Live Tail Query with contains",
|
||||
@@ -1327,9 +1368,12 @@ var testPrepLogsQueryData = []struct {
|
||||
},
|
||||
},
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where attributes_string_value[indexOf(attributes_string_key, 'method')] ILIKE '%GET%' AND ",
|
||||
Options: Options{IsLivetailQuery: true},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') " +
|
||||
"as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') " +
|
||||
"as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope " +
|
||||
"from signoz_logs.distributed_logs where attributes_string_value[indexOf(attributes_string_key, 'method')] ILIKE '%GET%' AND ",
|
||||
Options: Options{IsLivetailQuery: true},
|
||||
},
|
||||
{
|
||||
Name: "Live Tail Query W/O filter",
|
||||
@@ -1343,9 +1387,12 @@ var testPrepLogsQueryData = []struct {
|
||||
Expression: "A",
|
||||
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}},
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where ",
|
||||
Options: Options{IsLivetailQuery: true},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') " +
|
||||
"as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') " +
|
||||
"as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope " +
|
||||
"from signoz_logs.distributed_logs where ",
|
||||
Options: Options{IsLivetailQuery: true},
|
||||
},
|
||||
{
|
||||
Name: "Table query w/o limit",
|
||||
@@ -1380,6 +1427,66 @@ var testPrepLogsQueryData = []struct {
|
||||
ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by value DESC LIMIT 10",
|
||||
Options: Options{},
|
||||
},
|
||||
{
|
||||
Name: "Ignore offset if order by is timestamp in list queries",
|
||||
PanelType: v3.PanelTypeList,
|
||||
Start: 1680066360726,
|
||||
End: 1680066458000,
|
||||
BuilderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
AggregateOperator: v3.AggregateOperatorNoOp,
|
||||
Expression: "A",
|
||||
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "logid", Operator: "<"},
|
||||
},
|
||||
},
|
||||
OrderBy: []v3.OrderBy{
|
||||
{
|
||||
ColumnName: "timestamp",
|
||||
Order: "DESC",
|
||||
},
|
||||
},
|
||||
Offset: 100,
|
||||
PageSize: 100,
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as " +
|
||||
"attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as " +
|
||||
"attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " +
|
||||
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND id < 'logid' order by " +
|
||||
"timestamp DESC LIMIT 100",
|
||||
},
|
||||
{
|
||||
Name: "Don't ignore offset if order by is not timestamp",
|
||||
PanelType: v3.PanelTypeList,
|
||||
Start: 1680066360726,
|
||||
End: 1680066458000,
|
||||
BuilderQuery: &v3.BuilderQuery{
|
||||
QueryName: "A",
|
||||
StepInterval: 60,
|
||||
AggregateOperator: v3.AggregateOperatorNoOp,
|
||||
Expression: "A",
|
||||
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="},
|
||||
},
|
||||
},
|
||||
OrderBy: []v3.OrderBy{
|
||||
{
|
||||
ColumnName: "mycolumn",
|
||||
Order: "DESC",
|
||||
},
|
||||
},
|
||||
Offset: 100,
|
||||
PageSize: 100,
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as " +
|
||||
"attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as " +
|
||||
"attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " +
|
||||
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' order by " +
|
||||
"resources_string_value[indexOf(resources_string_key, 'mycolumn')] DESC LIMIT 100 OFFSET 100",
|
||||
},
|
||||
}
|
||||
|
||||
func TestPrepareLogsQuery(t *testing.T) {
|
||||
@@ -1422,8 +1529,11 @@ var testPrepLogsQueryLimitOffsetData = []struct {
|
||||
Offset: 0,
|
||||
PageSize: 5,
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by `timestamp` desc LIMIT 1",
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') " +
|
||||
"as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') " +
|
||||
"as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope " +
|
||||
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by `timestamp` desc LIMIT 1",
|
||||
},
|
||||
{
|
||||
Name: "Test limit greater than pageSize - order by ts",
|
||||
@@ -1443,8 +1553,11 @@ var testPrepLogsQueryLimitOffsetData = []struct {
|
||||
Offset: 10,
|
||||
PageSize: 10,
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by `timestamp` desc LIMIT 10",
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') " +
|
||||
"as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') " +
|
||||
"as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope " +
|
||||
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by `timestamp` desc LIMIT 10",
|
||||
},
|
||||
{
|
||||
Name: "Test limit less than pageSize - order by custom",
|
||||
@@ -1462,8 +1575,11 @@ var testPrepLogsQueryLimitOffsetData = []struct {
|
||||
Offset: 0,
|
||||
PageSize: 5,
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 1 OFFSET 0",
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') " +
|
||||
"as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') " +
|
||||
"as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope " +
|
||||
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 1 OFFSET 0",
|
||||
},
|
||||
{
|
||||
Name: "Test limit greater than pageSize - order by custom",
|
||||
@@ -1483,8 +1599,11 @@ var testPrepLogsQueryLimitOffsetData = []struct {
|
||||
Offset: 50,
|
||||
PageSize: 50,
|
||||
},
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 50 OFFSET 50",
|
||||
TableName: "logs",
|
||||
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') " +
|
||||
"as attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') " +
|
||||
"as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope " +
|
||||
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND id < '2TNh4vp2TpiWyLt3SzuadLJF2s4' order by attributes_string_value[indexOf(attributes_string_key, 'method')] desc LIMIT 50 OFFSET 50",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func prepareLogsQuery(ctx context.Context,
|
||||
func prepareLogsQuery(_ context.Context,
|
||||
start,
|
||||
end int64,
|
||||
builderQuery *v3.BuilderQuery,
|
||||
|
||||
@@ -50,8 +50,10 @@ type querier struct {
|
||||
// TODO(srikanthccv): remove this once we have a proper mock
|
||||
testingMode bool
|
||||
queriesExecuted []string
|
||||
returnedSeries []*v3.Series
|
||||
returnedErr error
|
||||
// tuple of start and end time in milliseconds
|
||||
timeRanges [][]int
|
||||
returnedSeries []*v3.Series
|
||||
returnedErr error
|
||||
}
|
||||
|
||||
type QuerierOptions struct {
|
||||
@@ -117,6 +119,7 @@ func (q *querier) execClickHouseQuery(ctx context.Context, query string) ([]*v3.
|
||||
func (q *querier) execPromQuery(ctx context.Context, params *model.QueryRangeParams) ([]*v3.Series, error) {
|
||||
q.queriesExecuted = append(q.queriesExecuted, params.Query)
|
||||
if q.testingMode && q.reader == nil {
|
||||
q.timeRanges = append(q.timeRanges, []int{int(params.Start.UnixMilli()), int(params.End.UnixMilli())})
|
||||
return q.returnedSeries, q.returnedErr
|
||||
}
|
||||
promResult, _, err := q.reader.GetQueryRangeResult(ctx, params)
|
||||
@@ -342,10 +345,10 @@ func (q *querier) runPromQueries(ctx context.Context, params *v3.QueryRangeParam
|
||||
wg.Add(1)
|
||||
go func(queryName string, promQuery *v3.PromQuery) {
|
||||
defer wg.Done()
|
||||
cacheKey := cacheKeys[queryName]
|
||||
cacheKey, ok := cacheKeys[queryName]
|
||||
var cachedData []byte
|
||||
// Ensure NoCache is not set and cache is not nil
|
||||
if !params.NoCache && q.cache != nil {
|
||||
if !params.NoCache && q.cache != nil && ok {
|
||||
data, retrieveStatus, err := q.cache.Retrieve(cacheKey, true)
|
||||
zap.L().Info("cache retrieve status", zap.String("status", retrieveStatus.String()))
|
||||
if err == nil {
|
||||
@@ -373,7 +376,7 @@ func (q *querier) runPromQueries(ctx context.Context, params *v3.QueryRangeParam
|
||||
channelResults <- channelResult{Err: nil, Name: queryName, Query: promQuery.Query, Series: mergedSeries}
|
||||
|
||||
// Cache the seriesList for future queries
|
||||
if len(missedSeries) > 0 && !params.NoCache && q.cache != nil {
|
||||
if len(missedSeries) > 0 && !params.NoCache && q.cache != nil && ok {
|
||||
mergedSeriesData, err := json.Marshal(mergedSeries)
|
||||
if err != nil {
|
||||
zap.L().Error("error marshalling merged series", zap.Error(err))
|
||||
@@ -546,3 +549,7 @@ func (q *querier) QueryRange(ctx context.Context, params *v3.QueryRangeParamsV3,
|
||||
func (q *querier) QueriesExecuted() []string {
|
||||
return q.queriesExecuted
|
||||
}
|
||||
|
||||
func (q *querier) TimeRanges() [][]int {
|
||||
return q.timeRanges
|
||||
}
|
||||
|
||||
@@ -951,3 +951,102 @@ func TestQueryRangeTimeShiftWithLimitAndCache(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRangeValueTypePromQL(t *testing.T) {
|
||||
// There shouldn't be any caching for value panel type
|
||||
params := []*v3.QueryRangeParamsV3{
|
||||
{
|
||||
Start: 1675115596722,
|
||||
End: 1675115596722 + 120*60*1000,
|
||||
Step: 5 * time.Minute.Milliseconds(),
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypePromQL,
|
||||
PanelType: v3.PanelTypeValue,
|
||||
PromQueries: map[string]*v3.PromQuery{
|
||||
"A": {
|
||||
Query: "signoz_calls_total",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Start: 1675115596722 + 60*60*1000,
|
||||
End: 1675115596722 + 180*60*1000,
|
||||
Step: 5 * time.Minute.Milliseconds(),
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypePromQL,
|
||||
PanelType: v3.PanelTypeValue,
|
||||
PromQueries: map[string]*v3.PromQuery{
|
||||
"A": {
|
||||
Query: "signoz_latency_bucket",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
cache := inmemory.New(&inmemory.Options{TTL: 60 * time.Minute, CleanupInterval: 10 * time.Minute})
|
||||
opts := QuerierOptions{
|
||||
Cache: cache,
|
||||
Reader: nil,
|
||||
FluxInterval: 5 * time.Minute,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
|
||||
TestingMode: true,
|
||||
ReturnedSeries: []*v3.Series{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"method": "GET",
|
||||
"service_name": "test",
|
||||
"__name__": "doesn't matter",
|
||||
},
|
||||
Points: []v3.Point{
|
||||
{Timestamp: 1675115596722, Value: 1},
|
||||
{Timestamp: 1675115596722 + 60*60*1000, Value: 2},
|
||||
{Timestamp: 1675115596722 + 120*60*1000, Value: 3},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
q := NewQuerier(opts)
|
||||
|
||||
expectedQueryAndTimeRanges := []struct {
|
||||
query string
|
||||
ranges []missInterval
|
||||
}{
|
||||
{
|
||||
query: "signoz_calls_total",
|
||||
ranges: []missInterval{
|
||||
{start: 1675115596722, end: 1675115596722 + 120*60*1000},
|
||||
},
|
||||
},
|
||||
{
|
||||
query: "signoz_latency_bucket",
|
||||
ranges: []missInterval{
|
||||
{start: 1675115596722 + 60*60*1000, end: 1675115596722 + 180*60*1000},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, param := range params {
|
||||
_, errByName, err := q.QueryRange(context.Background(), param, nil)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %s", err)
|
||||
}
|
||||
if len(errByName) > 0 {
|
||||
t.Errorf("expected no error, got %v", errByName)
|
||||
}
|
||||
|
||||
if !strings.Contains(q.QueriesExecuted()[i], expectedQueryAndTimeRanges[i].query) {
|
||||
t.Errorf("expected query to contain %s, got %s", expectedQueryAndTimeRanges[i].query, q.QueriesExecuted()[i])
|
||||
}
|
||||
if len(q.TimeRanges()[i]) != 2 {
|
||||
t.Errorf("expected time ranges to be %v, got %v", expectedQueryAndTimeRanges[i].ranges, q.TimeRanges()[i])
|
||||
}
|
||||
if q.TimeRanges()[i][0] != int(expectedQueryAndTimeRanges[i].ranges[0].start) {
|
||||
t.Errorf("expected time ranges to be %v, got %v", expectedQueryAndTimeRanges[i].ranges, q.TimeRanges()[i])
|
||||
}
|
||||
if q.TimeRanges()[i][1] != int(expectedQueryAndTimeRanges[i].ranges[0].end) {
|
||||
t.Errorf("expected time ranges to be %v, got %v", expectedQueryAndTimeRanges[i].ranges, q.TimeRanges()[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func prepareLogsQuery(ctx context.Context,
|
||||
func prepareLogsQuery(_ context.Context,
|
||||
start,
|
||||
end int64,
|
||||
builderQuery *v3.BuilderQuery,
|
||||
|
||||
@@ -50,8 +50,10 @@ type querier struct {
|
||||
// TODO(srikanthccv): remove this once we have a proper mock
|
||||
testingMode bool
|
||||
queriesExecuted []string
|
||||
returnedSeries []*v3.Series
|
||||
returnedErr error
|
||||
// tuple of start and end time in milliseconds
|
||||
timeRanges [][]int
|
||||
returnedSeries []*v3.Series
|
||||
returnedErr error
|
||||
}
|
||||
|
||||
type QuerierOptions struct {
|
||||
@@ -117,6 +119,7 @@ func (q *querier) execClickHouseQuery(ctx context.Context, query string) ([]*v3.
|
||||
func (q *querier) execPromQuery(ctx context.Context, params *model.QueryRangeParams) ([]*v3.Series, error) {
|
||||
q.queriesExecuted = append(q.queriesExecuted, params.Query)
|
||||
if q.testingMode && q.reader == nil {
|
||||
q.timeRanges = append(q.timeRanges, []int{int(params.Start.UnixMilli()), int(params.End.UnixMilli())})
|
||||
return q.returnedSeries, q.returnedErr
|
||||
}
|
||||
promResult, _, err := q.reader.GetQueryRangeResult(ctx, params)
|
||||
@@ -335,10 +338,10 @@ func (q *querier) runPromQueries(ctx context.Context, params *v3.QueryRangeParam
|
||||
wg.Add(1)
|
||||
go func(queryName string, promQuery *v3.PromQuery) {
|
||||
defer wg.Done()
|
||||
cacheKey := cacheKeys[queryName]
|
||||
cacheKey, ok := cacheKeys[queryName]
|
||||
var cachedData []byte
|
||||
// Ensure NoCache is not set and cache is not nil
|
||||
if !params.NoCache && q.cache != nil {
|
||||
if !params.NoCache && q.cache != nil && ok {
|
||||
data, retrieveStatus, err := q.cache.Retrieve(cacheKey, true)
|
||||
zap.L().Info("cache retrieve status", zap.String("status", retrieveStatus.String()))
|
||||
if err == nil {
|
||||
@@ -366,7 +369,7 @@ func (q *querier) runPromQueries(ctx context.Context, params *v3.QueryRangeParam
|
||||
channelResults <- channelResult{Err: nil, Name: queryName, Query: promQuery.Query, Series: mergedSeries}
|
||||
|
||||
// Cache the seriesList for future queries
|
||||
if len(missedSeries) > 0 && !params.NoCache && q.cache != nil {
|
||||
if len(missedSeries) > 0 && !params.NoCache && q.cache != nil && ok {
|
||||
mergedSeriesData, err := json.Marshal(mergedSeries)
|
||||
if err != nil {
|
||||
zap.L().Error("error marshalling merged series", zap.Error(err))
|
||||
@@ -539,3 +542,7 @@ func (q *querier) QueryRange(ctx context.Context, params *v3.QueryRangeParamsV3,
|
||||
func (q *querier) QueriesExecuted() []string {
|
||||
return q.queriesExecuted
|
||||
}
|
||||
|
||||
func (q *querier) TimeRanges() [][]int {
|
||||
return q.timeRanges
|
||||
}
|
||||
|
||||
1060
pkg/query-service/app/querier/v2/querier_test.go
Normal file
1060
pkg/query-service/app/querier/v2/querier_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,10 @@ func GetAlertManagerApiPrefix() string {
|
||||
return "http://alertmanager:9093/api/"
|
||||
}
|
||||
|
||||
var TELEMETRY_HEART_BEAT_DURATION_MINUTES = GetOrDefaultEnvInt("TELEMETRY_HEART_BEAT_DURATION_MINUTES", 720)
|
||||
|
||||
var TELEMETRY_ACTIVE_USER_DURATION_MINUTES = GetOrDefaultEnvInt("TELEMETRY_ACTIVE_USER_DURATION_MINUTES", 360)
|
||||
|
||||
var InviteEmailTemplate = GetOrDefaultEnv("INVITE_EMAIL_TEMPLATE", "/root/templates/invitation_email_template.html")
|
||||
|
||||
// Alert manager channel subpath
|
||||
@@ -232,6 +236,18 @@ func GetOrDefaultEnv(key string, fallback string) string {
|
||||
return v
|
||||
}
|
||||
|
||||
func GetOrDefaultEnvInt(key string, fallback int) int {
|
||||
v := os.Getenv(key)
|
||||
if len(v) == 0 {
|
||||
return fallback
|
||||
}
|
||||
intVal, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return intVal
|
||||
}
|
||||
|
||||
const (
|
||||
STRING = "String"
|
||||
UINT32 = "UInt32"
|
||||
@@ -280,12 +296,13 @@ var StaticSelectedLogFields = []model.LogField{
|
||||
|
||||
const (
|
||||
LogsSQLSelect = "SELECT " +
|
||||
"timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body," +
|
||||
"timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body," +
|
||||
"CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string," +
|
||||
"CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64," +
|
||||
"CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64," +
|
||||
"CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool," +
|
||||
"CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string "
|
||||
"CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string, " +
|
||||
"CAST((scope_string_key, scope_string_value), 'Map(String, String)') as scope "
|
||||
TracesExplorerViewSQLSelectWithSubQuery = "WITH subQuery AS (SELECT distinct on (traceID) traceID, durationNano, " +
|
||||
"serviceName, name FROM %s.%s WHERE parentSpanID = '' AND %s %s ORDER BY durationNano DESC "
|
||||
TracesExplorerViewSQLSelectQuery = "SELECT subQuery.serviceName, subQuery.name, count() AS " +
|
||||
@@ -350,6 +367,18 @@ var StaticFieldsLogsV3 = map[string]v3.AttributeKey{
|
||||
Type: v3.AttributeKeyTypeUnspecified,
|
||||
IsColumn: true,
|
||||
},
|
||||
"scope_name": {
|
||||
Key: "scope_name",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeUnspecified,
|
||||
IsColumn: true,
|
||||
},
|
||||
"scope_version": {
|
||||
Key: "scope_version",
|
||||
DataType: v3.AttributeKeyDataTypeString,
|
||||
Type: v3.AttributeKeyTypeUnspecified,
|
||||
IsColumn: true,
|
||||
},
|
||||
}
|
||||
|
||||
const SigNozOrderByValue = "#SIGNOZ_VALUE"
|
||||
|
||||
@@ -110,4 +110,5 @@ type Querier interface {
|
||||
|
||||
// test helpers
|
||||
QueriesExecuted() []string
|
||||
TimeRanges() [][]int
|
||||
}
|
||||
|
||||
@@ -229,13 +229,14 @@ type AggregateAttributeRequest struct {
|
||||
type TagType string
|
||||
|
||||
const (
|
||||
TagTypeTag TagType = "tag"
|
||||
TagTypeResource TagType = "resource"
|
||||
TagTypeTag TagType = "tag"
|
||||
TagTypeResource TagType = "resource"
|
||||
TagTypeInstrumentationScope TagType = "scope"
|
||||
)
|
||||
|
||||
func (q TagType) Validate() error {
|
||||
switch q {
|
||||
case TagTypeTag, TagTypeResource:
|
||||
case TagTypeTag, TagTypeResource, TagTypeInstrumentationScope:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid tag type: %s", q)
|
||||
@@ -300,9 +301,10 @@ type FilterAttributeKeyResponse struct {
|
||||
type AttributeKeyType string
|
||||
|
||||
const (
|
||||
AttributeKeyTypeUnspecified AttributeKeyType = ""
|
||||
AttributeKeyTypeTag AttributeKeyType = "tag"
|
||||
AttributeKeyTypeResource AttributeKeyType = "resource"
|
||||
AttributeKeyTypeUnspecified AttributeKeyType = ""
|
||||
AttributeKeyTypeTag AttributeKeyType = "tag"
|
||||
AttributeKeyTypeResource AttributeKeyType = "resource"
|
||||
AttributeKeyTypeInstrumentationScope AttributeKeyType = "scope"
|
||||
)
|
||||
|
||||
type AttributeKey struct {
|
||||
@@ -327,7 +329,7 @@ func (a AttributeKey) Validate() error {
|
||||
|
||||
if a.IsColumn {
|
||||
switch a.Type {
|
||||
case AttributeKeyTypeResource, AttributeKeyTypeTag, AttributeKeyTypeUnspecified:
|
||||
case AttributeKeyTypeResource, AttributeKeyTypeTag, AttributeKeyTypeUnspecified, AttributeKeyTypeInstrumentationScope:
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("invalid attribute type: %s", a.Type)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-co-op/gocron"
|
||||
"gopkg.in/segmentio/analytics-go.v3"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
@@ -85,19 +86,11 @@ const api_key = "9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr"
|
||||
const IP_NOT_FOUND_PLACEHOLDER = "NA"
|
||||
const DEFAULT_NUMBER_OF_SERVICES = 6
|
||||
|
||||
const HEART_BEAT_DURATION = 12 * time.Hour
|
||||
|
||||
const ACTIVE_USER_DURATION = 6 * time.Hour
|
||||
|
||||
// const HEART_BEAT_DURATION = 30 * time.Second
|
||||
// const ACTIVE_USER_DURATION = 30 * time.Second
|
||||
const SCHEDULE_START_TIME = "04:00" // 4 AM UTC
|
||||
|
||||
const RATE_LIMIT_CHECK_DURATION = 1 * time.Minute
|
||||
const RATE_LIMIT_VALUE = 1
|
||||
|
||||
// const RATE_LIMIT_CHECK_DURATION = 20 * time.Second
|
||||
// const RATE_LIMIT_VALUE = 5
|
||||
|
||||
var telemetry *Telemetry
|
||||
var once sync.Once
|
||||
|
||||
@@ -213,8 +206,11 @@ func createTelemetry() {
|
||||
|
||||
telemetry.SetTelemetryEnabled(constants.IsTelemetryEnabled())
|
||||
|
||||
ticker := time.NewTicker(HEART_BEAT_DURATION)
|
||||
activeUserTicker := time.NewTicker(ACTIVE_USER_DURATION)
|
||||
// Create a new scheduler
|
||||
s := gocron.NewScheduler(time.UTC)
|
||||
|
||||
HEART_BEAT_DURATION := time.Duration(constants.TELEMETRY_HEART_BEAT_DURATION_MINUTES) * time.Minute
|
||||
ACTIVE_USER_DURATION := time.Duration(constants.TELEMETRY_ACTIVE_USER_DURATION_MINUTES) * time.Minute
|
||||
|
||||
rateLimitTicker := time.NewTicker(RATE_LIMIT_CHECK_DURATION)
|
||||
|
||||
@@ -227,141 +223,171 @@ func createTelemetry() {
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-activeUserTicker.C:
|
||||
if telemetry.activeUser["logs"] != 0 {
|
||||
getLogsInfoInLastHeartBeatInterval, err := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(context.Background(), ACTIVE_USER_DURATION)
|
||||
if err != nil && getLogsInfoInLastHeartBeatInterval == 0 {
|
||||
telemetry.activeUser["logs"] = 0
|
||||
}
|
||||
}
|
||||
if telemetry.activeUser["metrics"] != 0 {
|
||||
getSamplesInfoInLastHeartBeatInterval, err := telemetry.reader.GetSamplesInfoInLastHeartBeatInterval(context.Background(), ACTIVE_USER_DURATION)
|
||||
if err != nil && getSamplesInfoInLastHeartBeatInterval == 0 {
|
||||
telemetry.activeUser["metrics"] = 0
|
||||
}
|
||||
}
|
||||
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"]},
|
||||
"", true, false)
|
||||
telemetry.activeUser = map[string]int8{"traces": 0, "metrics": 0, "logs": 0, "any": 0}
|
||||
ctx := context.Background()
|
||||
// Define heartbeat function
|
||||
heartbeatFunc := func() {
|
||||
tagsInfo, _ := telemetry.reader.GetTagsInfoInLastHeartBeatInterval(ctx, HEART_BEAT_DURATION)
|
||||
|
||||
case <-ticker.C:
|
||||
if len(tagsInfo.Env) != 0 {
|
||||
telemetry.SendEvent(TELEMETRY_EVENT_ENVIRONMENT, map[string]interface{}{"value": tagsInfo.Env}, "", true, false)
|
||||
}
|
||||
|
||||
tagsInfo, _ := telemetry.reader.GetTagsInfoInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION)
|
||||
languages := []string{}
|
||||
for language := range tagsInfo.Languages {
|
||||
languages = append(languages, language)
|
||||
}
|
||||
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(ctx)
|
||||
totalLogs, _ := telemetry.reader.GetTotalLogs(ctx)
|
||||
spansInLastHeartBeatInterval, _ := telemetry.reader.GetSpansInLastHeartBeatInterval(ctx, HEART_BEAT_DURATION)
|
||||
getSamplesInfoInLastHeartBeatInterval, _ := telemetry.reader.GetSamplesInfoInLastHeartBeatInterval(ctx, HEART_BEAT_DURATION)
|
||||
totalSamples, _ := telemetry.reader.GetTotalSamples(ctx)
|
||||
tsInfo, _ := telemetry.reader.GetTimeSeriesInfo(ctx)
|
||||
|
||||
if len(tagsInfo.Env) != 0 {
|
||||
telemetry.SendEvent(TELEMETRY_EVENT_ENVIRONMENT, map[string]interface{}{"value": tagsInfo.Env}, "", true, false)
|
||||
}
|
||||
getLogsInfoInLastHeartBeatInterval, _ := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(ctx, HEART_BEAT_DURATION)
|
||||
|
||||
languages := []string{}
|
||||
for language := range tagsInfo.Languages {
|
||||
languages = append(languages, language)
|
||||
}
|
||||
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)
|
||||
getSamplesInfoInLastHeartBeatInterval, _ := telemetry.reader.GetSamplesInfoInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION)
|
||||
totalSamples, _ := telemetry.reader.GetTotalSamples(context.Background())
|
||||
tsInfo, _ := telemetry.reader.GetTimeSeriesInfo(context.Background())
|
||||
traceTTL, _ := telemetry.reader.GetTTL(ctx, &model.GetTTLParams{Type: constants.TraceTTL})
|
||||
metricsTTL, _ := telemetry.reader.GetTTL(ctx, &model.GetTTLParams{Type: constants.MetricsTTL})
|
||||
logsTTL, _ := telemetry.reader.GetTTL(ctx, &model.GetTTLParams{Type: constants.LogsTTL})
|
||||
|
||||
getLogsInfoInLastHeartBeatInterval, _ := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(context.Background(), HEART_BEAT_DURATION)
|
||||
data := map[string]interface{}{
|
||||
"totalSpans": totalSpans,
|
||||
"spansInLastHeartBeatInterval": spansInLastHeartBeatInterval,
|
||||
"totalSamples": totalSamples,
|
||||
"getSamplesInfoInLastHeartBeatInterval": getSamplesInfoInLastHeartBeatInterval,
|
||||
"totalLogs": totalLogs,
|
||||
"getLogsInfoInLastHeartBeatInterval": getLogsInfoInLastHeartBeatInterval,
|
||||
"countUsers": telemetry.countUsers,
|
||||
"metricsTTLStatus": metricsTTL.Status,
|
||||
"tracesTTLStatus": traceTTL.Status,
|
||||
"logsTTLStatus": logsTTL.Status,
|
||||
"patUser": telemetry.patTokenUser,
|
||||
}
|
||||
telemetry.patTokenUser = false
|
||||
for key, value := range tsInfo {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
traceTTL, _ := telemetry.reader.GetTTL(context.Background(), &model.GetTTLParams{Type: constants.TraceTTL})
|
||||
metricsTTL, _ := telemetry.reader.GetTTL(context.Background(), &model.GetTTLParams{Type: constants.MetricsTTL})
|
||||
logsTTL, _ := telemetry.reader.GetTTL(context.Background(), &model.GetTTLParams{Type: constants.LogsTTL})
|
||||
users, apiErr := telemetry.reader.GetUsers(ctx)
|
||||
if apiErr == nil {
|
||||
for _, user := range users {
|
||||
if user.Email == DEFAULT_CLOUD_EMAIL {
|
||||
continue
|
||||
}
|
||||
telemetry.SendEvent(TELEMETRY_EVENT_HEART_BEAT, data, user.Email, false, false)
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"totalSpans": totalSpans,
|
||||
"spansInLastHeartBeatInterval": spansInLastHeartBeatInterval,
|
||||
"totalSamples": totalSamples,
|
||||
"getSamplesInfoInLastHeartBeatInterval": getSamplesInfoInLastHeartBeatInterval,
|
||||
"totalLogs": totalLogs,
|
||||
"getLogsInfoInLastHeartBeatInterval": getLogsInfoInLastHeartBeatInterval,
|
||||
"countUsers": telemetry.countUsers,
|
||||
"metricsTTLStatus": metricsTTL.Status,
|
||||
"tracesTTLStatus": traceTTL.Status,
|
||||
"logsTTLStatus": logsTTL.Status,
|
||||
"patUser": telemetry.patTokenUser,
|
||||
}
|
||||
telemetry.patTokenUser = false
|
||||
for key, value := range tsInfo {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
users, apiErr := telemetry.reader.GetUsers(context.Background())
|
||||
if apiErr == nil {
|
||||
for _, user := range users {
|
||||
if user.Email == DEFAULT_CLOUD_EMAIL {
|
||||
continue
|
||||
}
|
||||
telemetry.SendEvent(TELEMETRY_EVENT_HEART_BEAT, data, user.Email, false, false)
|
||||
}
|
||||
}
|
||||
alertsInfo, err := telemetry.alertsInfoCallback(context.Background())
|
||||
alertsInfo, err := telemetry.alertsInfoCallback(ctx)
|
||||
if err == nil {
|
||||
dashboardsInfo, err := telemetry.reader.GetDashboardsInfo(ctx)
|
||||
if err == nil {
|
||||
channels, err := telemetry.reader.GetChannels()
|
||||
if err == nil {
|
||||
dashboardsInfo, err := telemetry.reader.GetDashboardsInfo(context.Background())
|
||||
savedViewsInfo, err := telemetry.reader.GetSavedViewsInfo(ctx)
|
||||
if err == nil {
|
||||
channels, err := telemetry.reader.GetChannels()
|
||||
if err == nil {
|
||||
savedViewsInfo, err := telemetry.reader.GetSavedViewsInfo(context.Background())
|
||||
if err == nil {
|
||||
dashboardsAlertsData := map[string]interface{}{
|
||||
"totalDashboards": dashboardsInfo.TotalDashboards,
|
||||
"totalDashboardsWithPanelAndName": dashboardsInfo.TotalDashboardsWithPanelAndName,
|
||||
"logsBasedPanels": dashboardsInfo.LogsBasedPanels,
|
||||
"metricBasedPanels": dashboardsInfo.MetricBasedPanels,
|
||||
"tracesBasedPanels": dashboardsInfo.TracesBasedPanels,
|
||||
"totalAlerts": alertsInfo.TotalAlerts,
|
||||
"logsBasedAlerts": alertsInfo.LogsBasedAlerts,
|
||||
"metricBasedAlerts": alertsInfo.MetricBasedAlerts,
|
||||
"tracesBasedAlerts": alertsInfo.TracesBasedAlerts,
|
||||
"totalChannels": len(*channels),
|
||||
"totalSavedViews": savedViewsInfo.TotalSavedViews,
|
||||
"logsSavedViews": savedViewsInfo.LogsSavedViews,
|
||||
"tracesSavedViews": savedViewsInfo.TracesSavedViews,
|
||||
}
|
||||
// send event only if there are dashboards or alerts or channels
|
||||
if (dashboardsInfo.TotalDashboards > 0 || alertsInfo.TotalAlerts > 0 || len(*channels) > 0 || savedViewsInfo.TotalSavedViews > 0) && apiErr == nil {
|
||||
for _, user := range users {
|
||||
if user.Email == DEFAULT_CLOUD_EMAIL {
|
||||
continue
|
||||
}
|
||||
telemetry.SendEvent(TELEMETRY_EVENT_DASHBOARDS_ALERTS, dashboardsAlertsData, user.Email, false, false)
|
||||
}
|
||||
dashboardsAlertsData := map[string]interface{}{
|
||||
"totalDashboards": dashboardsInfo.TotalDashboards,
|
||||
"totalDashboardsWithPanelAndName": dashboardsInfo.TotalDashboardsWithPanelAndName,
|
||||
"logsBasedPanels": dashboardsInfo.LogsBasedPanels,
|
||||
"metricBasedPanels": dashboardsInfo.MetricBasedPanels,
|
||||
"tracesBasedPanels": dashboardsInfo.TracesBasedPanels,
|
||||
"totalAlerts": alertsInfo.TotalAlerts,
|
||||
"logsBasedAlerts": alertsInfo.LogsBasedAlerts,
|
||||
"metricBasedAlerts": alertsInfo.MetricBasedAlerts,
|
||||
"tracesBasedAlerts": alertsInfo.TracesBasedAlerts,
|
||||
"totalChannels": len(*channels),
|
||||
"totalSavedViews": savedViewsInfo.TotalSavedViews,
|
||||
"logsSavedViews": savedViewsInfo.LogsSavedViews,
|
||||
"tracesSavedViews": savedViewsInfo.TracesSavedViews,
|
||||
}
|
||||
// send event only if there are dashboards or alerts or channels
|
||||
if (dashboardsInfo.TotalDashboards > 0 || alertsInfo.TotalAlerts > 0 || len(*channels) > 0 || savedViewsInfo.TotalSavedViews > 0) && apiErr == nil {
|
||||
for _, user := range users {
|
||||
if user.Email == DEFAULT_CLOUD_EMAIL {
|
||||
continue
|
||||
}
|
||||
telemetry.SendEvent(TELEMETRY_EVENT_DASHBOARDS_ALERTS, dashboardsAlertsData, user.Email, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil || apiErr != nil {
|
||||
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, "", true, false)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err != nil || apiErr != nil {
|
||||
telemetry.SendEvent(TELEMETRY_EVENT_DASHBOARDS_ALERTS, map[string]interface{}{"error": err.Error()}, "", true, false)
|
||||
}
|
||||
|
||||
getDistributedInfoInLastHeartBeatInterval, _ := telemetry.reader.GetDistributedInfoInLastHeartBeatInterval(ctx)
|
||||
telemetry.SendEvent(TELEMETRY_EVENT_DISTRIBUTED, getDistributedInfoInLastHeartBeatInterval, "", true, false)
|
||||
}
|
||||
|
||||
// Define active user function
|
||||
activeUserFunc := func() {
|
||||
if telemetry.activeUser["logs"] != 0 {
|
||||
getLogsInfoInLastHeartBeatInterval, err := telemetry.reader.GetLogsInfoInLastHeartBeatInterval(ctx, ACTIVE_USER_DURATION)
|
||||
if err != nil && getLogsInfoInLastHeartBeatInterval == 0 {
|
||||
telemetry.activeUser["logs"] = 0
|
||||
}
|
||||
}
|
||||
if telemetry.activeUser["metrics"] != 0 {
|
||||
getSamplesInfoInLastHeartBeatInterval, err := telemetry.reader.GetSamplesInfoInLastHeartBeatInterval(ctx, ACTIVE_USER_DURATION)
|
||||
if err != nil && getSamplesInfoInLastHeartBeatInterval == 0 {
|
||||
telemetry.activeUser["metrics"] = 0
|
||||
}
|
||||
}
|
||||
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"]},
|
||||
"", true, false)
|
||||
telemetry.activeUser = map[string]int8{"traces": 0, "metrics": 0, "logs": 0, "any": 0}
|
||||
}
|
||||
|
||||
// Calculate next run time based on duration and start time
|
||||
calculateNextRun := func(duration time.Duration, startTimeStr string) time.Time {
|
||||
now := time.Now().UTC()
|
||||
startTime, _ := time.Parse("15:04", startTimeStr)
|
||||
todayStartTime := time.Date(now.Year(), now.Month(), now.Day(), startTime.Hour(), startTime.Minute(), 0, 0, time.UTC)
|
||||
|
||||
if now.Before(todayStartTime) {
|
||||
todayStartTime = todayStartTime.Add(-24 * time.Hour)
|
||||
}
|
||||
|
||||
diff := now.Sub(todayStartTime)
|
||||
intervalsPassed := int(diff / duration)
|
||||
nextRun := todayStartTime.Add(time.Duration(intervalsPassed+1) * duration)
|
||||
|
||||
return nextRun
|
||||
}
|
||||
|
||||
// Schedule next runs
|
||||
scheduleNextRuns := func() {
|
||||
nextHeartbeat := calculateNextRun(HEART_BEAT_DURATION, SCHEDULE_START_TIME)
|
||||
nextActiveUser := calculateNextRun(ACTIVE_USER_DURATION, SCHEDULE_START_TIME)
|
||||
|
||||
s.Every(HEART_BEAT_DURATION).StartAt(nextHeartbeat).Do(heartbeatFunc)
|
||||
s.Every(ACTIVE_USER_DURATION).StartAt(nextActiveUser).Do(activeUserFunc)
|
||||
}
|
||||
|
||||
// Schedule immediate execution and subsequent runs
|
||||
scheduleNextRuns()
|
||||
|
||||
// Start the scheduler in a separate goroutine
|
||||
go s.StartBlocking()
|
||||
}
|
||||
|
||||
// Get preferred outbound ip of this machine
|
||||
|
||||
@@ -25,12 +25,12 @@ Commit timestamp : %v
|
||||
Branch : %v
|
||||
Go version : %v
|
||||
|
||||
For SigNoz Official Documentation, visit https://signoz.io/docs
|
||||
For SigNoz Community Slack, visit http://signoz.io/slack
|
||||
For discussions about SigNoz, visit https://community.signoz.io
|
||||
For SigNoz Official Documentation, visit https://signoz.io/docs/
|
||||
For SigNoz Community Slack, visit http://signoz.io/slack/
|
||||
For archive of discussions about SigNoz, visit https://knowledgebase.signoz.io/
|
||||
|
||||
%s.
|
||||
Copyright 2022 SigNoz
|
||||
Copyright 2024 SigNoz
|
||||
`,
|
||||
buildVersion, buildHash, buildTime, gitBranch,
|
||||
runtime.Version(), licenseInfo)
|
||||
|
||||
Reference in New Issue
Block a user