From 65280cf4e18f25872e78e451bca190233ce8c7fa Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Fri, 16 Aug 2024 13:11:39 +0530 Subject: [PATCH 01/60] feat: support for attribute key suggestions and example queries in logs explorer query builder (#5608) * feat: qb-suggestions base setup * chore: make the dropdown a little similar to the designs * chore: move out example queries from og and add to renderer * chore: added the handlers for example queries * chore: hide the example queries as soon as the user starts typing * feat: handle changes for cancel query * chore: remove stupid concept of option group * chore: show only first 3 items and add option to show all filters * chore: minor css changes and remove transitions * feat: integrate suggestions api and control re-renders * feat: added keyboard shortcuts for the dropdown * fix: design cleanups and touchups * fix: build issues and tests * chore: extra safety check for base64 and fix tests * fix: qs doesn't handle padding in base64 strings, added client logic * chore: some code comments * chore: some code comments * chore: increase the height of the bar when key is set * chore: address minor designs * chore: update the keyboard shortcut to cmd+/ * feat: correct the option render for logs for tooltip * chore: search bar to not loose focus on btn click * fix: update the spacing and icon for search bar * chore: address review comments --- .../queryBuilder/getAttributeSuggestions.ts | 63 +++ frontend/src/constants/queryBuilder.ts | 3 +- .../shortcuts/logsExplorerShortcuts.ts | 3 + .../src/container/LogsExplorerViews/index.tsx | 40 +- .../tests/LogsExplorerViews.test.tsx | 3 + .../src/container/OptionsMenu/constants.ts | 5 + .../ToolbarActions/RightToolbarActions.tsx | 56 ++- .../ToolbarActions/ToolbarActions.styles.scss | 65 ++- .../tests/ToolbarActions.test.tsx | 5 +- .../ExampleQueriesRendererForLogs.tsx | 30 ++ .../OptionRendererForLogs.tsx | 77 ++++ .../QueryBuilderSearch.styles.scss | 388 ++++++++++++++++++ .../filters/QueryBuilderSearch/index.tsx | 177 +++++++- .../filters/QueryBuilderSearch/utils.ts | 16 + frontend/src/container/QueryBuilder/type.ts | 1 + .../src/hooks/queryBuilder/useAutoComplete.ts | 13 +- .../queryBuilder/useFetchKeysAndValues.ts | 86 +++- .../useGetAttributeSuggestions.ts | 38 ++ .../queryBuilder/useGetExplorerQueryRange.ts | 8 +- frontend/src/hooks/queryBuilder/useOptions.ts | 1 + frontend/src/pages/LogsExplorer/index.tsx | 20 +- .../queryBuilder/getAttributeSuggestions.ts | 15 + .../queryBuilder/queryAutocompleteResponse.ts | 1 + 23 files changed, 1070 insertions(+), 44 deletions(-) create mode 100644 frontend/src/api/queryBuilder/getAttributeSuggestions.ts create mode 100644 frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/ExampleQueriesRendererForLogs.tsx create mode 100644 frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/OptionRendererForLogs.tsx create mode 100644 frontend/src/hooks/queryBuilder/useGetAttributeSuggestions.ts create mode 100644 frontend/src/types/api/queryBuilder/getAttributeSuggestions.ts diff --git a/frontend/src/api/queryBuilder/getAttributeSuggestions.ts b/frontend/src/api/queryBuilder/getAttributeSuggestions.ts new file mode 100644 index 0000000000..45b380f9e8 --- /dev/null +++ b/frontend/src/api/queryBuilder/getAttributeSuggestions.ts @@ -0,0 +1,63 @@ +import { ApiV3Instance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError, AxiosResponse } from 'axios'; +import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder'; +import { encode } from 'js-base64'; +import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; +import createQueryParams from 'lib/createQueryParams'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + IGetAttributeSuggestionsPayload, + IGetAttributeSuggestionsSuccessResponse, +} from 'types/api/queryBuilder/getAttributeSuggestions'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +export const getAttributeSuggestions = async ({ + searchText, + dataSource, + filters, +}: IGetAttributeSuggestionsPayload): Promise< + SuccessResponse | ErrorResponse +> => { + try { + let base64EncodedFiltersString; + try { + // the replace function is to remove the padding at the end of base64 encoded string which is auto added to make it a multiple of 4 + // why ? because the current working of qs doesn't work well with padding + base64EncodedFiltersString = encode(JSON.stringify(filters)).replace( + /=+$/, + '', + ); + } catch { + // default base64 encoded string for empty filters object + base64EncodedFiltersString = 'eyJpdGVtcyI6W10sIm9wIjoiQU5EIn0'; + } + const response: AxiosResponse<{ + data: IGetAttributeSuggestionsSuccessResponse; + }> = await ApiV3Instance.get( + `/filter_suggestions?${createQueryParams({ + searchText, + dataSource, + existingFilter: base64EncodedFiltersString, + })}`, + ); + + const payload: BaseAutocompleteData[] = + response.data.data.attributes?.map(({ id: _, ...item }) => ({ + ...item, + id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder), + })) || []; + + return { + statusCode: 200, + error: null, + message: response.statusText, + payload: { + attributes: payload, + example_queries: response.data.data.example_queries, + }, + }; + } catch (e) { + return ErrorResponseHandler(e as AxiosError); + } +}; diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index 7b7b464b3e..5fe7112796 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -52,7 +52,7 @@ export const selectValueDivider = '__'; export const baseAutoCompleteIdKeysOrder: (keyof Omit< BaseAutocompleteData, - 'id' | 'isJSON' + 'id' | 'isJSON' | 'isIndexed' >)[] = ['key', 'dataType', 'type', 'isColumn']; export const autocompleteType: Record = { @@ -71,6 +71,7 @@ export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str)); export enum QueryBuilderKeys { GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE', GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS', + GET_ATTRIBUTE_SUGGESTIONS = 'GET_ATTRIBUTE_SUGGESTIONS', } export const mapOfOperators = { diff --git a/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts b/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts index 33c2b4061f..68331a4c2d 100644 --- a/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts +++ b/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts @@ -4,6 +4,7 @@ const userOS = getUserOperatingSystem(); export const LogsExplorerShortcuts = { StageAndRunQuery: 'enter+meta', FocusTheSearchBar: 's', + ShowAllFilters: '/+meta', }; export const LogsExplorerShortcutsName = { @@ -11,9 +12,11 @@ export const LogsExplorerShortcutsName = { userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl' }+enter`, FocusTheSearchBar: 's', + ShowAllFilters: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+/`, }; export const LogsExplorerShortcutsDescription = { StageAndRunQuery: 'Stage and Run the current query', FocusTheSearchBar: 'Shift the focus to the last query filter bar', + ShowAllFilters: 'Toggle all filters in the filters dropdown', }; diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 6318e1da71..c7214ab260 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -48,7 +48,15 @@ import { } from 'lodash-es'; import { Sliders } from 'lucide-react'; import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + memo, + MutableRefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { AppState } from 'store/reducers'; @@ -72,9 +80,15 @@ import { v4 } from 'uuid'; function LogsExplorerViews({ selectedView, showFrequencyChart, + setIsLoadingQueries, + listQueryKeyRef, + chartQueryKeyRef, }: { selectedView: SELECTED_VIEWS; showFrequencyChart: boolean; + setIsLoadingQueries: React.Dispatch>; + listQueryKeyRef: MutableRefObject; + chartQueryKeyRef: MutableRefObject; }): JSX.Element { const { notifications } = useNotifications(); const history = useHistory(); @@ -214,6 +228,9 @@ function LogsExplorerViews({ { enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST, }, + {}, + undefined, + chartQueryKeyRef, ); const { data, isLoading, isFetching, isError } = useGetExplorerQueryRange( @@ -232,6 +249,8 @@ function LogsExplorerViews({ end: timeRange.end, }), }, + undefined, + listQueryKeyRef, ); const getRequestData = useCallback( @@ -569,6 +588,25 @@ function LogsExplorerViews({ }, }); + useEffect(() => { + if ( + isLoading || + isFetching || + isLoadingListChartData || + isFetchingListChartData + ) { + setIsLoadingQueries(true); + } else { + setIsLoadingQueries(false); + } + }, [ + isLoading, + isFetching, + isFetchingListChartData, + isLoadingListChartData, + setIsLoadingQueries, + ]); + const flattenLogData = useMemo( () => logs.map((log) => { diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index a1ae308efd..7cd58316e8 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -79,6 +79,9 @@ const renderer = (): RenderResult => {}} + listQueryKeyRef={{ current: {} }} + chartQueryKeyRef={{ current: {} }} /> diff --git a/frontend/src/container/OptionsMenu/constants.ts b/frontend/src/container/OptionsMenu/constants.ts index 2db02f85b8..7a454de8ca 100644 --- a/frontend/src/container/OptionsMenu/constants.ts +++ b/frontend/src/container/OptionsMenu/constants.ts @@ -18,6 +18,7 @@ export const defaultTraceSelectedColumns = [ isColumn: true, isJSON: false, id: 'serviceName--string--tag--true', + isIndexed: false, }, { key: 'name', @@ -26,6 +27,7 @@ export const defaultTraceSelectedColumns = [ isColumn: true, isJSON: false, id: 'name--string--tag--true', + isIndexed: false, }, { key: 'durationNano', @@ -34,6 +36,7 @@ export const defaultTraceSelectedColumns = [ isColumn: true, isJSON: false, id: 'durationNano--float64--tag--true', + isIndexed: false, }, { key: 'httpMethod', @@ -42,6 +45,7 @@ export const defaultTraceSelectedColumns = [ isColumn: true, isJSON: false, id: 'httpMethod--string--tag--true', + isIndexed: false, }, { key: 'responseStatusCode', @@ -50,5 +54,6 @@ export const defaultTraceSelectedColumns = [ isColumn: true, isJSON: false, id: 'responseStatusCode--string--tag--true', + isIndexed: false, }, ]; diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx b/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx index aea205d569..f5106dffbe 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx @@ -3,18 +3,27 @@ import './ToolbarActions.styles.scss'; import { Button } from 'antd'; import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; -import { Play } from 'lucide-react'; -import { useEffect } from 'react'; +import { Play, X } from 'lucide-react'; +import { MutableRefObject, useEffect } from 'react'; +import { useQueryClient } from 'react-query'; interface RightToolbarActionsProps { onStageRunQuery: () => void; + isLoadingQueries?: boolean; + listQueryKeyRef?: MutableRefObject; + chartQueryKeyRef?: MutableRefObject; } export default function RightToolbarActions({ onStageRunQuery, + isLoadingQueries, + listQueryKeyRef, + chartQueryKeyRef, }: RightToolbarActionsProps): JSX.Element { const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); + const queryClient = useQueryClient(); + useEffect(() => { registerShortcut(LogsExplorerShortcuts.StageAndRunQuery, onStageRunQuery); @@ -25,14 +34,41 @@ export default function RightToolbarActions({ }, [onStageRunQuery]); return (
- + {isLoadingQueries ? ( +
+ +
+ ) : ( + + )}
); } + +RightToolbarActions.defaultProps = { + isLoadingQueries: false, + listQueryKeyRef: null, + chartQueryKeyRef: null, +}; diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss b/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss index a848cf8680..2eda1b7c86 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss @@ -5,8 +5,8 @@ .left-toolbar-query-actions { display: flex; border-radius: 2px; - border: 1px solid var(--bg-slate-400, #1d212d); - background: var(--bg-ink-300, #16181d); + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); flex-direction: row; .prom-ql-icon { @@ -24,7 +24,7 @@ border-radius: 0; &.active-tab { - background-color: #1d212d; + background-color: var(--bg-slate-400); } &:disabled { @@ -33,7 +33,7 @@ } } .action-btn + .action-btn { - border-left: 1px solid var(--bg-slate-400, #1d212d); + border-left: 1px solid var(--bg-slate-400); } } @@ -51,6 +51,50 @@ background-color: var(--bg-robin-600); } +.right-actions { + display: flex; + align-items: center; +} + +.loading-container { + display: flex; + gap: 8px; + align-items: center; + + .loading-btn { + display: flex; + width: 32px; + height: 33px; + padding: 4px 10px; + justify-content: center; + align-items: center; + gap: 6px; + flex-shrink: 0; + border-radius: 2px; + background: var(--bg-slate-300); + box-shadow: none; + border: none; + } + + .cancel-run { + display: flex; + height: 33px; + padding: 4px 10px; + justify-content: center; + align-items: center; + gap: 6px; + flex: 1 0 0; + border-radius: 2px; + background: var(--bg-cherry-500); + border-color: none; + } + .cancel-run:hover { + background-color: #ff7875 !important; + color: var(--bg-vanilla-100) !important; + border: none; + } +} + .lightMode { .left-toolbar { .left-toolbar-query-actions { @@ -68,4 +112,17 @@ } } } + .loading-container { + .loading-btn { + background: var(--bg-vanilla-300); + } + + .cancel-run { + color: var(--bg-vanilla-100); + } + + .cancel-run:hover { + background-color: #ff7875; + } + } } diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx b/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx index 751bdbf99d..79ee020802 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; +import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; import LeftToolbarActions from '../LeftToolbarActions'; import RightToolbarActions from '../RightToolbarActions'; @@ -94,7 +95,9 @@ describe('ToolbarActions', () => { it('RightToolbarActions - render correctly with props', async () => { const onStageRunQuery = jest.fn(); const { queryByText } = render( - , + + , + , ); const stageNRunBtn = queryByText('Stage & Run Query'); diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/ExampleQueriesRendererForLogs.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/ExampleQueriesRendererForLogs.tsx new file mode 100644 index 0000000000..df8921b4d1 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/ExampleQueriesRendererForLogs.tsx @@ -0,0 +1,30 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import './QueryBuilderSearch.styles.scss'; + +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; + +function ExampleQueriesRendererForLogs({ + label, + value, + handleAddTag, +}: ExampleQueriesRendererForLogsProps): JSX.Element { + return ( +
{ + handleAddTag(value); + }} + > + {label} +
+ ); +} + +interface ExampleQueriesRendererForLogsProps { + label: string; + value: TagFilter; + handleAddTag: (value: TagFilter) => void; +} + +export default ExampleQueriesRendererForLogs; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/OptionRendererForLogs.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/OptionRendererForLogs.tsx new file mode 100644 index 0000000000..a1b5149a05 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/OptionRendererForLogs.tsx @@ -0,0 +1,77 @@ +import './QueryBuilderSearch.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Tooltip, Typography } from 'antd'; +import cx from 'classnames'; +import { Zap } from 'lucide-react'; +import { useState } from 'react'; + +import { getOptionType } from './utils'; + +function OptionRendererForLogs({ + label, + value, + dataType, + isIndexed, + setDynamicPlaceholder, +}: OptionRendererProps): JSX.Element { + const [truncated, setTruncated] = useState(false); + const optionType = getOptionType(label); + + return ( + setDynamicPlaceholder(value)} + onFocus={(): void => setDynamicPlaceholder(value)} + > + {optionType ? ( + +
+
+ {isIndexed ? ( + + ) : ( +
+ )} + setTruncated(ellipsis) }} + > + {value} + +
+
+
{dataType}
+
+
+ {optionType} +
+
+
+
+ ) : ( + +
+
+ setTruncated(ellipsis) }} + > + {label} + +
+ + )} + + ); +} + +interface OptionRendererProps { + label: string; + value: string; + dataType: string; + isIndexed: boolean; + setDynamicPlaceholder: React.Dispatch>; +} + +export default OptionRendererForLogs; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss index a6f5fcaf37..db03f5a862 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss @@ -11,6 +11,290 @@ } } +.logs-popup { + &.hide-scroll { + .rc-virtual-list-holder { + height: 100px; + } + } +} + +.logs-explorer-popup { + padding: 0px; + .ant-select-item-group { + padding: 12px 14px 8px 14px; + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .show-all-filter-props { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 13px; + width: 100%; + cursor: pointer; + + .content { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + + .left-section { + display: flex; + align-items: center; + gap: 4px; + + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .text:hover { + color: var(--bg-vanilla-100); + } + } + .right-section { + display: flex; + align-items: center; + gap: 4px; + .keyboard-shortcut-slash { + width: 16px; + height: 16px; + flex-shrink: 0; + border-radius: 2.286px; + border-top: 1.143px solid var(--bg-ink-200); + border-right: 1.143px solid var(--bg-ink-200); + border-bottom: 2.286px solid var(--bg-ink-200); + border-left: 1.143px solid var(--bg-ink-200); + background: var(--bg-ink-400); + } + } + } + } + + .show-all-filter-props:hover { + background: rgba(255, 255, 255, 0.04) !important; + } + + .example-queries { + cursor: default; + .heading { + padding: 12px 14px 8px 14px; + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .query-container { + display: flex; + flex-direction: column; + gap: 12px; + padding: 0px 12px 12px 12px; + cursor: pointer; + + .example-query { + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 2px; + background: var(--bg-ink-200); + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.07px; + width: fit-content; + } + + .example-query:hover { + color: var(--bg-vanilla-100); + } + } + } + + .ant-select-item-option-grouped { + padding-inline-start: 0px; + padding: 7px 13px; + } + + .keyboard-shortcuts { + display: flex; + align-items: center; + border-radius: 0px 0px 4px 4px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + padding: 11px 16px; + cursor: default; + + .icons { + width: 16px; + height: 16px; + flex-shrink: 0; + border-radius: 2.286px; + border-top: 1.143px solid var(--Ink-200, #23262e); + border-right: 1.143px solid var(--Ink-200, #23262e); + border-bottom: 2.286px solid var(--Ink-200, #23262e); + border-left: 1.143px solid var(--Ink-200, #23262e); + background: var(--Ink-400, #121317); + } + + .keyboard-text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .navigate { + display: flex; + align-items: center; + padding-right: 12px; + gap: 4px; + border-right: 1px solid #1d212d; + } + + .update-query { + display: flex; + align-items: center; + margin-left: 12px; + gap: 4px; + } + } + + .without-option-type { + display: flex; + gap: 8px; + align-items: center; + .dot { + height: 5px; + width: 5px; + border-radius: 50%; + background-color: var(--bg-slate-300); + } + } + + .logs-options-select { + display: flex; + align-items: center; + justify-content: space-between; + + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .tags { + display: flex; + height: 20px; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 20px; + } + + .dot { + height: 5px; + width: 5px; + border-radius: 50%; + flex-shrink: 0; + } + + .left-section { + display: flex; + align-items: center; + gap: 8px; + width: 90%; + + .dot { + background-color: var(--bg-slate-300); + } + + .value { + width: 100%; + } + } + + .right-section { + display: flex; + align-items: center; + gap: 4px; + + .data-type-tag { + background: rgba(255, 255, 255, 0.08); + } + + .option-type-tag { + display: flex; + gap: 4px; + align-items: center; + padding: 0px 6px; + text-transform: capitalize; + } + + .tag { + border-radius: 50px; + background: rgba(189, 153, 121, 0.1); + color: var(--bg-sienna-400); + + .dot { + background-color: var(--bg-sienna-400); + } + } + + .resource { + border-radius: 50px; + background: rgba(245, 108, 135, 0.1); + color: var(--bg-sakura-400); + + .dot { + background-color: var(--bg-sakura-400); + } + } + } + } + + .ant-select-item-option-active { + .logs-options-select { + .left-section { + .value { + color: var(--bg-vanilla-100); + } + } + } + } +} + .lightMode { .query-builder-search { .ant-select-dropdown { @@ -21,4 +305,108 @@ background-color: var(--bg-vanilla-200) !important; } } + .logs-explorer-popup { + .ant-select-item-group { + color: var(--bg-slate-50); + } + + .show-all-filter-props { + .content { + .left-section { + .text { + color: var(--bg-ink-400); + } + + .text:hover { + color: var(--bg-slate-100); + } + } + .right-section { + .keyboard-shortcut-slash { + border-top: 1.143px solid var(--bg-ink-200); + border-right: 1.143px solid var(--bg-ink-200); + border-bottom: 2.286px solid var(--bg-ink-200); + border-left: 1.143px solid var(--bg-ink-200); + background: var(--bg-vanilla-200); + } + } + } + } + + .show-all-filter-props:hover { + background: var(--bg-vanilla-200) !important; + } + + .example-queries { + .heading { + color: var(--bg-slate-50); + } + + .query-container { + .example-query-container { + .example-query { + background: var(--bg-vanilla-200); + color: var(--bg-ink-400); + } + + .example-query:hover { + color: var(--bg-ink-400); + } + } + } + } + + .keyboard-shortcuts { + border: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-200); + + .icons { + border-top: 1.143px solid var(--Ink-200, #23262e); + border-right: 1.143px solid var(--Ink-200, #23262e); + border-bottom: 2.286px solid var(--Ink-200, #23262e); + border-left: 1.143px solid var(--Ink-200, #23262e); + background: var(--bg-vanilla-200); + } + + .keyboard-text { + color: var(--bg-ink-400); + } + + .navigate { + border-right: 1px solid #1d212d; + } + } + + .logs-options-select { + .text { + color: var(--bg-ink-400); + } + + .right-section { + .data-type-tag { + background: var(--bg-vanilla-200); + } + + .tag { + background: rgba(189, 153, 121, 0.1); + color: var(--bg-sienna-400); + } + + .resource { + background: rgba(245, 108, 135, 0.1); + color: var(--bg-sakura-400); + } + } + } + + .ant-select-item-option-active { + .logs-options-select { + .left-section { + .value { + color: var(--bg-ink-100); + } + } + } + } + } } diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx index eaaccb607d..c1f4b85a11 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx @@ -1,7 +1,10 @@ +/* eslint-disable react/no-unstable-nested-components */ import './QueryBuilderSearch.styles.scss'; -import { Select, Spin, Tag, Tooltip } from 'antd'; +import { Button, Select, Spin, Tag, Tooltip, Typography } from 'antd'; +import cx from 'classnames'; import { OPERATORS } from 'constants/queryBuilder'; +import ROUTES from 'constants/routes'; import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts'; import { getDataTypes } from 'container/LogDetailedView/utils'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; @@ -11,7 +14,17 @@ import { } from 'hooks/queryBuilder/useAutoComplete'; import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { isEqual } from 'lodash-es'; +import { isEqual, isUndefined } from 'lodash-es'; +import { + ArrowDown, + ArrowUp, + ChevronDown, + ChevronUp, + Command, + CornerDownLeft, + Filter, + Slash, +} from 'lucide-react'; import type { BaseSelectRef } from 'rc-select'; import { KeyboardEvent, @@ -23,6 +36,7 @@ import { useRef, useState, } from 'react'; +import { useLocation } from 'react-router-dom'; import { BaseAutocompleteData, DataTypes, @@ -32,14 +46,18 @@ import { TagFilter, } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; +import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS'; import { popupContainer } from 'utils/selectPopupContainer'; import { v4 as uuid } from 'uuid'; import { selectStyle } from './config'; import { PLACEHOLDER } from './constant'; +import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs'; import OptionRenderer from './OptionRenderer'; +import OptionRendererForLogs from './OptionRendererForLogs'; import { StyledCheckOutlined, TypographyText } from './style'; import { + convertExampleQueriesToOptions, getOperatorValue, getRemovePrefixFromKey, getTagToken, @@ -55,6 +73,10 @@ function QueryBuilderSearch({ placeholder, suffixIcon, }: QueryBuilderSearchProps): JSX.Element { + const { pathname } = useLocation(); + const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ + pathname, + ]); const { updateTag, handleClearTag, @@ -69,14 +91,20 @@ function QueryBuilderSearch({ isFetching, setSearchKey, searchKey, - } = useAutoComplete(query, whereClauseConfig); - + key, + exampleQueries, + } = useAutoComplete(query, whereClauseConfig, isLogsExplorerPage); const [isOpen, setIsOpen] = useState(false); + const [showAllFilters, setShowAllFilters] = useState(false); + const [dynamicPlacholder, setDynamicPlaceholder] = useState( + placeholder || '', + ); const selectRef = useRef(null); const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues( searchValue, query, searchKey, + isLogsExplorerPage, ); const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); @@ -140,6 +168,12 @@ function QueryBuilderSearch({ handleRunQuery(); setIsOpen(false); } + + if ((event.ctrlKey || event.metaKey) && event.key === '/') { + event.preventDefault(); + event.stopPropagation(); + setShowAllFilters((prev) => !prev); + } }; const handleDeselect = useCallback( @@ -229,6 +263,28 @@ function QueryBuilderSearch({ deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar); }, [deregisterShortcut, isLastQuery, registerShortcut]); + useEffect(() => { + if (!isOpen) { + setDynamicPlaceholder(placeholder || ''); + } + }, [isOpen, placeholder]); + + const userOs = getUserOperatingSystem(); + + // conditional changes here to use a seperate component to render the example queries based on the option group label + const customRendererForLogsExplorer = options.map((option) => ( + + + {option.selected && } + + )); + return (
3 && !key ? 'hide-scroll' : '', + )} rootClassName="query-builder-search" disabled={isMetricsDataSource && !query.aggregateAttribute.key} style={selectStyle} @@ -259,20 +321,99 @@ function QueryBuilderSearch({ onDeselect={handleDeselect} onInputKeyDown={onInputKeyDownHandler} notFoundContent={isFetching ? : null} - suffixIcon={suffixIcon} + suffixIcon={ + // eslint-disable-next-line no-nested-ternary + !isUndefined(suffixIcon) ? ( + suffixIcon + ) : isOpen ? ( + + ) : ( + + ) + } showAction={['focus']} onBlur={handleOnBlur} + popupClassName={isLogsExplorerPage ? 'logs-explorer-popup' : ''} + dropdownRender={(menu): ReactElement => ( +
+ {!searchKey && isLogsExplorerPage && ( +
Suggested Filters
+ )} + {menu} + {isLogsExplorerPage && ( +
+ {!searchKey && tags.length === 0 && ( +
+
Example Queries
+
+ {convertExampleQueriesToOptions(exampleQueries).map((query) => ( + + ))} +
+
+ )} + {!key && !isFetching && !showAllFilters && options.length > 3 && ( + + )} +
+
+ + + to navigate +
+
+ + to update query +
+
+
+ )} +
+ )} > - {options.map((option) => ( - - - {option.selected && } - - ))} + {isLogsExplorerPage + ? customRendererForLogsExplorer + : options.map((option) => ( + + + {option.selected && } + + ))}
); diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts index ec7eba3973..04d8a0f77d 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts @@ -1,6 +1,8 @@ import { OPERATORS } from 'constants/queryBuilder'; import { MetricsType } from 'container/MetricsApplication/constant'; +import { queryFilterTags } from 'hooks/queryBuilder/useTag'; import { parse } from 'papaparse'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { orderByValueDelimiter } from '../OrderByFilter/utils'; @@ -162,3 +164,17 @@ export function getOptionType(label: string): MetricsType | undefined { return optionType; } + +/** + * + * @param exampleQueries the example queries based on recommendation engine + * @returns the data formatted to the Option[] + */ +export function convertExampleQueriesToOptions( + exampleQueries: TagFilter[], +): { label: string; value: TagFilter }[] { + return exampleQueries.map((query) => ({ + value: query, + label: queryFilterTags(query).join(' , '), + })); +} diff --git a/frontend/src/container/QueryBuilder/type.ts b/frontend/src/container/QueryBuilder/type.ts index 892330ebdd..183dd157f8 100644 --- a/frontend/src/container/QueryBuilder/type.ts +++ b/frontend/src/container/QueryBuilder/type.ts @@ -15,4 +15,5 @@ export type Option = { label: string; selected?: boolean; dataType?: string; + isIndexed?: boolean; }; diff --git a/frontend/src/hooks/queryBuilder/useAutoComplete.ts b/frontend/src/hooks/queryBuilder/useAutoComplete.ts index 8872b2dc02..ed1836a0fa 100644 --- a/frontend/src/hooks/queryBuilder/useAutoComplete.ts +++ b/frontend/src/hooks/queryBuilder/useAutoComplete.ts @@ -8,7 +8,10 @@ import { import { Option } from 'container/QueryBuilder/type'; import { parse } from 'papaparse'; import { KeyboardEvent, useCallback, useState } from 'react'; -import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { + IBuilderQuery, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; import { useFetchKeysAndValues } from './useFetchKeysAndValues'; import { useOptions, WHERE_CLAUSE_CUSTOM_SUFFIX } from './useOptions'; @@ -24,14 +27,16 @@ export type WhereClauseConfig = { export const useAutoComplete = ( query: IBuilderQuery, whereClauseConfig?: WhereClauseConfig, + shouldUseSuggestions?: boolean, ): IAutoComplete => { const [searchValue, setSearchValue] = useState(''); const [searchKey, setSearchKey] = useState(''); - const { keys, results, isFetching } = useFetchKeysAndValues( + const { keys, results, isFetching, exampleQueries } = useFetchKeysAndValues( searchValue, query, searchKey, + shouldUseSuggestions, ); const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys); @@ -144,6 +149,8 @@ export const useAutoComplete = ( isFetching, setSearchKey, searchKey, + key, + exampleQueries, }; }; @@ -161,4 +168,6 @@ interface IAutoComplete { isFetching: boolean; setSearchKey: (value: string) => void; searchKey: string; + key: string; + exampleQueries: TagFilter[]; } diff --git a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts index db54685b8e..5fe5e100fa 100644 --- a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts +++ b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts @@ -6,17 +6,21 @@ import { isInNInOperator, } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; import useDebounceValue from 'hooks/useDebounce'; -import { isEqual, uniqWith } from 'lodash-es'; +import { cloneDeep, isEqual, uniqWith, unset } from 'lodash-es'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDebounce } from 'react-use'; import { BaseAutocompleteData, DataTypes, } from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { + IBuilderQuery, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; import { useGetAggregateKeys } from './useGetAggregateKeys'; +import { useGetAttributeSuggestions } from './useGetAttributeSuggestions'; type IuseFetchKeysAndValues = { keys: BaseAutocompleteData[]; @@ -24,6 +28,7 @@ type IuseFetchKeysAndValues = { isFetching: boolean; sourceKeys: BaseAutocompleteData[]; handleRemoveSourceKey: (newSourceKey: string) => void; + exampleQueries: TagFilter[]; }; /** @@ -37,8 +42,10 @@ export const useFetchKeysAndValues = ( searchValue: string, query: IBuilderQuery, searchKey: string, + shouldUseSuggestions?: boolean, ): IuseFetchKeysAndValues => { const [keys, setKeys] = useState([]); + const [exampleQueries, setExampleQueries] = useState([]); const [sourceKeys, setSourceKeys] = useState([]); const [results, setResults] = useState([]); const [isAggregateFetching, setAggregateFetching] = useState(false); @@ -60,6 +67,28 @@ export const useFetchKeysAndValues = ( const searchParams = useDebounceValue(memoizedSearchParams, DEBOUNCE_DELAY); + const queryFiltersWithoutId = useMemo( + () => ({ + ...query.filters, + items: query.filters.items.map((item) => { + const filterWithoutId = cloneDeep(item); + unset(filterWithoutId, 'id'); + return filterWithoutId; + }), + }), + [query.filters], + ); + + const memoizedSuggestionsParams = useMemo( + () => [searchKey, query.dataSource, queryFiltersWithoutId], + [query.dataSource, queryFiltersWithoutId, searchKey], + ); + + const suggestionsParams = useDebounceValue( + memoizedSuggestionsParams, + DEBOUNCE_DELAY, + ); + const isQueryEnabled = useMemo( () => query.dataSource === DataSource.METRICS @@ -82,7 +111,26 @@ export const useFetchKeysAndValues = ( aggregateAttribute: query.aggregateAttribute.key, tagType: query.aggregateAttribute.type ?? null, }, - { queryKey: [searchParams], enabled: isQueryEnabled }, + { + queryKey: [searchParams], + enabled: isQueryEnabled && !shouldUseSuggestions, + }, + ); + + const { + data: suggestionsData, + isFetching: isFetchingSuggestions, + status: fetchingSuggestionsStatus, + } = useGetAttributeSuggestions( + { + searchText: searchKey, + dataSource: query.dataSource, + filters: query.filters, + }, + { + queryKey: [suggestionsParams], + enabled: isQueryEnabled && shouldUseSuggestions, + }, ); /** @@ -162,11 +210,41 @@ export const useFetchKeysAndValues = ( } }, [data?.payload?.attributeKeys, status]); + useEffect(() => { + if ( + fetchingSuggestionsStatus === 'success' && + suggestionsData?.payload?.attributes + ) { + setKeys(suggestionsData.payload.attributes); + setSourceKeys((prevState) => + uniqWith( + [...(suggestionsData.payload.attributes ?? []), ...prevState], + isEqual, + ), + ); + } else { + setKeys([]); + } + if ( + fetchingSuggestionsStatus === 'success' && + suggestionsData?.payload?.example_queries + ) { + setExampleQueries(suggestionsData.payload.example_queries); + } else { + setExampleQueries([]); + } + }, [ + suggestionsData?.payload?.attributes, + fetchingSuggestionsStatus, + suggestionsData?.payload?.example_queries, + ]); + return { keys, results, - isFetching: isFetching || isAggregateFetching, + isFetching: isFetching || isAggregateFetching || isFetchingSuggestions, sourceKeys, handleRemoveSourceKey, + exampleQueries, }; }; diff --git a/frontend/src/hooks/queryBuilder/useGetAttributeSuggestions.ts b/frontend/src/hooks/queryBuilder/useGetAttributeSuggestions.ts new file mode 100644 index 0000000000..d2ee7042cd --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetAttributeSuggestions.ts @@ -0,0 +1,38 @@ +import { getAttributeSuggestions } from 'api/queryBuilder/getAttributeSuggestions'; +import { QueryBuilderKeys } from 'constants/queryBuilder'; +import { useMemo } from 'react'; +import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + IGetAttributeSuggestionsPayload, + IGetAttributeSuggestionsSuccessResponse, +} from 'types/api/queryBuilder/getAttributeSuggestions'; + +type UseGetAttributeSuggestions = ( + requestData: IGetAttributeSuggestionsPayload, + options?: UseQueryOptions< + SuccessResponse | ErrorResponse + >, +) => UseQueryResult< + SuccessResponse | ErrorResponse +>; + +export const useGetAttributeSuggestions: UseGetAttributeSuggestions = ( + requestData, + options, +) => { + const queryKey = useMemo(() => { + if (options?.queryKey && Array.isArray(options.queryKey)) { + return [QueryBuilderKeys.GET_ATTRIBUTE_SUGGESTIONS, ...options.queryKey]; + } + return [QueryBuilderKeys.GET_ATTRIBUTE_SUGGESTIONS, requestData]; + }, [options?.queryKey, requestData]); + + return useQuery< + SuccessResponse | ErrorResponse + >({ + queryKey, + queryFn: () => getAttributeSuggestions(requestData), + ...options, + }); +}; diff --git a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts index cdcfb3e0c7..04b9deac16 100644 --- a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts @@ -1,6 +1,6 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; -import { useMemo } from 'react'; +import { MutableRefObject, useMemo } from 'react'; import { UseQueryOptions, UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; @@ -19,6 +19,7 @@ export const useGetExplorerQueryRange = ( options?: UseQueryOptions, Error>, params?: Record, isDependentOnQB = true, + keyRef?: MutableRefObject, ): UseQueryResult, Error> => { const { isEnabledQuery } = useQueryBuilder(); const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector< @@ -40,6 +41,11 @@ export const useGetExplorerQueryRange = ( return isEnabledQuery; }, [options, isEnabledQuery, isDependentOnQB]); + if (keyRef) { + // eslint-disable-next-line no-param-reassign + keyRef.current = [key, globalSelectedInterval, requestData, minTime, maxTime]; + } + return useGetQueryRange( { graphType: panelType || PANEL_TYPES.LIST, diff --git a/frontend/src/hooks/queryBuilder/useOptions.ts b/frontend/src/hooks/queryBuilder/useOptions.ts index f050d19f82..2f24dd0d21 100644 --- a/frontend/src/hooks/queryBuilder/useOptions.ts +++ b/frontend/src/hooks/queryBuilder/useOptions.ts @@ -45,6 +45,7 @@ export const useOptions = ( label: `${getLabel(item)}`, value: item.key, dataType: item.dataType, + isIndexed: item?.isIndexed, })), [getLabel], ); diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index d4422f58fe..505851da10 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -9,7 +9,7 @@ import RightToolbarActions from 'container/QueryBuilder/components/ToolbarAction import Toolbar from 'container/Toolbar/Toolbar'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { DataSource } from 'types/common/queryBuilder'; import { WrapperStyled } from './styles'; @@ -23,6 +23,12 @@ function LogsExplorer(): JSX.Element { const { handleRunQuery, currentQuery } = useQueryBuilder(); + const listQueryKeyRef = useRef(); + + const chartQueryKeyRef = useRef(); + + const [isLoadingQueries, setIsLoadingQueries] = useState(false); + const handleToggleShowFrequencyChart = (): void => { setShowFrequencyChart(!showFrequencyChart); }; @@ -82,7 +88,14 @@ function LogsExplorer(): JSX.Element { showFrequencyChart={showFrequencyChart} /> } - rightActions={} + rightActions={ + + } showOldCTA /> @@ -97,6 +110,9 @@ function LogsExplorer(): JSX.Element {
diff --git a/frontend/src/types/api/queryBuilder/getAttributeSuggestions.ts b/frontend/src/types/api/queryBuilder/getAttributeSuggestions.ts new file mode 100644 index 0000000000..b30f386382 --- /dev/null +++ b/frontend/src/types/api/queryBuilder/getAttributeSuggestions.ts @@ -0,0 +1,15 @@ +import { DataSource } from 'types/common/queryBuilder'; + +import { BaseAutocompleteData } from './queryAutocompleteResponse'; +import { TagFilter } from './queryBuilderData'; + +export interface IGetAttributeSuggestionsPayload { + dataSource: DataSource; + searchText: string; + filters: TagFilter; +} + +export interface IGetAttributeSuggestionsSuccessResponse { + attributes: BaseAutocompleteData[]; + example_queries: TagFilter[]; +} diff --git a/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts b/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts index 67503761ca..a8d5f0f324 100644 --- a/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts +++ b/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts @@ -21,6 +21,7 @@ export interface BaseAutocompleteData { key: string; type: AutocompleteType | string | null; isJSON?: boolean; + isIndexed?: boolean; } export interface IQueryAutocompleteResponse { From 1b9683d699ea4ed7bdadf880c9912df9dc22422d Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Fri, 16 Aug 2024 15:07:06 +0530 Subject: [PATCH 02/60] feat: client changes for query stats (#5706) * feat: changes for the query stats websockets * chore: remove unwanted files * fix: work on random id rather than hash * fix: improve the icons and design * feat: webpack and docker file changes * fix: test cases * chore: format the units * chore: address review comments * chore: update the id to uuid package * fix: build issues * chore: remove docker file changes * chore: remove docker file changes --- frontend/public/Icons/solid-x-circle.svg | 1 + frontend/src/api/common/getQueryStats.ts | 32 ++++++++++ frontend/src/api/metrics/getQueryRange.ts | 10 +++- frontend/src/constants/env.ts | 1 + .../LogsExplorerViews.styles.scss | 39 ++++++++++++ .../LogsExplorerViews/QueryStatus.styles.scss | 4 ++ .../LogsExplorerViews/QueryStatus.tsx | 42 +++++++++++++ .../src/container/LogsExplorerViews/index.tsx | 60 ++++++++++++++++++- .../tests/LogsExplorerViews.test.tsx | 2 + .../queryBuilder/useGetExplorerQueryRange.ts | 2 + .../hooks/queryBuilder/useGetQueryRange.ts | 4 +- frontend/src/lib/dashboard/getQueryResults.ts | 2 + frontend/src/typings/environment.ts | 1 + frontend/webpack.config.js | 1 + frontend/webpack.config.prod.js | 1 + 15 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 frontend/public/Icons/solid-x-circle.svg create mode 100644 frontend/src/api/common/getQueryStats.ts create mode 100644 frontend/src/container/LogsExplorerViews/QueryStatus.styles.scss create mode 100644 frontend/src/container/LogsExplorerViews/QueryStatus.tsx diff --git a/frontend/public/Icons/solid-x-circle.svg b/frontend/public/Icons/solid-x-circle.svg new file mode 100644 index 0000000000..3f189e3865 --- /dev/null +++ b/frontend/public/Icons/solid-x-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/api/common/getQueryStats.ts b/frontend/src/api/common/getQueryStats.ts new file mode 100644 index 0000000000..98f25c6486 --- /dev/null +++ b/frontend/src/api/common/getQueryStats.ts @@ -0,0 +1,32 @@ +import getLocalStorageApi from 'api/browser/localstorage/get'; +import { ENVIRONMENT } from 'constants/env'; +import { LOCALSTORAGE } from 'constants/localStorage'; + +export interface WsDataEvent { + read_rows: number; + read_bytes: number; + elapsed_ms: number; +} +interface GetQueryStatsProps { + queryId: string; + setData: React.Dispatch>; +} + +export function getQueryStats(props: GetQueryStatsProps): void { + const { queryId, setData } = props; + + const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || ''; + const socket = new WebSocket( + `${ENVIRONMENT.wsURL}api/v3/query_progress?q=${queryId}`, + token, + ); + + socket.addEventListener('message', (event) => { + try { + const parsedData = JSON.parse(event?.data); + setData(parsedData); + } catch { + setData(event?.data); + } + }); +} diff --git a/frontend/src/api/metrics/getQueryRange.ts b/frontend/src/api/metrics/getQueryRange.ts index 40deb021bc..631372478d 100644 --- a/frontend/src/api/metrics/getQueryRange.ts +++ b/frontend/src/api/metrics/getQueryRange.ts @@ -12,10 +12,13 @@ export const getMetricsQueryRange = async ( props: QueryRangePayload, version: string, signal: AbortSignal, + headers?: Record, ): Promise | ErrorResponse> => { try { if (version && version === ENTITY_VERSION_V4) { - const response = await ApiV4Instance.post('/query_range', props, { signal }); + const response = await ApiV4Instance.post('/query_range', props, { + signal, + }); return { statusCode: 200, @@ -26,7 +29,10 @@ export const getMetricsQueryRange = async ( }; } - const response = await ApiV3Instance.post('/query_range', props, { signal }); + const response = await ApiV3Instance.post('/query_range', props, { + signal, + headers, + }); return { statusCode: 200, diff --git a/frontend/src/constants/env.ts b/frontend/src/constants/env.ts index 2c5230dfcc..adeb98b4a0 100644 --- a/frontend/src/constants/env.ts +++ b/frontend/src/constants/env.ts @@ -3,4 +3,5 @@ export const ENVIRONMENT = { process?.env?.FRONTEND_API_ENDPOINT || process?.env?.GITPOD_WORKSPACE_URL?.replace('://', '://8080-') || '', + wsURL: process?.env?.WEBSOCKET_API_ENDPOINT || 'ws://localhost:8080/', }; diff --git a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss index a6142d195c..3821e7025c 100644 --- a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss +++ b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styles.scss @@ -80,6 +80,36 @@ position: relative; } } + .query-stats { + display: flex; + align-items: center; + gap: 12px; + .rows { + color: var(--bg-vanilla-400); + font-family: 'Geist Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: 0.36px; + } + + .divider { + width: 1px; + height: 14px; + background: #242834; + } + + .time { + color: var(--bg-vanilla-400); + font-family: 'Geist Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: 0.36px; + } + } } .logs-actions-container { @@ -149,6 +179,15 @@ background: var(--bg-robin-400); } } + .query-stats { + .rows { + color: var(--bg-ink-400); + } + + .time { + color: var(--bg-ink-400); + } + } } } } diff --git a/frontend/src/container/LogsExplorerViews/QueryStatus.styles.scss b/frontend/src/container/LogsExplorerViews/QueryStatus.styles.scss new file mode 100644 index 0000000000..d6f9579113 --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/QueryStatus.styles.scss @@ -0,0 +1,4 @@ +.query-status { + display: flex; + align-items: center; +} diff --git a/frontend/src/container/LogsExplorerViews/QueryStatus.tsx b/frontend/src/container/LogsExplorerViews/QueryStatus.tsx new file mode 100644 index 0000000000..628b3d5fc1 --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/QueryStatus.tsx @@ -0,0 +1,42 @@ +import './QueryStatus.styles.scss'; + +import { LoadingOutlined } from '@ant-design/icons'; +import { Color } from '@signozhq/design-tokens'; +import { Spin } from 'antd'; +import { CircleCheck } from 'lucide-react'; +import React, { useMemo } from 'react'; + +interface IQueryStatusProps { + loading: boolean; + error: boolean; + success: boolean; +} + +export default function QueryStatus( + props: IQueryStatusProps, +): React.ReactElement { + const { loading, error, success } = props; + + const content = useMemo((): React.ReactElement => { + if (loading) { + return } />; + } + if (error) { + return ( + header + ); + } + if (success) { + return ( + + ); + } + return
; + }, [error, loading, success]); + return
{content}
; +} diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index c7214ab260..35aa3f6e33 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -1,8 +1,10 @@ /* eslint-disable sonarjs/cognitive-complexity */ import './LogsExplorerViews.styles.scss'; -import { Button } from 'antd'; +import { Button, Typography } from 'antd'; +import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats'; import logEvent from 'api/common/logEvent'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu'; import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { LOCALSTORAGE } from 'constants/localStorage'; @@ -77,6 +79,8 @@ import { GlobalReducer } from 'types/reducer/globalTime'; import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink'; import { v4 } from 'uuid'; +import QueryStatus from './QueryStatus'; + function LogsExplorerViews({ selectedView, showFrequencyChart, @@ -130,6 +134,8 @@ function LogsExplorerViews({ const [logs, setLogs] = useState([]); const [requestData, setRequestData] = useState(null); const [showFormatMenuItems, setShowFormatMenuItems] = useState(false); + const [queryId, setQueryId] = useState(v4()); + const [queryStats, setQueryStats] = useState(); const handleAxisError = useAxiosError(); @@ -233,7 +239,13 @@ function LogsExplorerViews({ chartQueryKeyRef, ); - const { data, isLoading, isFetching, isError } = useGetExplorerQueryRange( + const { + data, + isLoading, + isFetching, + isError, + isSuccess, + } = useGetExplorerQueryRange( requestData, panelType, DEFAULT_ENTITY_VERSION, @@ -251,6 +263,9 @@ function LogsExplorerViews({ }, undefined, listQueryKeyRef, + { + ...(!isEmpty(queryId) && { 'X-SIGNOZ-QUERY-ID': queryId }), + }, ); const getRequestData = useCallback( @@ -337,6 +352,23 @@ function LogsExplorerViews({ ], ); + useEffect(() => { + setQueryId(v4()); + }, [isError, isSuccess]); + + useEffect(() => { + if ( + !isEmpty(queryId) && + (isLoading || isFetching) && + selectedPanelType !== PANEL_TYPES.LIST + ) { + setQueryStats(undefined); + setTimeout(() => { + getQueryStats({ queryId, setData: setQueryStats }); + }, 500); + } + }, [queryId, isLoading, isFetching, selectedPanelType]); + const logEventCalledRef = useRef(false); useEffect(() => { if (!logEventCalledRef.current && !isUndefined(data?.payload)) { @@ -703,6 +735,30 @@ function LogsExplorerViews({
)} + {(selectedPanelType === PANEL_TYPES.TIME_SERIES || + selectedPanelType === PANEL_TYPES.TABLE) && ( +
+ + {queryStats?.read_rows && ( + + {getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '} + rows + + )} + {queryStats?.elapsed_ms && ( + <> +
+ + {getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')} + + + )} +
+ )}
diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index 7cd58316e8..67c3fcfe2c 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -46,6 +46,8 @@ jest.mock( }, ); +jest.mock('api/common/getQueryStats', () => jest.fn()); + jest.mock('constants/panelTypes', () => ({ AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'], })); diff --git a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts index 04b9deac16..13093ab254 100644 --- a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts @@ -20,6 +20,7 @@ export const useGetExplorerQueryRange = ( params?: Record, isDependentOnQB = true, keyRef?: MutableRefObject, + headers?: Record, ): UseQueryResult, Error> => { const { isEnabledQuery } = useQueryBuilder(); const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector< @@ -61,5 +62,6 @@ export const useGetExplorerQueryRange = ( queryKey: [key, globalSelectedInterval, requestData, minTime, maxTime], enabled: isEnabled, }, + headers, ); }; diff --git a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts index 334ee7f628..e0c6ea3172 100644 --- a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts @@ -13,12 +13,14 @@ type UseGetQueryRange = ( requestData: GetQueryResultsProps, version: string, options?: UseQueryOptions, Error>, + headers?: Record, ) => UseQueryResult, Error>; export const useGetQueryRange: UseGetQueryRange = ( requestData, version, options, + headers, ) => { const newRequestData: GetQueryResultsProps = useMemo( () => ({ @@ -45,7 +47,7 @@ export const useGetQueryRange: UseGetQueryRange = ( return useQuery, Error>({ queryFn: async ({ signal }) => - GetMetricQueryRange(requestData, version, signal), + GetMetricQueryRange(requestData, version, signal, headers), ...options, queryKey, }); diff --git a/frontend/src/lib/dashboard/getQueryResults.ts b/frontend/src/lib/dashboard/getQueryResults.ts index 50e6648924..03ad5f8caa 100644 --- a/frontend/src/lib/dashboard/getQueryResults.ts +++ b/frontend/src/lib/dashboard/getQueryResults.ts @@ -23,6 +23,7 @@ export async function GetMetricQueryRange( props: GetQueryResultsProps, version: string, signal?: AbortSignal, + headers?: Record, ): Promise> { const { legendMap, queryPayload } = prepareQueryRangePayload(props); @@ -30,6 +31,7 @@ export async function GetMetricQueryRange( queryPayload, version || 'v3', signal, + headers, ); if (response.statusCode >= 400) { diff --git a/frontend/src/typings/environment.ts b/frontend/src/typings/environment.ts index c77832ce43..58d01f2f69 100644 --- a/frontend/src/typings/environment.ts +++ b/frontend/src/typings/environment.ts @@ -3,6 +3,7 @@ declare global { namespace NodeJS { interface ProcessEnv { FRONTEND_API_ENDPOINT: string | undefined; + WEBSOCKET_API_ENDPOINT: string | undefined; } } } diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 2e5c0d0f4e..8fc1188448 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -38,6 +38,7 @@ const plugins = [ 'process.env': JSON.stringify({ NODE_ENV: process.env.NODE_ENV, FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT, + WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT, INTERCOM_APP_ID: process.env.INTERCOM_APP_ID, SEGMENT_ID: process.env.SEGMENT_ID, POSTHOG_KEY: process.env.POSTHOG_KEY, diff --git a/frontend/webpack.config.prod.js b/frontend/webpack.config.prod.js index 87ef8b7143..f9af80bcf5 100644 --- a/frontend/webpack.config.prod.js +++ b/frontend/webpack.config.prod.js @@ -48,6 +48,7 @@ const plugins = [ new webpack.DefinePlugin({ 'process.env': JSON.stringify({ FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT, + WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT, INTERCOM_APP_ID: process.env.INTERCOM_APP_ID, SEGMENT_ID: process.env.SEGMENT_ID, POSTHOG_KEY: process.env.POSTHOG_KEY, From 5d903b548717c470bac7a9c86f16623d847cbd9c Mon Sep 17 00:00:00 2001 From: Shaheer Kochai Date: Fri, 16 Aug 2024 14:12:22 +0430 Subject: [PATCH 03/60] NOOP to Count in alert creation from logs (#5464) * fix: change NOOP to count on creating alert from Logs and traces * fix: change 'count' back to 'noop' in Traces page, in case there is a single query * fix: handle the query modification in useGetCompositeQueryParam instead of Filter * chore: use values StringOperators enum instead of hard coded strings * Revert "fix: handle the query modification in useGetCompositeQueryParam instead of Filter" This reverts commit 5bb837ec2756a8bae11a208dc31ebc504157a57b. * Revert "fix: change 'count' back to 'noop' in Traces page, in case there is a single query" This reverts commit 5e506dbd35e72827eee177472dada2c004496432. --- .../ExplorerOptions/ExplorerOptions.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx index 138694058e..f34a4470a9 100644 --- a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx +++ b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx @@ -33,6 +33,7 @@ import useErrorNotification from 'hooks/useErrorNotification'; import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; import { useNotifications } from 'hooks/useNotifications'; import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery'; +import { cloneDeep } from 'lodash-es'; import { Check, ConciergeBell, @@ -56,7 +57,7 @@ import { useHistory } from 'react-router-dom'; import { AppState } from 'store/reducers'; import { Dashboard } from 'types/api/dashboard/getAll'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; -import { DataSource } from 'types/common/queryBuilder'; +import { DataSource, StringOperators } from 'types/common/queryBuilder'; import AppReducer from 'types/reducer/app'; import { USER_ROLES } from 'types/roles'; @@ -120,6 +121,21 @@ function ExplorerOptions({ const { role } = useSelector((state) => state.app); + const handleConditionalQueryModification = useCallback((): string => { + if ( + query?.builder?.queryData?.[0]?.aggregateOperator !== StringOperators.NOOP + ) { + return JSON.stringify(query); + } + + // Modify aggregateOperator to count, as noop is not supported in alerts + const modifiedQuery = cloneDeep(query); + + modifiedQuery.builder.queryData[0].aggregateOperator = StringOperators.COUNT; + + return JSON.stringify(modifiedQuery); + }, [query]); + const onCreateAlertsHandler = useCallback(() => { if (sourcepage === DataSource.TRACES) { logEvent('Traces Explorer: Create alert', { @@ -130,13 +146,16 @@ function ExplorerOptions({ panelType, }); } + + const stringifiedQuery = handleConditionalQueryModification(); + history.push( `${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent( - JSON.stringify(query), + stringifiedQuery, )}`, ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [history, query]); + }, [handleConditionalQueryModification, history]); const onCancel = (value: boolean) => (): void => { onModalToggle(value); From 29f1883eddcb0b31cbe7962f98c0cf39a5a68bf8 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 16 Aug 2024 16:16:12 +0530 Subject: [PATCH 04/60] chore: add telemetry for dashboards/alerts with tsv2 table (#5677) --- pkg/query-service/app/clickhouseReader/reader.go | 13 +++++++++++++ pkg/query-service/model/response.go | 2 ++ pkg/query-service/rules/db.go | 3 +++ pkg/query-service/telemetry/telemetry.go | 2 ++ 4 files changed, 20 insertions(+) diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index b34780c37d..5f33bfe2a9 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -3346,17 +3346,30 @@ func (r *ClickHouseReader) GetDashboardsInfo(ctx context.Context) (*model.Dashbo return &dashboardsInfo, err } totalDashboardsWithPanelAndName := 0 + count := 0 for _, dashboard := range dashboardsData { if isDashboardWithPanelAndName(dashboard.Data) { totalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName + 1 } dashboardsInfo = countPanelsInDashboard(dashboard.Data) + if isDashboardWithTSV2(dashboard.Data) { + count = count + 1 + } } dashboardsInfo.TotalDashboards = len(dashboardsData) dashboardsInfo.TotalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName + dashboardsInfo.QueriesWithTSV2 = count return &dashboardsInfo, nil } +func isDashboardWithTSV2(data map[string]interface{}) bool { + jsonData, err := json.Marshal(data) + if err != nil { + return false + } + return strings.Contains(string(jsonData), "time_series_v2") +} + func isDashboardWithPanelAndName(data map[string]interface{}) bool { isDashboardName := false isDashboardWithPanelAndName := false diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index eb979642da..b8250bd1d3 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -648,6 +648,7 @@ type AlertsInfo struct { MetricsClickHouseQueries int `json:"metricsClickHouseQueries"` MetricsPrometheusQueries int `json:"metricsPrometheusQueries"` SpanMetricsPrometheusQueries int `json:"spanMetricsPrometheusQueries"` + AlertsWithTSV2 int `json:"alertsWithTSv2"` } type SavedViewsInfo struct { @@ -661,6 +662,7 @@ type DashboardsInfo struct { TotalDashboardsWithPanelAndName int `json:"totalDashboardsWithPanelAndName"` // dashboards with panel and name without sample title LogsBasedPanels int `json:"logsBasedPanels"` MetricBasedPanels int `json:"metricBasedPanels"` + QueriesWithTSV2 int `json:"queriesWithTSV2"` TracesBasedPanels int `json:"tracesBasedPanels"` } diff --git a/pkg/query-service/rules/db.go b/pkg/query-service/rules/db.go index 64a35e3eb9..c49a8dd391 100644 --- a/pkg/query-service/rules/db.go +++ b/pkg/query-service/rules/db.go @@ -315,6 +315,9 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) { } for _, alert := range alertsData { var rule GettableRule + if strings.Contains(alert, "time_series_v2") { + alertsInfo.AlertsWithTSV2 = alertsInfo.AlertsWithTSV2 + 1 + } err = json.Unmarshal([]byte(alert), &rule) if err != nil { zap.L().Error("invalid rule data", zap.Error(err)) diff --git a/pkg/query-service/telemetry/telemetry.go b/pkg/query-service/telemetry/telemetry.go index 2c9dceb910..ecfa86298e 100644 --- a/pkg/query-service/telemetry/telemetry.go +++ b/pkg/query-service/telemetry/telemetry.go @@ -317,7 +317,9 @@ func createTelemetry() { "logsBasedPanels": dashboardsInfo.LogsBasedPanels, "metricBasedPanels": dashboardsInfo.MetricBasedPanels, "tracesBasedPanels": dashboardsInfo.TracesBasedPanels, + "dashboardsWithTSV2": dashboardsInfo.QueriesWithTSV2, "totalAlerts": alertsInfo.TotalAlerts, + "alertsWithTSV2": alertsInfo.AlertsWithTSV2, "logsBasedAlerts": alertsInfo.LogsBasedAlerts, "metricBasedAlerts": alertsInfo.MetricBasedAlerts, "tracesBasedAlerts": alertsInfo.TracesBasedAlerts, From 57c45f22d62f51907144f33f374f12a6dce3a52a Mon Sep 17 00:00:00 2001 From: Vibhu Pandey Date: Fri, 16 Aug 2024 16:50:40 +0530 Subject: [PATCH 05/60] feat(premium-support): add premium-support feature (#5707) --- ee/query-service/model/plans.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ee/query-service/model/plans.go b/ee/query-service/model/plans.go index 135d276cad..9b696c013f 100644 --- a/ee/query-service/model/plans.go +++ b/ee/query-service/model/plans.go @@ -12,6 +12,7 @@ const DisableUpsell = "DISABLE_UPSELL" const Onboarding = "ONBOARDING" const ChatSupport = "CHAT_SUPPORT" const Gateway = "GATEWAY" +const PremiumSupport = "PREMIUM_SUPPORT" var BasicPlan = basemodel.FeatureSet{ basemodel.Feature{ @@ -119,6 +120,13 @@ var BasicPlan = basemodel.FeatureSet{ UsageLimit: -1, Route: "", }, + basemodel.Feature{ + Name: PremiumSupport, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, } var ProPlan = basemodel.FeatureSet{ @@ -220,6 +228,13 @@ var ProPlan = basemodel.FeatureSet{ UsageLimit: -1, Route: "", }, + basemodel.Feature{ + Name: PremiumSupport, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, } var EnterprisePlan = basemodel.FeatureSet{ @@ -335,4 +350,11 @@ var EnterprisePlan = basemodel.FeatureSet{ UsageLimit: -1, Route: "", }, + basemodel.Feature{ + Name: PremiumSupport, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, } From 0401c27dbc343ab3418e2cd0afa55fd0214d29d8 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Fri, 16 Aug 2024 17:41:19 +0530 Subject: [PATCH 06/60] chore: remove the base URL from the ws config (#5708) --- frontend/src/constants/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/constants/env.ts b/frontend/src/constants/env.ts index adeb98b4a0..cf75739eff 100644 --- a/frontend/src/constants/env.ts +++ b/frontend/src/constants/env.ts @@ -3,5 +3,5 @@ export const ENVIRONMENT = { process?.env?.FRONTEND_API_ENDPOINT || process?.env?.GITPOD_WORKSPACE_URL?.replace('://', '://8080-') || '', - wsURL: process?.env?.WEBSOCKET_API_ENDPOINT || 'ws://localhost:8080/', + wsURL: process?.env?.WEBSOCKET_API_ENDPOINT || '', }; From 871e5ada9e7d33677aaf3e06c966675aed806c40 Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Fri, 16 Aug 2024 18:08:59 +0530 Subject: [PATCH 07/60] chore: dashboard and alert names (#5705) * chore: dashboard names * chore: fix panel count --- .../app/clickhouseReader/reader.go | 25 ++++++++++- pkg/query-service/model/response.go | 44 ++++++++++--------- pkg/query-service/rules/db.go | 4 +- pkg/query-service/telemetry/telemetry.go | 2 + 4 files changed, 52 insertions(+), 23 deletions(-) diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 5f33bfe2a9..8dce05250d 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -3346,16 +3346,26 @@ func (r *ClickHouseReader) GetDashboardsInfo(ctx context.Context) (*model.Dashbo return &dashboardsInfo, err } totalDashboardsWithPanelAndName := 0 + var dashboardNames []string count := 0 for _, dashboard := range dashboardsData { if isDashboardWithPanelAndName(dashboard.Data) { totalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName + 1 } - dashboardsInfo = countPanelsInDashboard(dashboard.Data) + dashboardName := extractDashboardName(dashboard.Data) + if dashboardName != "" { + dashboardNames = append(dashboardNames, dashboardName) + } + dashboardInfo := countPanelsInDashboard(dashboard.Data) + dashboardsInfo.LogsBasedPanels += dashboardInfo.LogsBasedPanels + dashboardsInfo.TracesBasedPanels += dashboardInfo.TracesBasedPanels + dashboardsInfo.MetricBasedPanels += dashboardsInfo.MetricBasedPanels if isDashboardWithTSV2(dashboard.Data) { count = count + 1 } } + + dashboardsInfo.DashboardNames = dashboardNames dashboardsInfo.TotalDashboards = len(dashboardsData) dashboardsInfo.TotalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName dashboardsInfo.QueriesWithTSV2 = count @@ -3389,6 +3399,19 @@ func isDashboardWithPanelAndName(data map[string]interface{}) bool { return isDashboardWithPanelAndName } + +func extractDashboardName(data map[string]interface{}) string { + + if data != nil && data["title"] != nil { + title, ok := data["title"].(string) + if ok { + return title + } + } + + return "" +} + func countPanelsInDashboard(data map[string]interface{}) model.DashboardsInfo { var logsPanelCount, tracesPanelCount, metricsPanelCount int // totalPanels := 0 diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index b8250bd1d3..83df872175 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -634,21 +634,22 @@ type TagsInfo struct { } type AlertsInfo struct { - TotalAlerts int `json:"totalAlerts"` - LogsBasedAlerts int `json:"logsBasedAlerts"` - MetricBasedAlerts int `json:"metricBasedAlerts"` - TracesBasedAlerts int `json:"tracesBasedAlerts"` - SlackChannels int `json:"slackChannels"` - WebHookChannels int `json:"webHookChannels"` - PagerDutyChannels int `json:"pagerDutyChannels"` - OpsGenieChannels int `json:"opsGenieChannels"` - EmailChannels int `json:"emailChannels"` - MSTeamsChannels int `json:"microsoftTeamsChannels"` - MetricsBuilderQueries int `json:"metricsBuilderQueries"` - MetricsClickHouseQueries int `json:"metricsClickHouseQueries"` - MetricsPrometheusQueries int `json:"metricsPrometheusQueries"` - SpanMetricsPrometheusQueries int `json:"spanMetricsPrometheusQueries"` - AlertsWithTSV2 int `json:"alertsWithTSv2"` + TotalAlerts int `json:"totalAlerts"` + LogsBasedAlerts int `json:"logsBasedAlerts"` + MetricBasedAlerts int `json:"metricBasedAlerts"` + TracesBasedAlerts int `json:"tracesBasedAlerts"` + SlackChannels int `json:"slackChannels"` + WebHookChannels int `json:"webHookChannels"` + PagerDutyChannels int `json:"pagerDutyChannels"` + OpsGenieChannels int `json:"opsGenieChannels"` + EmailChannels int `json:"emailChannels"` + MSTeamsChannels int `json:"microsoftTeamsChannels"` + MetricsBuilderQueries int `json:"metricsBuilderQueries"` + MetricsClickHouseQueries int `json:"metricsClickHouseQueries"` + MetricsPrometheusQueries int `json:"metricsPrometheusQueries"` + SpanMetricsPrometheusQueries int `json:"spanMetricsPrometheusQueries"` + AlertNames []string `json:"alertNames"` + AlertsWithTSV2 int `json:"alertsWithTSv2"` } type SavedViewsInfo struct { @@ -658,12 +659,13 @@ type SavedViewsInfo struct { } type DashboardsInfo struct { - TotalDashboards int `json:"totalDashboards"` - TotalDashboardsWithPanelAndName int `json:"totalDashboardsWithPanelAndName"` // dashboards with panel and name without sample title - LogsBasedPanels int `json:"logsBasedPanels"` - MetricBasedPanels int `json:"metricBasedPanels"` - QueriesWithTSV2 int `json:"queriesWithTSV2"` - TracesBasedPanels int `json:"tracesBasedPanels"` + TotalDashboards int `json:"totalDashboards"` + TotalDashboardsWithPanelAndName int `json:"totalDashboardsWithPanelAndName"` // dashboards with panel and name without sample title + LogsBasedPanels int `json:"logsBasedPanels"` + MetricBasedPanels int `json:"metricBasedPanels"` + TracesBasedPanels int `json:"tracesBasedPanels"` + DashboardNames []string `json:"dashboardNames"` + QueriesWithTSV2 int `json:"queriesWithTSV2"` } type TagTelemetryData struct { diff --git a/pkg/query-service/rules/db.go b/pkg/query-service/rules/db.go index c49a8dd391..37e45f2711 100644 --- a/pkg/query-service/rules/db.go +++ b/pkg/query-service/rules/db.go @@ -308,6 +308,7 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) { // fetch alerts from rules db query := "SELECT data FROM rules" var alertsData []string + var alertNames []string err := r.Select(&alertsData, query) if err != nil { zap.L().Error("Error in processing sql query", zap.Error(err)) @@ -323,6 +324,7 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) { zap.L().Error("invalid rule data", zap.Error(err)) continue } + alertNames = append(alertNames, rule.AlertName) if rule.AlertType == "LOGS_BASED_ALERT" { alertsInfo.LogsBasedAlerts = alertsInfo.LogsBasedAlerts + 1 } else if rule.AlertType == "METRIC_BASED_ALERT" { @@ -346,6 +348,6 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) { } alertsInfo.TotalAlerts = alertsInfo.TotalAlerts + 1 } - + alertsInfo.AlertNames = alertNames return &alertsInfo, nil } diff --git a/pkg/query-service/telemetry/telemetry.go b/pkg/query-service/telemetry/telemetry.go index ecfa86298e..c916135f4e 100644 --- a/pkg/query-service/telemetry/telemetry.go +++ b/pkg/query-service/telemetry/telemetry.go @@ -314,6 +314,8 @@ func createTelemetry() { dashboardsAlertsData := map[string]interface{}{ "totalDashboards": dashboardsInfo.TotalDashboards, "totalDashboardsWithPanelAndName": dashboardsInfo.TotalDashboardsWithPanelAndName, + "dashboardNames": dashboardsInfo.DashboardNames, + "alertNames": alertsInfo.AlertNames, "logsBasedPanels": dashboardsInfo.LogsBasedPanels, "metricBasedPanels": dashboardsInfo.MetricBasedPanels, "tracesBasedPanels": dashboardsInfo.TracesBasedPanels, From 79e96e544f059448258e22c63062de16d3374a53 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Fri, 16 Aug 2024 18:51:02 +0530 Subject: [PATCH 08/60] chore: added leading slash for for ws URL (#5709) --- frontend/src/api/common/getQueryStats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/api/common/getQueryStats.ts b/frontend/src/api/common/getQueryStats.ts index 98f25c6486..7277a32852 100644 --- a/frontend/src/api/common/getQueryStats.ts +++ b/frontend/src/api/common/getQueryStats.ts @@ -17,7 +17,7 @@ export function getQueryStats(props: GetQueryStatsProps): void { const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || ''; const socket = new WebSocket( - `${ENVIRONMENT.wsURL}api/v3/query_progress?q=${queryId}`, + `${ENVIRONMENT.wsURL}/api/v3/query_progress?q=${queryId}`, token, ); From f0c9f1289790d003eb0e2dee18beaa78dd5f7c96 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Tue, 20 Aug 2024 13:17:56 +0530 Subject: [PATCH 09/60] fix: do not use relative URLs for ws connections (#5715) * fix: do not use relative URLs for ws connections * fix: handle local env better * chore: update the websocket endpoint * chore: handle OSS/Docker installations --- frontend/src/api/common/getQueryStats.ts | 25 ++++++++++++++++--- .../tests/LogsExplorerViews.test.tsx | 4 ++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/frontend/src/api/common/getQueryStats.ts b/frontend/src/api/common/getQueryStats.ts index 7277a32852..b70c4c722a 100644 --- a/frontend/src/api/common/getQueryStats.ts +++ b/frontend/src/api/common/getQueryStats.ts @@ -1,6 +1,7 @@ import getLocalStorageApi from 'api/browser/localstorage/get'; import { ENVIRONMENT } from 'constants/env'; import { LOCALSTORAGE } from 'constants/localStorage'; +import { isEmpty } from 'lodash-es'; export interface WsDataEvent { read_rows: number; @@ -12,14 +13,30 @@ interface GetQueryStatsProps { setData: React.Dispatch>; } +function getURL(baseURL: string, queryId: string): URL | string { + if (baseURL && !isEmpty(baseURL)) { + return `${baseURL}/ws/query_progress?q=${queryId}`; + } + const url = new URL(`/ws/query_progress?q=${queryId}`, window.location.href); + + if (window.location.protocol === 'http:') { + url.protocol = 'ws'; + } else { + url.protocol = 'wss'; + } + + return url; +} + export function getQueryStats(props: GetQueryStatsProps): void { const { queryId, setData } = props; const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || ''; - const socket = new WebSocket( - `${ENVIRONMENT.wsURL}/api/v3/query_progress?q=${queryId}`, - token, - ); + + // https://github.com/whatwg/websockets/issues/20 reason for not using the relative URLs + const url = getURL(ENVIRONMENT.wsURL, queryId); + + const socket = new WebSocket(url, token); socket.addEventListener('message', (event) => { try { diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index 67c3fcfe2c..8262d6f9bc 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -46,7 +46,9 @@ jest.mock( }, ); -jest.mock('api/common/getQueryStats', () => jest.fn()); +jest.mock('api/common/getQueryStats', () => ({ + getQueryStats: jest.fn(), +})); jest.mock('constants/panelTypes', () => ({ AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'], From ff8df5dc3660a268538453512121c64eb554b985 Mon Sep 17 00:00:00 2001 From: Raj Kamal Singh <1133322+raj-k-singh@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:26:34 +0530 Subject: [PATCH 10/60] chore: use base prefix of /ws for websocket paths (#5719) Co-authored-by: Vikrant Gupta --- ee/query-service/app/server.go | 1 + pkg/query-service/app/http_handler.go | 7 ++++++- pkg/query-service/app/server.go | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 348cdbddd2..082ddcd358 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -359,6 +359,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e apiHandler.RegisterIntegrationRoutes(r, am) apiHandler.RegisterQueryRangeV3Routes(r, am) apiHandler.RegisterQueryRangeV4Routes(r, am) + apiHandler.RegisterWebSocketPaths(r, am) c := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 97b5f0de2a..5064cc359b 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -333,13 +333,18 @@ func (aH *APIHandler) RegisterQueryRangeV3Routes(router *mux.Router, am *AuthMid subRouter.HandleFunc("/filter_suggestions", am.ViewAccess(aH.getQueryBuilderSuggestions)).Methods(http.MethodGet) - // websocket handler for query progress + // TODO(Raj): Remove this handler after /ws based path has been completely rolled out. subRouter.HandleFunc("/query_progress", am.ViewAccess(aH.GetQueryProgressUpdates)).Methods(http.MethodGet) // live logs subRouter.HandleFunc("/logs/livetail", am.ViewAccess(aH.liveTailLogs)).Methods(http.MethodGet) } +func (aH *APIHandler) RegisterWebSocketPaths(router *mux.Router, am *AuthMiddleware) { + subRouter := router.PathPrefix("/ws").Subrouter() + subRouter.HandleFunc("/query_progress", am.ViewAccess(aH.GetQueryProgressUpdates)).Methods(http.MethodGet) +} + func (aH *APIHandler) RegisterQueryRangeV4Routes(router *mux.Router, am *AuthMiddleware) { subRouter := router.PathPrefix("/api/v4").Subrouter() subRouter.HandleFunc("/query_range", am.ViewAccess(aH.QueryRangeV4)).Methods(http.MethodPost) diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 3f5352edbd..4fb4d9ad22 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -303,6 +303,7 @@ func (s *Server) createPublicServer(api *APIHandler) (*http.Server, error) { api.RegisterLogsRoutes(r, am) api.RegisterIntegrationRoutes(r, am) api.RegisterQueryRangeV3Routes(r, am) + api.RegisterWebSocketPaths(r, am) api.RegisterQueryRangeV4Routes(r, am) api.RegisterMessagingQueuesRoutes(r, am) From 98367fd0549ab0a2bb73c55a974e08c9faeac47d Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 20 Aug 2024 15:08:29 +0530 Subject: [PATCH 11/60] fix: add missing selected time range variables (#5714) --- .../variables/dashboardVariablesQuery.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts b/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts index 8605ce75f1..5e5c333520 100644 --- a/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts +++ b/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts @@ -1,6 +1,8 @@ import { ApiV2Instance as axios } from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; +import getStartEndRangeTime from 'lib/getStartEndRangeTime'; +import store from 'store'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { Props, @@ -11,7 +13,26 @@ const dashboardVariablesQuery = async ( props: Props, ): Promise | ErrorResponse> => { try { - const response = await axios.post(`/variables/query`, props); + const { globalTime } = store.getState(); + const { start, end } = getStartEndRangeTime({ + type: 'GLOBAL_TIME', + interval: globalTime.selectedTime, + }); + + const timeVariables: Record = { + start_timestamp_ms: parseInt(start, 10) * 1e3, + end_timestamp_ms: parseInt(end, 10) * 1e3, + start_timestamp_nano: parseInt(start, 10) * 1e9, + end_timestamp_nano: parseInt(end, 10) * 1e9, + start_timestamp: parseInt(start, 10), + end_timestamp: parseInt(end, 10), + }; + + const payload = { ...props }; + + payload.variables = { ...payload.variables, ...timeVariables }; + + const response = await axios.post(`/variables/query`, payload); return { statusCode: 200, From 5796d6cb8c836d88207022b74609f23892495b3c Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Tue, 20 Aug 2024 17:09:17 +0530 Subject: [PATCH 12/60] feat: rewrite the query builder search component (#5659) * feat: make the query builder search extensible * feat: setup the framework and necessary states needed * feat: cover the happy path of selects * chore: forward typing flow handled * chore: add antd select * chore: add antd select * chore: handle forward and backward flows * feat: added tag properites to the search bar and multi tag partial handling * feat: handle tag on blur and body contains changes * feat: handle tag deselect * feat: multi tag handling * feat: multi tag handling * fix: jest test cases * chore: update the key * chore: add edit tag not working as expected * feat: handle cases for exists and nexists * fix: handle has / nhas operators * chore: fix usability issues * chore: remove the usage for the new bar * fix: flaky build issues * feat: client changes for consumption and design changes for where clause in logs explorer (#5712) * feat: query search new ui * feat: suggestions changes in v2 * feat: dropdown and tags ui touch up * feat: added missing keyboard shortcuts * fix: race condition issues * feat: remove usage * fix: operator select fix * fix: handle example queries click changes * chore: design sync * chore: handle boolean selects * chore: address review comments --- .../src/container/LogsExplorerViews/index.tsx | 3 +- .../QueryBuilderSearchDropdown.tsx | 112 +++ .../QueryBuilderSearchV2.styles.scss | 261 ++++++ .../QueryBuilderSearchV2.tsx | 862 ++++++++++++++++++ .../Suggestions.styles.scss | 147 +++ .../QueryBuilderSearchV2/Suggestions.tsx | 75 ++ .../queryBuilder/useGetAggregateValues.ts | 33 + .../src/hooks/queryBuilder/useIsValidTag.ts | 2 +- .../src/hooks/queryBuilder/useOperatorType.ts | 2 +- 9 files changed, 1494 insertions(+), 3 deletions(-) create mode 100644 frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx create mode 100644 frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss create mode 100644 frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx create mode 100644 frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss create mode 100644 frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.tsx create mode 100644 frontend/src/hooks/queryBuilder/useGetAggregateValues.ts diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 35aa3f6e33..bc7002e7dc 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -264,7 +264,8 @@ function LogsExplorerViews({ undefined, listQueryKeyRef, { - ...(!isEmpty(queryId) && { 'X-SIGNOZ-QUERY-ID': queryId }), + ...(!isEmpty(queryId) && + selectedPanelType !== PANEL_TYPES.LIST && { 'X-SIGNOZ-QUERY-ID': queryId }), }, ); diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx new file mode 100644 index 0000000000..c910ca1be4 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx @@ -0,0 +1,112 @@ +/* eslint-disable no-nested-ternary */ +import { Typography } from 'antd'; +import { + ArrowDown, + ArrowUp, + ChevronUp, + Command, + CornerDownLeft, + Slash, +} from 'lucide-react'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; +import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS'; + +import ExampleQueriesRendererForLogs from '../QueryBuilderSearch/ExampleQueriesRendererForLogs'; +import { convertExampleQueriesToOptions } from '../QueryBuilderSearch/utils'; +import { ITag, Option } from './QueryBuilderSearchV2'; + +interface ICustomDropdownProps { + menu: React.ReactElement; + searchValue: string; + tags: ITag[]; + options: Option[]; + exampleQueries: TagFilter[]; + onChange: (value: TagFilter) => void; + currentFilterItem?: ITag; +} + +export default function QueryBuilderSearchDropdown( + props: ICustomDropdownProps, +): React.ReactElement { + const { + menu, + currentFilterItem, + searchValue, + tags, + exampleQueries, + options, + onChange, + } = props; + const userOs = getUserOperatingSystem(); + return ( + <> +
+ {!currentFilterItem?.key ? ( +
Suggested Filters
+ ) : !currentFilterItem?.op ? ( +
+ + Operator for{' '} + + + {currentFilterItem?.key?.key} + +
+ ) : ( +
+ + Value(s) for{' '} + + + {currentFilterItem?.key?.key} {currentFilterItem?.op} + +
+ )} + {menu} + {!searchValue && tags.length === 0 && ( +
+
Example Queries
+
+ {convertExampleQueriesToOptions(exampleQueries).map((query) => ( + + ))} +
+
+ )} +
+ +
+
+ + + to navigate +
+
+ + to update query +
+ {!currentFilterItem?.key && options.length > 3 && ( +
+ {userOs === UserOperatingSystem.MACOS ? ( + + ) : ( + + )} + + + + Show all filter items +
+ )} +
+ + ); +} + +QueryBuilderSearchDropdown.defaultProps = { + currentFilterItem: undefined, +}; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss new file mode 100644 index 0000000000..1ca8bd7529 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss @@ -0,0 +1,261 @@ +.query-builder-search-v2 { + display: flex; + gap: 4px; + + .show-all-filters { + .content { + .rc-virtual-list-holder { + height: 100px; + } + } + } + + .content { + .suggested-filters { + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; + letter-spacing: 0.88px; + text-transform: uppercase; + padding: 12px 0px 8px 14px; + } + + .operator-for { + display: flex; + align-items: center; + gap: 6px; + padding: 12px 0px 8px 14px; + + .operator-for-text { + color: var(--bg-slate-200); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .operator-for-value { + display: flex; + align-items: center; + height: 20px; + padding: 0px 8px; + justify-content: center; + gap: 4px; + border-radius: 50px; + background: rgba(255, 255, 255, 0.1); + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: -0.06px; + } + } + + .value-for { + display: flex; + align-items: center; + gap: 6px; + padding: 12px 0px 8px 14px; + .value-for-text { + color: var(--bg-slate-200); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .value-for-value { + display: flex; + align-items: center; + height: 20px; + padding: 0px 8px; + justify-content: center; + gap: 4px; + border-radius: 50px; + background: rgba(255, 255, 255, 0.1); + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: -0.06px; + } + } + .example-queries { + cursor: default; + .heading { + padding: 12px 14px 8px 14px; + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .query-container { + display: flex; + flex-direction: column; + gap: 12px; + padding: 0px 12px 12px 12px; + cursor: pointer; + + .example-query { + display: flex; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 2px; + background: var(--bg-ink-200); + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.07px; + width: fit-content; + } + + .example-query:hover { + color: var(--bg-vanilla-100); + } + } + } + } + + .keyboard-shortcuts { + display: flex; + align-items: center; + border-radius: 0px 0px 4px 4px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + padding: 11px 16px; + cursor: default; + + .icons { + width: 16px; + height: 16px; + flex-shrink: 0; + border-radius: 2.286px; + border-top: 1.143px solid var(--bg-ink-200); + border-right: 1.143px solid var(--bg-ink-200); + border-bottom: 2.286px solid var(--bg-ink-200); + border-left: 1.143px solid var(--bg-ink-200); + background: var(--Ink-400, #121317); + } + + .keyboard-text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .navigate { + display: flex; + align-items: center; + padding-right: 12px; + gap: 4px; + border-right: 1px solid #1d212d; + } + + .update-query { + display: flex; + align-items: center; + margin-left: 12px; + gap: 4px; + } + .show-all-filter-items { + padding-left: 12px; + border-left: 1px solid #1d212d; + display: flex; + align-items: center; + margin-left: 12px; + gap: 4px; + } + } + + .search-bar { + width: 100%; + } + + .qb-search-bar-tokenised-tags { + .ant-tag { + display: flex; + align-items: center; + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-300); + background: var(--bg-slate-300); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + padding: 0px; + + .ant-typography { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px !important; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + padding: 2px 6px; + } + + .ant-tag-close-icon { + display: flex; + align-items: center; + justify-content: center; + border-radius: 0px 2px 2px 0px; + width: 20px; + height: 24px; + flex-shrink: 0; + margin-inline-start: 0px !important; + margin-inline-end: 0px !important; + } + + &.resource { + border: 1px solid rgba(242, 71, 105, 0.2); + + .ant-typography { + color: var(--bg-sakura-400); + background: rgba(245, 108, 135, 0.1); + font-size: 14px; + } + + .ant-tag-close-icon { + background: rgba(245, 108, 135, 0.1); + } + } + &.tag { + border: 1px solid rgba(189, 153, 121, 0.2); + + .ant-typography { + color: var(--bg-sienna-400); + background: rgba(189, 153, 121, 0.1); + font-size: 14px; + } + + .ant-tag-close-icon { + background: rgba(189, 153, 121, 0.1); + } + } + } + } +} diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx new file mode 100644 index 0000000000..d53a517c48 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx @@ -0,0 +1,862 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +import './QueryBuilderSearchV2.styles.scss'; + +import { Select, Spin, Tag, Tooltip } from 'antd'; +import cx from 'classnames'; +import { + OPERATORS, + QUERY_BUILDER_OPERATORS_BY_TYPES, + QUERY_BUILDER_SEARCH_VALUES, +} from 'constants/queryBuilder'; +import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; +import ROUTES from 'constants/routes'; +import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts'; +import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; +import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete'; +import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; +import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'; +import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions'; +import { validationMapper } from 'hooks/queryBuilder/useIsValidTag'; +import { operatorTypeMapper } from 'hooks/queryBuilder/useOperatorType'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import useDebounceValue from 'hooks/useDebounce'; +import { + cloneDeep, + isArray, + isEmpty, + isEqual, + isObject, + isUndefined, + unset, +} from 'lodash-es'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import type { BaseSelectRef } from 'rc-select'; +import { + KeyboardEvent, + ReactElement, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useLocation } from 'react-router-dom'; +import { + BaseAutocompleteData, + DataTypes, +} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { + IBuilderQuery, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource } from 'types/common/queryBuilder'; +import { popupContainer } from 'utils/selectPopupContainer'; +import { v4 as uuid } from 'uuid'; + +import { selectStyle } from '../QueryBuilderSearch/config'; +import { PLACEHOLDER } from '../QueryBuilderSearch/constant'; +import { TypographyText } from '../QueryBuilderSearch/style'; +import { getTagToken, isInNInOperator } from '../QueryBuilderSearch/utils'; +import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown'; +import Suggestions from './Suggestions'; + +export interface ITag { + id?: string; + key: BaseAutocompleteData; + op: string; + value: string[] | string | number | boolean; +} + +interface CustomTagProps { + label: React.ReactNode; + value: string; + disabled: boolean; + onClose: () => void; + closable: boolean; +} + +interface QueryBuilderSearchV2Props { + query: IBuilderQuery; + onChange: (value: TagFilter) => void; + whereClauseConfig?: WhereClauseConfig; + placeholder?: string; + className?: string; + suffixIcon?: React.ReactNode; +} + +export interface Option { + label: string; + value: BaseAutocompleteData | string; +} + +export enum DropdownState { + ATTRIBUTE_KEY = 'ATTRIBUTE_KEY', + OPERATOR = 'OPERATOR', + ATTRIBUTE_VALUE = 'ATTRIBUTE_VALUE', +} + +function getInitTags(query: IBuilderQuery): ITag[] { + return query.filters.items.map((item) => ({ + id: item.id, + key: item.key as BaseAutocompleteData, + op: item.op, + value: `${item.value}`, + })); +} + +function QueryBuilderSearchV2( + props: QueryBuilderSearchV2Props, +): React.ReactElement { + const { + query, + onChange, + placeholder, + className, + suffixIcon, + whereClauseConfig, + } = props; + + const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); + + const { handleRunQuery, currentQuery } = useQueryBuilder(); + + const selectRef = useRef(null); + + const [isOpen, setIsOpen] = useState(false); + + // create the tags from the initial query here, this should only be computed on the first load as post that tags and query will be always in sync. + const [tags, setTags] = useState(() => getInitTags(query)); + + // this will maintain the current state of in process filter item + const [currentFilterItem, setCurrentFilterItem] = useState(); + + const [currentState, setCurrentState] = useState( + DropdownState.ATTRIBUTE_KEY, + ); + + // to maintain the current running state until the tokenization happens for the tag + const [searchValue, setSearchValue] = useState(''); + + const [dropdownOptions, setDropdownOptions] = useState([]); + + const [showAllFilters, setShowAllFilters] = useState(false); + + const { pathname } = useLocation(); + const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ + pathname, + ]); + + const memoizedSearchParams = useMemo( + () => [ + searchValue, + query.dataSource, + query.aggregateOperator, + query.aggregateAttribute.key, + ], + [ + searchValue, + query.dataSource, + query.aggregateOperator, + query.aggregateAttribute.key, + ], + ); + + const queryFiltersWithoutId = useMemo( + () => ({ + ...query.filters, + items: query.filters.items.map((item) => { + const filterWithoutId = cloneDeep(item); + unset(filterWithoutId, 'id'); + return filterWithoutId; + }), + }), + [query.filters], + ); + + const memoizedSuggestionsParams = useMemo( + () => [searchValue, query.dataSource, queryFiltersWithoutId], + [query.dataSource, queryFiltersWithoutId, searchValue], + ); + + const memoizedValueParams = useMemo( + () => [ + query.aggregateOperator, + query.dataSource, + query.aggregateAttribute.key, + currentFilterItem?.key?.key || '', + currentFilterItem?.key?.dataType, + currentFilterItem?.key?.type ?? '', + isArray(currentFilterItem?.value) + ? currentFilterItem?.value?.[currentFilterItem.value.length - 1] + : currentFilterItem?.value, + ], + [ + query.aggregateOperator, + query.dataSource, + query.aggregateAttribute.key, + currentFilterItem?.key?.key, + currentFilterItem?.key?.dataType, + currentFilterItem?.key?.type, + currentFilterItem?.value, + ], + ); + + const searchParams = useDebounceValue(memoizedSearchParams, DEBOUNCE_DELAY); + + const valueParams = useDebounceValue(memoizedValueParams, DEBOUNCE_DELAY); + + const suggestionsParams = useDebounceValue( + memoizedSuggestionsParams, + DEBOUNCE_DELAY, + ); + + const isQueryEnabled = useMemo(() => { + if (currentState === DropdownState.ATTRIBUTE_KEY) { + return query.dataSource === DataSource.METRICS + ? !!query.aggregateOperator && + !!query.dataSource && + !!query.aggregateAttribute.dataType + : true; + } + return false; + }, [ + currentState, + query.aggregateAttribute.dataType, + query.aggregateOperator, + query.dataSource, + ]); + + const { data, isFetching } = useGetAggregateKeys( + { + searchText: searchValue, + dataSource: query.dataSource, + aggregateOperator: query.aggregateOperator, + aggregateAttribute: query.aggregateAttribute.key, + tagType: query.aggregateAttribute.type ?? null, + }, + { + queryKey: [searchParams], + enabled: isQueryEnabled && !isLogsExplorerPage, + }, + ); + + const { + data: suggestionsData, + isFetching: isFetchingSuggestions, + } = useGetAttributeSuggestions( + { + searchText: searchValue.split(' ')[0], + dataSource: query.dataSource, + filters: query.filters, + }, + { + queryKey: [suggestionsParams], + enabled: isQueryEnabled && isLogsExplorerPage, + }, + ); + + const { + data: attributeValues, + isFetching: isFetchingAttributeValues, + } = useGetAggregateValues( + { + aggregateOperator: query.aggregateOperator, + dataSource: query.dataSource, + aggregateAttribute: query.aggregateAttribute.key, + attributeKey: currentFilterItem?.key?.key || '', + filterAttributeKeyDataType: + currentFilterItem?.key?.dataType ?? DataTypes.EMPTY, + tagType: currentFilterItem?.key?.type ?? '', + searchText: isArray(currentFilterItem?.value) + ? currentFilterItem?.value?.[currentFilterItem.value.length - 1] || '' + : currentFilterItem?.value?.toString() || '', + }, + { + enabled: currentState === DropdownState.ATTRIBUTE_VALUE, + queryKey: [valueParams], + }, + ); + + const handleDropdownSelect = useCallback( + (value: string) => { + let parsedValue: BaseAutocompleteData | string; + + try { + parsedValue = JSON.parse(value); + } catch { + parsedValue = value; + } + if (currentState === DropdownState.ATTRIBUTE_KEY) { + setCurrentFilterItem((prev) => ({ + ...prev, + key: parsedValue as BaseAutocompleteData, + op: '', + value: '', + })); + setCurrentState(DropdownState.OPERATOR); + setSearchValue((parsedValue as BaseAutocompleteData)?.key); + } else if (currentState === DropdownState.OPERATOR) { + if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) { + setTags((prev) => [ + ...prev, + { + key: currentFilterItem?.key, + op: value, + value: '', + } as ITag, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else { + setCurrentFilterItem((prev) => ({ + key: prev?.key as BaseAutocompleteData, + op: value as string, + value: '', + })); + setCurrentState(DropdownState.ATTRIBUTE_VALUE); + setSearchValue(`${currentFilterItem?.key?.key} ${value}`); + } + } else if (currentState === DropdownState.ATTRIBUTE_VALUE) { + const operatorType = + operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID'; + const isMulti = operatorType === QUERY_BUILDER_SEARCH_VALUES.MULTIPLY; + + if (isMulti) { + const { tagKey, tagOperator, tagValue } = getTagToken(searchValue); + const newSearch = [...tagValue]; + newSearch[newSearch.length === 0 ? 0 : newSearch.length - 1] = value; + const newSearchValue = newSearch.join(','); + setSearchValue(`${tagKey} ${tagOperator} ${newSearchValue},`); + } else { + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + setCurrentFilterItem(undefined); + setTags((prev) => [ + ...prev, + { + key: currentFilterItem?.key, + op: currentFilterItem?.op, + value, + } as ITag, + ]); + } + } + }, + [currentFilterItem?.key, currentFilterItem?.op, currentState, searchValue], + ); + + const handleSearch = useCallback((value: string) => { + setSearchValue(value); + }, []); + + const onInputKeyDownHandler = useCallback( + (event: KeyboardEvent): void => { + if (event.key === 'Backspace' && !searchValue) { + event.stopPropagation(); + setTags((prev) => prev.slice(0, -1)); + } + if ((event.ctrlKey || event.metaKey) && event.key === '/') { + event.preventDefault(); + event.stopPropagation(); + setShowAllFilters((prev) => !prev); + } + if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + handleRunQuery(); + setIsOpen(false); + } + }, + [handleRunQuery, searchValue], + ); + + const handleOnBlur = useCallback((): void => { + if (searchValue) { + const operatorType = + operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID'; + if ( + currentFilterItem?.key && + isEmpty(currentFilterItem?.op) && + whereClauseConfig?.customKey === 'body' && + whereClauseConfig?.customOp === OPERATORS.CONTAINS + ) { + setTags((prev) => [ + ...prev, + { + key: { + key: 'body', + dataType: DataTypes.String, + type: '', + isColumn: true, + isJSON: false, + id: 'body--string----true', + }, + op: OPERATORS.CONTAINS, + value: currentFilterItem?.key?.key, + }, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else if ( + currentFilterItem?.op === OPERATORS.EXISTS || + currentFilterItem?.op === OPERATORS.NOT_EXISTS + ) { + setTags((prev) => [ + ...prev, + { + key: currentFilterItem?.key, + op: currentFilterItem?.op, + value: '', + }, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else if ( + validationMapper[operatorType]?.( + isArray(currentFilterItem?.value) + ? currentFilterItem?.value.length || 0 + : 1, + ) + ) { + setTags((prev) => [ + ...prev, + { + key: currentFilterItem?.key as BaseAutocompleteData, + op: currentFilterItem?.op as string, + value: currentFilterItem?.value || '', + }, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } + } + }, [ + currentFilterItem?.key, + currentFilterItem?.op, + currentFilterItem?.value, + searchValue, + whereClauseConfig?.customKey, + whereClauseConfig?.customOp, + ]); + + // this useEffect takes care of tokenisation based on the search state + useEffect(() => { + if (isFetchingSuggestions) { + return; + } + if (!searchValue) { + setCurrentFilterItem(undefined); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } + const { tagKey, tagOperator, tagValue } = getTagToken(searchValue); + + if (tagKey && isUndefined(currentFilterItem?.key)) { + let currentRunningAttributeKey; + const isSuggestedKeyInAutocomplete = suggestionsData?.payload?.attributes?.some( + (value) => value.key === tagKey.split(' ')[0], + ); + + if (isSuggestedKeyInAutocomplete) { + const allAttributesMatchingTheKey = + suggestionsData?.payload?.attributes?.filter( + (value) => value.key === tagKey.split(' ')[0], + ) || []; + + if (allAttributesMatchingTheKey?.length === 1) { + [currentRunningAttributeKey] = allAttributesMatchingTheKey; + } + if (allAttributesMatchingTheKey?.length > 1) { + // the priority logic goes here + [currentRunningAttributeKey] = allAttributesMatchingTheKey; + } + + if (currentRunningAttributeKey) { + setCurrentFilterItem({ + key: currentRunningAttributeKey, + op: '', + value: '', + }); + + setCurrentState(DropdownState.OPERATOR); + } + } + if (suggestionsData?.payload?.attributes?.length === 0) { + setCurrentFilterItem({ + key: { + key: tagKey.split(' ')[0], + // update this for has and nhas operator , check the useEffect of source keys in older component for details + dataType: DataTypes.EMPTY, + type: '', + isColumn: false, + isJSON: false, + }, + op: '', + value: '', + }); + setCurrentState(DropdownState.OPERATOR); + } + } else if ( + currentFilterItem?.key && + currentFilterItem?.key?.key !== tagKey.split(' ')[0] + ) { + setCurrentFilterItem(undefined); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else if (tagOperator && isEmpty(currentFilterItem?.op)) { + if ( + tagOperator === OPERATORS.EXISTS || + tagOperator === OPERATORS.NOT_EXISTS + ) { + setTags((prev) => [ + ...prev, + { + key: currentFilterItem?.key, + op: tagOperator, + value: '', + } as ITag, + ]); + setCurrentFilterItem(undefined); + setSearchValue(''); + setCurrentState(DropdownState.ATTRIBUTE_KEY); + } else { + setCurrentFilterItem((prev) => ({ + key: prev?.key as BaseAutocompleteData, + op: tagOperator, + value: '', + })); + + setCurrentState(DropdownState.ATTRIBUTE_VALUE); + } + } else if ( + !isEmpty(currentFilterItem?.op) && + tagOperator !== currentFilterItem?.op + ) { + setCurrentFilterItem((prev) => ({ + key: prev?.key as BaseAutocompleteData, + op: '', + value: '', + })); + setCurrentState(DropdownState.OPERATOR); + } else if (!isEmpty(tagValue)) { + const currentValue = { + key: currentFilterItem?.key as BaseAutocompleteData, + operator: currentFilterItem?.op as string, + value: tagValue, + }; + if (!isEqual(currentValue, currentFilterItem)) { + setCurrentFilterItem((prev) => ({ + key: prev?.key as BaseAutocompleteData, + op: prev?.op as string, + value: tagValue, + })); + } + } + }, [ + currentFilterItem, + currentFilterItem?.key, + currentFilterItem?.op, + suggestionsData?.payload?.attributes, + searchValue, + isFetchingSuggestions, + ]); + + // the useEffect takes care of setting the dropdown values correctly on change of the current state + useEffect(() => { + if (currentState === DropdownState.ATTRIBUTE_KEY) { + if (isLogsExplorerPage) { + setDropdownOptions( + suggestionsData?.payload?.attributes?.map((key) => ({ + label: key.key, + value: key, + })) || [], + ); + } else { + setDropdownOptions( + data?.payload?.attributeKeys?.map((key) => ({ + label: key.key, + value: key, + })) || [], + ); + } + } + if (currentState === DropdownState.OPERATOR) { + const keyOperator = searchValue.split(' '); + const partialOperator = keyOperator?.[1]; + const strippedKey = keyOperator?.[0]; + + let operatorOptions; + if (currentFilterItem?.key?.dataType) { + operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES[ + currentFilterItem.key + .dataType as keyof typeof QUERY_BUILDER_OPERATORS_BY_TYPES + ].map((operator) => ({ + label: operator, + value: operator, + })); + + if (partialOperator) { + operatorOptions = operatorOptions.filter((op) => + op.label.startsWith(partialOperator.toLocaleUpperCase()), + ); + } + setDropdownOptions(operatorOptions); + } else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) { + operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({ + label: operator, + value: operator, + })); + setDropdownOptions(operatorOptions); + } else { + operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map( + (operator) => ({ + label: operator, + value: operator, + }), + ); + + if (partialOperator) { + operatorOptions = operatorOptions.filter((op) => + op.label.startsWith(partialOperator.toLocaleUpperCase()), + ); + } + setDropdownOptions(operatorOptions); + } + } + + if (currentState === DropdownState.ATTRIBUTE_VALUE) { + const values: string[] = + Object.values(attributeValues?.payload || {}).find((el) => !!el) || []; + + const { tagValue } = getTagToken(searchValue); + + if (values.length === 0) { + if (isArray(tagValue)) { + if (!isEmpty(tagValue[tagValue.length - 1])) + values.push(tagValue[tagValue.length - 1]); + } else if (!isEmpty(tagValue)) values.push(tagValue); + } + + setDropdownOptions( + values.map((val) => ({ + label: val, + value: val, + })), + ); + } + }, [ + attributeValues?.payload, + currentFilterItem?.key.dataType, + currentState, + data?.payload?.attributeKeys, + isLogsExplorerPage, + searchValue, + suggestionsData?.payload?.attributes, + ]); + + useEffect(() => { + const filterTags: IBuilderQuery['filters'] = { + op: 'AND', + items: [], + }; + tags.forEach((tag) => { + filterTags.items.push({ + id: tag.id || uuid().slice(0, 8), + key: tag.key, + op: tag.op, + value: tag.value, + }); + }); + + if (!isEqual(query.filters, filterTags)) { + onChange(filterTags); + setTags(filterTags.items as ITag[]); + } + }, [onChange, query.filters, tags]); + + const isLastQuery = useMemo( + () => + isEqual( + currentQuery.builder.queryData[currentQuery.builder.queryData.length - 1], + query, + ), + [currentQuery, query], + ); + + useEffect(() => { + if (isLastQuery) { + registerShortcut(LogsExplorerShortcuts.FocusTheSearchBar, () => { + // set timeout is needed here else the select treats the hotkey as input value + setTimeout(() => { + selectRef.current?.focus(); + }, 0); + }); + } + + return (): void => + deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar); + }, [deregisterShortcut, isLastQuery, registerShortcut]); + + const loading = useMemo( + () => isFetching || isFetchingAttributeValues || isFetchingSuggestions, + [isFetching, isFetchingAttributeValues, isFetchingSuggestions], + ); + + const isMetricsDataSource = useMemo( + () => query.dataSource === DataSource.METRICS, + [query.dataSource], + ); + + const queryTags = useMemo( + () => tags.map((tag) => `${tag.key.key} ${tag.op} ${tag.value}`), + [tags], + ); + + const onTagRender = ({ + value, + closable, + onClose, + }: CustomTagProps): React.ReactElement => { + const { tagOperator } = getTagToken(value); + const isInNin = isInNInOperator(tagOperator); + const chipValue = isInNin + ? value?.trim()?.replace(/,\s*$/, '') + : value?.trim(); + + const indexInQueryTags = queryTags.findIndex((qTag) => isEqual(qTag, value)); + const tagDetails = tags[indexInQueryTags]; + + const onCloseHandler = (): void => { + onClose(); + setSearchValue(''); + setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails))); + }; + + const tagEditHandler = (value: string): void => { + setCurrentFilterItem(tagDetails); + setSearchValue(value); + setCurrentState(DropdownState.ATTRIBUTE_VALUE); + setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails))); + }; + + const isDisabled = !!searchValue; + + return ( + + + + { + if (!isDisabled) tagEditHandler(value); + }} + > + {chipValue} + + + + + ); + }; + + return ( +
+ +
+ ); +} + +QueryBuilderSearchV2.defaultProps = { + placeholder: PLACEHOLDER, + className: '', + suffixIcon: null, + whereClauseConfig: {}, +}; + +export default QueryBuilderSearchV2; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss new file mode 100644 index 0000000000..362d6e4c6a --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss @@ -0,0 +1,147 @@ +.text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.07px; +} + +.dot { + height: 5px; + width: 5px; + border-radius: 50%; + background-color: var(--bg-slate-300); +} + +.option { + .container { + display: flex; + align-items: center; + justify-content: space-between; + + .left-section { + display: flex; + align-items: center; + width: 90%; + gap: 8px; + + .value { + } + } + .right-section { + display: flex; + align-items: center; + gap: 4px; + + .data-type { + display: flex; + height: 20px; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 20px; + background: rgba(255, 255, 255, 0.08); + } + + .type-tag { + display: flex; + align-items: center; + height: 20px; + padding: 0px 6px; + justify-content: center; + gap: 4px; + border-radius: 50px; + text-transform: capitalize; + + &.tag { + border-radius: 50px; + background: rgba(189, 153, 121, 0.1) !important; + color: var(--bg-sienna-400) !important; + + .dot { + background-color: var(--bg-sienna-400); + } + .text { + color: var(--bg-sienna-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: -0.06px; + } + } + + &.resource { + border-radius: 50px; + background: rgba(245, 108, 135, 0.1) !important; + color: var(--bg-sakura-400) !important; + + .dot { + background-color: var(--bg-sakura-400); + } + .text { + color: var(--bg-sakura-400); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: -0.06px; + } + } + } + } + .option-meta-data-container { + display: flex; + gap: 8px; + } + } + + .container-without-tag { + display: flex; + align-items: center; + gap: 8px; + + .OPERATOR { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + text-transform: uppercase; + width: 100%; + } + + .VALUE { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + text-transform: uppercase; + width: 100%; + } + } +} +.option:hover { + .container { + .left-section { + .value { + color: var(--bg-vanilla-100); + } + } + } + .container-without-tag { + .value { + color: var(--bg-vanilla-100); + } + } +} diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.tsx new file mode 100644 index 0000000000..49d6040b7b --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.tsx @@ -0,0 +1,75 @@ +import './Suggestions.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Tooltip, Typography } from 'antd'; +import cx from 'classnames'; +import { isEmpty, isObject } from 'lodash-es'; +import { Zap } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +import { DropdownState } from './QueryBuilderSearchV2'; + +interface ISuggestionsProps { + label: string; + value: BaseAutocompleteData | string; + option: DropdownState; +} + +function Suggestions(props: ISuggestionsProps): React.ReactElement { + const { label, value, option } = props; + + const optionType = useMemo(() => { + if (isObject(value)) { + return value.type; + } + return ''; + }, [value]); + + const [truncated, setTruncated] = useState(false); + + return ( +
+ {!isEmpty(optionType) && isObject(value) ? ( + +
+
+ {value.isIndexed ? ( + + ) : ( +
+ )} + setTruncated(ellipsis) }} + > + {label} + +
+
+ {value.dataType} +
+
+ {value.type} +
+
+
+
+ ) : ( + +
+
+ setTruncated(ellipsis) }} + > + {`${label}`} + +
+ + )} +
+ ); +} + +export default Suggestions; diff --git a/frontend/src/hooks/queryBuilder/useGetAggregateValues.ts b/frontend/src/hooks/queryBuilder/useGetAggregateValues.ts new file mode 100644 index 0000000000..d749e5ec9a --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetAggregateValues.ts @@ -0,0 +1,33 @@ +import { getAttributesValues } from 'api/queryBuilder/getAttributesValues'; +import { useMemo } from 'react'; +import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + IAttributeValuesResponse, + IGetAttributeValuesPayload, +} from 'types/api/queryBuilder/getAttributesValues'; + +type UseGetAttributeValues = ( + requestData: IGetAttributeValuesPayload, + options?: UseQueryOptions< + SuccessResponse | ErrorResponse + >, +) => UseQueryResult | ErrorResponse>; + +export const useGetAggregateValues: UseGetAttributeValues = ( + requestData, + options, +) => { + const queryKey = useMemo(() => { + if (options?.queryKey && Array.isArray(options.queryKey)) { + return [...options.queryKey]; + } + return [requestData]; + }, [options?.queryKey, requestData]); + + return useQuery | ErrorResponse>({ + queryKey, + queryFn: () => getAttributesValues(requestData), + ...options, + }); +}; diff --git a/frontend/src/hooks/queryBuilder/useIsValidTag.ts b/frontend/src/hooks/queryBuilder/useIsValidTag.ts index 216971b0ac..d4e2b58080 100644 --- a/frontend/src/hooks/queryBuilder/useIsValidTag.ts +++ b/frontend/src/hooks/queryBuilder/useIsValidTag.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { OperatorType } from './useOperatorType'; -const validationMapper: Record< +export const validationMapper: Record< OperatorType, (resultLength: number) => boolean > = { diff --git a/frontend/src/hooks/queryBuilder/useOperatorType.ts b/frontend/src/hooks/queryBuilder/useOperatorType.ts index 94de55df92..1ff0619506 100644 --- a/frontend/src/hooks/queryBuilder/useOperatorType.ts +++ b/frontend/src/hooks/queryBuilder/useOperatorType.ts @@ -6,7 +6,7 @@ export type OperatorType = | 'NON_VALUE' | 'NOT_VALID'; -const operatorTypeMapper: Record = { +export const operatorTypeMapper: Record = { [OPERATORS.IN]: 'MULTIPLY_VALUE', [OPERATORS.NIN]: 'MULTIPLY_VALUE', [OPERATORS.EXISTS]: 'NON_VALUE', From 7cff07333fc1dcbac59d35308a8fe659db7f86b7 Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:45:22 +0530 Subject: [PATCH 13/60] fix: added onDragSelect to DBCall and External metric app (#5694) * fix: added onDragSelect to DBCall and External metric app * fix: handled back navigation --- .../GridCardLayout/GridCardLayout.tsx | 2 +- .../MetricsApplication/Tabs/DBCall.tsx | 31 +++++++++++++++-- .../MetricsApplication/Tabs/External.tsx | 34 +++++++++++++++++-- .../MetricsApplication/Tabs/Overview.tsx | 2 +- 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.tsx b/frontend/src/container/GridCardLayout/GridCardLayout.tsx index 75fde3ce7a..a96599b127 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.tsx +++ b/frontend/src/container/GridCardLayout/GridCardLayout.tsx @@ -194,7 +194,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element { urlQuery.set(QueryParams.startTime, startTimestamp.toString()); urlQuery.set(QueryParams.endTime, endTimestamp.toString()); const generatedUrl = `${pathname}?${urlQuery.toString()}`; - history.replace(generatedUrl); + history.push(generatedUrl); if (startTimestamp !== endTimestamp) { dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index 3ee49ddc9a..da7bbbfc60 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -1,6 +1,7 @@ import { Col } from 'antd'; import logEvent from 'api/common/logEvent'; import { ENTITY_VERSION_V4 } from 'constants/app'; +import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; import Graph from 'container/GridCardLayout/GridCard'; import { @@ -12,8 +13,12 @@ import { convertRawQueriesToTraceSelectedTags, resourceAttributesToTagFilterItems, } from 'hooks/useResourceAttribute/utils'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import useUrlQuery from 'hooks/useUrlQuery'; +import history from 'lib/history'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useLocation, useParams } from 'react-router-dom'; +import { UpdateTimeInterval } from 'store/actions'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; import { v4 as uuid } from 'uuid'; @@ -37,6 +42,26 @@ function DBCall(): JSX.Element { const servicename = decodeURIComponent(encodedServiceName); const [selectedTimeStamp, setSelectedTimeStamp] = useState(0); const { queries } = useResourceAttribute(); + const urlQuery = useUrlQuery(); + const { pathname } = useLocation(); + const dispatch = useDispatch(); + + const onDragSelect = useCallback( + (start: number, end: number) => { + const startTimestamp = Math.trunc(start); + const endTimestamp = Math.trunc(end); + + urlQuery.set(QueryParams.startTime, startTimestamp.toString()); + urlQuery.set(QueryParams.endTime, endTimestamp.toString()); + const generatedUrl = `${pathname}?${urlQuery.toString()}`; + history.push(generatedUrl); + + if (startTimestamp !== endTimestamp) { + dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); + } + }, + [dispatch, pathname, urlQuery], + ); const tagFilterItems: TagFilterItem[] = useMemo( () => @@ -150,6 +175,7 @@ function DBCall(): JSX.Element { 'database_call_rps', ); }} + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> @@ -185,6 +211,7 @@ function DBCall(): JSX.Element { 'database_call_avg_duration', ); }} + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index 69abd6696d..5ba3d3df6c 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -1,6 +1,7 @@ import { Col } from 'antd'; import logEvent from 'api/common/logEvent'; import { ENTITY_VERSION_V4 } from 'constants/app'; +import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; import Graph from 'container/GridCardLayout/GridCard'; import { @@ -14,8 +15,12 @@ import { convertRawQueriesToTraceSelectedTags, resourceAttributesToTagFilterItems, } from 'hooks/useResourceAttribute/utils'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import useUrlQuery from 'hooks/useUrlQuery'; +import history from 'lib/history'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useLocation, useParams } from 'react-router-dom'; +import { UpdateTimeInterval } from 'store/actions'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { EQueryType } from 'types/common/dashboard'; import { v4 as uuid } from 'uuid'; @@ -40,6 +45,27 @@ function External(): JSX.Element { const servicename = decodeURIComponent(encodedServiceName); const { queries } = useResourceAttribute(); + const urlQuery = useUrlQuery(); + const { pathname } = useLocation(); + const dispatch = useDispatch(); + + const onDragSelect = useCallback( + (start: number, end: number) => { + const startTimestamp = Math.trunc(start); + const endTimestamp = Math.trunc(end); + + urlQuery.set(QueryParams.startTime, startTimestamp.toString()); + urlQuery.set(QueryParams.endTime, endTimestamp.toString()); + const generatedUrl = `${pathname}?${urlQuery.toString()}`; + history.push(generatedUrl); + + if (startTimestamp !== endTimestamp) { + dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); + } + }, + [dispatch, pathname, urlQuery], + ); + const tagFilterItems = useMemo( () => handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [], @@ -214,6 +240,7 @@ function External(): JSX.Element { 'external_call_error_percentage', ); }} + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> @@ -249,6 +276,7 @@ function External(): JSX.Element { 'external_call_duration', ); }} + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> @@ -285,6 +313,7 @@ function External(): JSX.Element { 'external_call_rps_by_address', ) } + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> @@ -320,6 +349,7 @@ function External(): JSX.Element { 'external_call_duration_by_address', ); }} + onDragSelect={onDragSelect} version={ENTITY_VERSION_V4} /> diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index 916c3435b5..241395b23a 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -185,7 +185,7 @@ function Application(): JSX.Element { urlQuery.set(QueryParams.startTime, startTimestamp.toString()); urlQuery.set(QueryParams.endTime, endTimestamp.toString()); const generatedUrl = `${pathname}?${urlQuery.toString()}`; - history.replace(generatedUrl); + history.push(generatedUrl); if (startTimestamp !== endTimestamp) { dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); From abc8096a39bacd97c30da6957c2440852b7caaf7 Mon Sep 17 00:00:00 2001 From: Vibhu Pandey Date: Tue, 20 Aug 2024 18:16:07 +0530 Subject: [PATCH 14/60] chore(codeowners): update codeowners to team (#5726) --- .github/CODEOWNERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 573be5f290..ae8840a453 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,6 @@ /frontend/ @YounixM /frontend/src/container/MetricsApplication @srikanthccv /frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv -/deploy/ @prashant-shahi -/sample-apps/ @prashant-shahi -.github @prashant-shahi +/deploy/ @SigNoz/devops +/sample-apps/ @SigNoz/devops +.github @SigNoz/devops From fa0a065b95832a6ac2054f2d1c757ff669e22db7 Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Tue, 20 Aug 2024 18:41:34 +0530 Subject: [PATCH 15/60] chore: chat block events (#5725) Also add go to integration event --- .../ChatSupportGateway/ChatSupportGateway.tsx | 7 ++- .../LaunchChatSupport/LaunchChatSupport.tsx | 9 ++- .../src/components/LaunchChatSupport/util.ts | 15 +++++ .../Steps/DataSource/DataSource.tsx | 5 ++ .../ModuleStepsContainer.tsx | 56 +++++++------------ frontend/src/pages/Support/Support.tsx | 11 +++- 6 files changed, 60 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx b/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx index b361f725c3..67353f8ba2 100644 --- a/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx +++ b/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx @@ -7,6 +7,7 @@ import { useNotifications } from 'hooks/useNotifications'; import { CreditCard, X } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useMutation } from 'react-query'; +import { useLocation } from 'react-router-dom'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; import { License } from 'types/api/licenses/def'; @@ -57,11 +58,11 @@ export default function ChatSupportGateway(): JSX.Element { onError: handleBillingOnError, }, ); - + const { pathname } = useLocation(); const handleAddCreditCard = (): void => { logEvent('Add Credit card modal: Clicked', { source: `intercom icon`, - page: '', + page: pathname, }); updateCreditCard({ @@ -79,7 +80,7 @@ export default function ChatSupportGateway(): JSX.Element { onClick={(): void => { logEvent('Disabled Chat Support: Clicked', { source: `intercom icon`, - page: '', + page: pathname, }); setIsAddCreditCardModalOpen(true); diff --git a/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx b/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx index c568a7e82b..eb0659cfb1 100644 --- a/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx +++ b/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx @@ -13,6 +13,7 @@ import { defaultTo } from 'lodash-es'; import { CreditCard, HelpCircle, X } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useMutation } from 'react-query'; +import { useLocation } from 'react-router-dom'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; import { License } from 'types/api/licenses/def'; @@ -47,6 +48,7 @@ function LaunchChatSupport({ false, ); + const { pathname } = useLocation(); const isPremiumChatSupportEnabled = useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false; @@ -65,6 +67,11 @@ function LaunchChatSupport({ const handleFacingIssuesClick = (): void => { if (showAddCreditCardModal) { + logEvent('Disabled Chat Support: Clicked', { + source: `facing issues button`, + page: pathname, + ...attributes, + }); setIsAddCreditCardModalOpen(true); } else { logEvent(eventName, attributes); @@ -105,7 +112,7 @@ function LaunchChatSupport({ const handleAddCreditCard = (): void => { logEvent('Add Credit card modal: Clicked', { source: `facing issues button`, - page: '', + page: pathname, ...attributes, }); diff --git a/frontend/src/components/LaunchChatSupport/util.ts b/frontend/src/components/LaunchChatSupport/util.ts index b99a31e970..8b610b11cc 100644 --- a/frontend/src/components/LaunchChatSupport/util.ts +++ b/frontend/src/components/LaunchChatSupport/util.ts @@ -41,6 +41,21 @@ I need help with managing alerts. Thanks`; +export const onboardingHelpMessage = ( + dataSourceName: string, + moduleId: string, +): string => `Hi Team, + +I am facing issues sending data to SigNoz. Here are my application details + +Data Source: ${dataSourceName} +Framework: +Environment: +Module: ${moduleId} + +Thanks +`; + export const alertHelpMessage = ( alertDef: AlertDef, ruleId: number, diff --git a/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx b/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx index 138aac775c..f2f7028bdc 100644 --- a/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx +++ b/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx @@ -131,6 +131,11 @@ export default function DataSource(): JSX.Element { }; const goToIntegrationsPage = (): void => { + logEvent('Onboarding V2: Go to integrations', { + module: selectedModule?.id, + dataSource: selectedDataSource?.name, + framework: selectedFramework, + }); history.push(ROUTES.INTEGRATIONS); }; diff --git a/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx b/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx index ae74930d57..f9efa061f7 100644 --- a/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx +++ b/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx @@ -11,13 +11,15 @@ import { } from '@ant-design/icons'; import { Button, Space, Steps, Typography } from 'antd'; import logEvent from 'api/common/logEvent'; +import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport'; +import { onboardingHelpMessage } from 'components/LaunchChatSupport/util'; import ROUTES from 'constants/routes'; import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig'; import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource'; import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils'; import history from 'lib/history'; import { isEmpty, isNull } from 'lodash-es'; -import { HelpCircle, UserPlus } from 'lucide-react'; +import { UserPlus } from 'lucide-react'; import { SetStateAction, useState } from 'react'; import { useOnboardingContext } from '../../context/OnboardingContext'; @@ -381,31 +383,6 @@ export default function ModuleStepsContainer({ history.push('/'); }; - const handleFacingIssuesClick = (): void => { - logEvent('Onboarding V2: Facing Issues Sending Data to SigNoz', { - dataSource: selectedDataSource?.id, - framework: selectedFramework, - environment: selectedEnvironment, - module: activeStep?.module?.id, - step: activeStep?.step?.id, - }); - - const message = `Hi Team, - -I am facing issues sending data to SigNoz. Here are my application details - -Data Source: ${selectedDataSource?.name} -Framework: -Environment: -Module: ${activeStep?.module?.id} - -Thanks -`; - if (window.Intercom) { - window.Intercom('showNewMessage', message); - } - }; - return (
@@ -493,19 +470,26 @@ Thanks > Back - - - +
diff --git a/frontend/src/pages/Support/Support.tsx b/frontend/src/pages/Support/Support.tsx index ad13c3993d..0dbd7a9526 100644 --- a/frontend/src/pages/Support/Support.tsx +++ b/frontend/src/pages/Support/Support.tsx @@ -20,7 +20,7 @@ import { } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useMutation } from 'react-query'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; import { License } from 'types/api/licenses/def'; @@ -105,6 +105,7 @@ export default function Support(): JSX.Element { false, ); + const { pathname } = useLocation(); const handleChannelWithRedirects = (url: string): void => { window.open(url, '_blank'); }; @@ -181,8 +182,8 @@ export default function Support(): JSX.Element { const handleAddCreditCard = (): void => { logEvent('Add Credit card modal: Clicked', { - source: `chat`, - page: 'support', + source: `help & support`, + page: pathname, }); updateCreditCard({ @@ -194,6 +195,10 @@ export default function Support(): JSX.Element { const handleChat = (): void => { if (showAddCreditCardModal) { + logEvent('Disabled Chat Support: Clicked', { + source: `help & support`, + page: pathname, + }); setIsAddCreditCardModalOpen(true); } else if (window.Intercom) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment From ab4a8dfbea7d7d759ccbe7aa117b907d54b054f8 Mon Sep 17 00:00:00 2001 From: Vibhu Pandey Date: Wed, 21 Aug 2024 14:18:44 +0530 Subject: [PATCH 16/60] feat(packages): add first dedicated confmap, config, version and instrumentation packages (#5727) ### Summary A config package based on https://github.com/open-telemetry/opentelemetry-collector/blob/main/confmap/confmap.go for signoz. #### Related Issues / PR's This is a part of https://github.com/SigNoz/signoz/pull/5710 --- go.mod | 97 ++++---- go.sum | 220 ++++++++++-------- pkg/config/config.go | 31 +++ pkg/config/config_test.go | 54 +++++ pkg/config/doc.go | 4 + pkg/config/provider.go | 52 +++++ pkg/config/unmarshaler.go | 58 +++++ pkg/config/unmarshaler_test.go | 34 +++ pkg/confmap/config.go | 9 + pkg/confmap/doc.go | 3 + .../provider/signozenvprovider/provider.go | 94 ++++++++ .../signozenvprovider/provider_test.go | 40 ++++ pkg/instrumentation/config.go | 62 +++++ pkg/instrumentation/doc.go | 5 + pkg/instrumentation/instrumentation.go | 95 ++++++++ pkg/instrumentation/logger.go | 45 ++++ pkg/instrumentation/meter.go | 30 +++ pkg/instrumentation/tracer.go | 30 +++ .../inmemoryexporter/config_test.go | 2 +- .../inmemoryexporter/exporter_test.go | 5 +- .../inmemoryreceiver/config_test.go | 2 +- .../inmemoryreceiver/receiver_test.go | 6 +- pkg/version/doc.go | 4 + pkg/version/version.go | 9 + 24 files changed, 833 insertions(+), 158 deletions(-) create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/doc.go create mode 100644 pkg/config/provider.go create mode 100644 pkg/config/unmarshaler.go create mode 100644 pkg/config/unmarshaler_test.go create mode 100644 pkg/confmap/config.go create mode 100644 pkg/confmap/doc.go create mode 100644 pkg/confmap/provider/signozenvprovider/provider.go create mode 100644 pkg/confmap/provider/signozenvprovider/provider_test.go create mode 100644 pkg/instrumentation/config.go create mode 100644 pkg/instrumentation/doc.go create mode 100644 pkg/instrumentation/instrumentation.go create mode 100644 pkg/instrumentation/logger.go create mode 100644 pkg/instrumentation/meter.go create mode 100644 pkg/instrumentation/tracer.go create mode 100644 pkg/version/doc.go create mode 100644 pkg/version/version.go diff --git a/go.mod b/go.mod index f2dadeede1..c7cafd89cc 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.1 + github.com/gorilla/websocket v1.5.0 github.com/gosimple/slug v1.10.0 github.com/jmoiron/sqlx v1.3.4 github.com/json-iterator/go v1.1.12 @@ -37,7 +38,7 @@ require ( github.com/opentracing/opentracing-go v1.2.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 - github.com/prometheus/common v0.54.0 + github.com/prometheus/common v0.55.0 github.com/prometheus/prometheus v2.5.0+incompatible github.com/rs/cors v1.11.0 github.com/russellhaering/gosaml2 v0.9.0 @@ -48,21 +49,26 @@ require ( github.com/soheilhy/cmux v0.1.5 github.com/srikanthccv/ClickHouse-go-mock v0.8.0 github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/collector/component v0.102.1 - go.opentelemetry.io/collector/confmap v0.102.1 - go.opentelemetry.io/collector/confmap/converter/expandconverter v0.102.0 - go.opentelemetry.io/collector/confmap/provider/fileprovider v0.102.0 - go.opentelemetry.io/collector/connector v0.102.0 - go.opentelemetry.io/collector/consumer v0.102.1 - go.opentelemetry.io/collector/exporter v0.102.0 - go.opentelemetry.io/collector/extension v0.102.1 - go.opentelemetry.io/collector/otelcol v0.102.0 - go.opentelemetry.io/collector/pdata v1.9.0 - go.opentelemetry.io/collector/processor v0.102.0 - go.opentelemetry.io/collector/receiver v0.102.0 - go.opentelemetry.io/collector/service v0.102.0 - go.opentelemetry.io/otel v1.27.0 - go.opentelemetry.io/otel/sdk v1.27.0 + go.opentelemetry.io/collector/component v0.103.0 + go.opentelemetry.io/collector/confmap v0.103.0 + go.opentelemetry.io/collector/confmap/converter/expandconverter v0.103.0 + go.opentelemetry.io/collector/confmap/provider/fileprovider v0.103.0 + go.opentelemetry.io/collector/connector v0.103.0 + go.opentelemetry.io/collector/consumer v0.103.0 + go.opentelemetry.io/collector/exporter v0.103.0 + go.opentelemetry.io/collector/extension v0.103.0 + go.opentelemetry.io/collector/otelcol v0.103.0 + go.opentelemetry.io/collector/pdata v1.10.0 + go.opentelemetry.io/collector/processor v0.103.0 + go.opentelemetry.io/collector/receiver v0.103.0 + go.opentelemetry.io/collector/service v0.103.0 + go.opentelemetry.io/contrib/bridges/otelzap v0.0.0-20240820072021-3fab5f5f20fb + go.opentelemetry.io/contrib/config v0.8.0 + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/log v0.4.0 + go.opentelemetry.io/otel/metric v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.24.0 @@ -70,8 +76,8 @@ require ( golang.org/x/net v0.26.0 golang.org/x/oauth2 v0.21.0 golang.org/x/text v0.16.0 - google.golang.org/grpc v1.64.1 - google.golang.org/protobuf v1.34.1 + google.golang.org/grpc v1.65.0 + google.golang.org/protobuf v1.34.2 gopkg.in/segmentio/analytics-go.v3 v3.1.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -104,16 +110,15 @@ require ( github.com/go-faster/errors v0.7.1 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect - github.com/gorilla/websocket v1.5.0 // indirect github.com/gosimple/unidecode v1.0.0 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect @@ -139,6 +144,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/oklog/run v1.1.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.102.0 // indirect @@ -150,11 +156,11 @@ require ( github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect - github.com/prometheus/procfs v0.15.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/backo-go v1.0.1 // indirect - github.com/shirou/gopsutil/v3 v3.24.4 // indirect + github.com/shirou/gopsutil/v4 v4.24.5 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -168,36 +174,35 @@ require ( github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/collector v0.102.1 // indirect - go.opentelemetry.io/collector/config/configtelemetry v0.102.1 // indirect - go.opentelemetry.io/collector/confmap/provider/envprovider v0.102.0 // indirect - go.opentelemetry.io/collector/confmap/provider/httpprovider v0.102.0 // indirect - go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.102.0 // indirect - go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.102.0 // indirect - go.opentelemetry.io/collector/featuregate v1.9.0 // indirect - go.opentelemetry.io/collector/semconv v0.102.0 // indirect - go.opentelemetry.io/contrib/config v0.7.0 // indirect + go.opentelemetry.io/collector v0.103.0 // indirect + go.opentelemetry.io/collector/config/configtelemetry v0.103.0 // indirect + go.opentelemetry.io/collector/confmap/provider/envprovider v0.103.0 // indirect + go.opentelemetry.io/collector/confmap/provider/httpprovider v0.103.0 // indirect + go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.103.0 // indirect + go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.103.0 // indirect + go.opentelemetry.io/collector/featuregate v1.13.0 // indirect + go.opentelemetry.io/collector/semconv v0.103.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.27.0 // indirect go.opentelemetry.io/otel/bridge/opencensus v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.49.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.27.0 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect - go.opentelemetry.io/proto/otlp v1.2.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.50.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.4.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect gonum.org/v1/gonum v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect gopkg.in/ini.v1 v1.67.0 // indirect k8s.io/client-go v0.29.3 // indirect k8s.io/klog/v2 v2.120.1 // indirect diff --git a/go.sum b/go.sum index b1eeff2873..a98137d6db 100644 --- a/go.sum +++ b/go.sum @@ -131,8 +131,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= -github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= +github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -216,8 +216,8 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= @@ -240,8 +240,8 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -486,7 +486,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linode/linodego v1.35.0 h1:rIhUeCHBLEDlkoRnOTwzSGzljQ3ksXwLxacmXnrV+Do= github.com/linode/linodego v1.35.0/go.mod h1:JxuhOEAMfSxun6RU5/MgTKH2GGTmFrhKRj3wL1NFin0= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY= github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -624,7 +623,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI= github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -644,16 +642,16 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= -github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek= -github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= @@ -685,8 +683,8 @@ github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N+ github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI= github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= -github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU= -github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8= +github.com/shirou/gopsutil/v4 v4.24.5 h1:gGsArG5K6vmsh5hcFOHaPm87UD003CaDMkAOweSQjhM= +github.com/shirou/gopsutil/v4 v4.24.5/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= @@ -734,7 +732,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -774,92 +771,112 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/collector v0.102.1 h1:M/ciCcReQsSDYG9bJ2Qwqk7pQILDJ2bM/l0MdeCAvJE= -go.opentelemetry.io/collector v0.102.1/go.mod h1:yF1lDRgL/Eksb4/LUnkMjvLvHHpi6wqBVlzp+dACnPM= -go.opentelemetry.io/collector/component v0.102.1 h1:66z+LN5dVCXhvuVKD1b56/3cYLK+mtYSLIwlskYA9IQ= -go.opentelemetry.io/collector/component v0.102.1/go.mod h1:XfkiSeImKYaewT2DavA80l0VZ3JjvGndZ8ayPXfp8d0= -go.opentelemetry.io/collector/config/confignet v0.102.1 h1:nSiAFQMzNCO4sDBztUxY73qFw4Vh0hVePq8+3wXUHtU= -go.opentelemetry.io/collector/config/confignet v0.102.1/go.mod h1:pfOrCTfSZEB6H2rKtx41/3RN4dKs+X2EKQbw3MGRh0E= -go.opentelemetry.io/collector/config/configtelemetry v0.102.1 h1:f/CYcrOkaHd+COIJ2lWnEgBCHfhEycpbow4ZhrGwAlA= -go.opentelemetry.io/collector/config/configtelemetry v0.102.1/go.mod h1:WxWKNVAQJg/Io1nA3xLgn/DWLE/W1QOB2+/Js3ACi40= -go.opentelemetry.io/collector/confmap v0.102.1 h1:wZuH+d/P11Suz8wbp+xQCJ0BPE9m5pybtUe74c+rU7E= -go.opentelemetry.io/collector/confmap v0.102.1/go.mod h1:KgpS7UxH5rkd69CzAzlY2I1heH8Z7eNCZlHmwQBMxNg= -go.opentelemetry.io/collector/confmap/converter/expandconverter v0.102.0 h1:8Ne/oL6M4kMWK0P3FKV9EduQa+1UOGyVAnFHfSo4c1A= -go.opentelemetry.io/collector/confmap/converter/expandconverter v0.102.0/go.mod h1:Xj4Ld/RriP/Bj+5oPpaYJsLNs2wWRDN2TvzX3Lbi6+M= -go.opentelemetry.io/collector/confmap/provider/envprovider v0.102.0 h1:o1iKqN+oM+TZqHoGdnKw1Am2BQlIGCYbxCRzU8T3jbM= -go.opentelemetry.io/collector/confmap/provider/envprovider v0.102.0/go.mod h1:JpCemLtL/sXQ2Rk3Bx7OPPA7Qt/9NVH91q0bR655gSo= -go.opentelemetry.io/collector/confmap/provider/fileprovider v0.102.0 h1:SfASE6lxXjrmYj/UibcWdOiFWvRG0zt4hJgenloEhlY= -go.opentelemetry.io/collector/confmap/provider/fileprovider v0.102.0/go.mod h1:+Ku0Fvdb5f6e9UkfqJXAV5FaUJVxxg6Ykfx7Js8y+V4= -go.opentelemetry.io/collector/confmap/provider/httpprovider v0.102.0 h1:GwJQTXs7pYPUv/fVf+0nBgsJdlrTuY/PfwQ/TRA/sIk= -go.opentelemetry.io/collector/confmap/provider/httpprovider v0.102.0/go.mod h1:PGE3DcRgqYWWC2cq2hYZoET1d3Q8JZyPNmgvqXPFWEU= -go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.102.0 h1:zdYZLiHHtDf4Kk9WU7mW9dW6WAXtBF54I5jmTMRJtiw= -go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.102.0/go.mod h1:yFsgUM0PbUJkPlbpJfOG6da+YiF0Z80tv7YcnL3qwv4= -go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.102.0 h1:Y4H+GaCQl2URp9mEJMV5CYOhw+erONqNyvtFoKQfIoA= -go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.102.0/go.mod h1:g1RjfVD0gHAf/mPOIs3zBoKBeuDsN+rc5x0lZtgA8tI= -go.opentelemetry.io/collector/connector v0.102.0 h1:IvAsVfYRxP0ajmKbUovF8qugkcUtHq6RuYNtjcMa63E= -go.opentelemetry.io/collector/connector v0.102.0/go.mod h1:f4M7wZ/9+XtgTE0fivBFH3WlwntaEd0qFFA0giFkdnY= -go.opentelemetry.io/collector/consumer v0.102.1 h1:0CkgHhxwx4lI/m+hWjh607xyjooW5CObZ8hFQy5vvo0= -go.opentelemetry.io/collector/consumer v0.102.1/go.mod h1:HoXqmrRV13jLnP3/Gg3fYNdRkDPoO7UW58hKiLyFF60= -go.opentelemetry.io/collector/exporter v0.102.0 h1:hvyTyyGVx5FIikA6HzlTeZHILJ62hrIBsoZCoKlpX3A= -go.opentelemetry.io/collector/exporter v0.102.0/go.mod h1:JWE+1qNoSVBSelzhI3Iao/VkYVssY+sXaTPK1JOmpQ0= -go.opentelemetry.io/collector/extension v0.102.1 h1:gAvE3w15q+Vv0Tj100jzcDpeMTyc8dAiemHRtJbspLg= -go.opentelemetry.io/collector/extension v0.102.1/go.mod h1:XBxUOXjZpwYLZYOK5u3GWlbBTOKmzStY5eU1R/aXkIo= -go.opentelemetry.io/collector/extension/zpagesextension v0.102.0 h1:BPq98py8nwzaV7KAsxt4ZZAF9LiSRu7ZjHNGavFNyKo= -go.opentelemetry.io/collector/extension/zpagesextension v0.102.0/go.mod h1:P86HW3x3epDS5F4yP0gAvsZiw4xxP1OupTEx2o6UqjY= -go.opentelemetry.io/collector/featuregate v1.9.0 h1:mC4/HnR5cx/kkG1RKOQAvHxxg5Ktmd9gpFdttPEXQtA= -go.opentelemetry.io/collector/featuregate v1.9.0/go.mod h1:PsOINaGgTiFc+Tzu2K/X2jP+Ngmlp7YKGV1XrnBkH7U= -go.opentelemetry.io/collector/otelcol v0.102.0 h1:HuE+ok4iUjOrmYhQBSWpG5kBTVhcA24ljPL4pBERZ5E= -go.opentelemetry.io/collector/otelcol v0.102.0/go.mod h1:w8pCRu2nM/jAkLlEAS6ccKtJv5ylUQe6Ugl98zzTfyE= -go.opentelemetry.io/collector/pdata v1.9.0 h1:qyXe3HEVYYxerIYu0rzgo1Tx2d1Zs6iF+TCckbHLFOw= -go.opentelemetry.io/collector/pdata v1.9.0/go.mod h1:vk7LrfpyVpGZrRWcpjyy0DDZzL3SZiYMQxfap25551w= -go.opentelemetry.io/collector/pdata/testdata v0.102.1 h1:S3idZaJxy8M7mCC4PG4EegmtiSaOuh6wXWatKIui8xU= -go.opentelemetry.io/collector/pdata/testdata v0.102.1/go.mod h1:JEoSJTMgeTKyGxoMRy48RMYyhkA5vCCq/abJq9B6vXs= -go.opentelemetry.io/collector/processor v0.102.0 h1:JsjTlpBRmoSYxcu3cAbKBchOmL6aNUxLa03ZkWIqZr8= -go.opentelemetry.io/collector/processor v0.102.0/go.mod h1:IaCSDcfy75uQTaOM+LgR1bMf/bUw2eFfzn20uvWYfLQ= -go.opentelemetry.io/collector/receiver v0.102.0 h1:8rHNjWjV90bL0dgvKVc/7D10NCbM7bXCiqpcLRz5jBI= -go.opentelemetry.io/collector/receiver v0.102.0/go.mod h1:bYDwYItMrj7Drx0Pn4wZQ8Ii67lp9Nta62gbau93FhA= -go.opentelemetry.io/collector/semconv v0.102.0 h1:VEOdog9IbSfaGR7yg4AVmT54MwHAgH9lzITH6C33uyc= -go.opentelemetry.io/collector/semconv v0.102.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZTWsLf5YGZ7qwKulIl5hw= -go.opentelemetry.io/collector/service v0.102.0 h1:B5nfyQZF7eB/y+yucl9G/7VsusbXixYXWingXn7VszM= -go.opentelemetry.io/collector/service v0.102.0/go.mod h1:c+0n0DfQeCjgrdplNHYwYbG/5aupTZVYU/50nMQraoc= -go.opentelemetry.io/contrib/config v0.7.0 h1:b1rK5tGTuhhPirJiMxOcyQfZs76j2VapY6ODn3b2Dbs= -go.opentelemetry.io/contrib/config v0.7.0/go.mod h1:8tdiFd8N5etOi3XzBmAoMxplEzI3TcL8dU5rM5/xcOQ= +go.opentelemetry.io/collector v0.103.0 h1:mssWo1y31p1F/SRsSBnVUX6YocgawCqM1blpE+hkWog= +go.opentelemetry.io/collector v0.103.0/go.mod h1:mgqdTFB7QCYiOeEdJSSEktovPqy+2fw4oTKJzyeSB0U= +go.opentelemetry.io/collector/component v0.103.0 h1:j52YAsp8EmqYUotVUwhovkqFZGuxArEkk65V4TI46NE= +go.opentelemetry.io/collector/component v0.103.0/go.mod h1:jKs19tGtCO8Hr5/YM0F+PoFcl8SVe/p4Ge30R6srkbc= +go.opentelemetry.io/collector/config/configauth v0.103.0 h1:tv2Ilj0X9T8ZsDd4mB8Sl+nXQ8CG8MJVQ1Lo4mmE0Pk= +go.opentelemetry.io/collector/config/configauth v0.103.0/go.mod h1:VIo8DpFeyOOCMUVoQsBdq3t2snUiBBECP0UxW1bwz/o= +go.opentelemetry.io/collector/config/configcompression v1.10.0 h1:ClkAY1rzaxFawmC53BUf3TjTWKOGx+2xnpqOJIkg6Tk= +go.opentelemetry.io/collector/config/configcompression v1.10.0/go.mod h1:6+m0GKCv7JKzaumn7u80A2dLNCuYf5wdR87HWreoBO0= +go.opentelemetry.io/collector/config/confighttp v0.103.0 h1:tgCWMKuIorSr4+iQOv0A8Ya/8do73hiG5KHinWaz63Q= +go.opentelemetry.io/collector/config/confighttp v0.103.0/go.mod h1:xMXoLsTGTJlftu+VAL3iadEs4gkmqFrvuPPnpNi6ETo= +go.opentelemetry.io/collector/config/configopaque v1.10.0 h1:FAxj6ggLpJE/kFnR1ezYwjRdo6gHo2+CjlIsHVCFVnQ= +go.opentelemetry.io/collector/config/configopaque v1.10.0/go.mod h1:0xURn2sOy5j4fbaocpEYfM97HPGsiffkkVudSPyTJlM= +go.opentelemetry.io/collector/config/configtelemetry v0.103.0 h1:KLbhkFqdw9D31t0IhJ/rnhMRvz/s14eie0fKfm5xWns= +go.opentelemetry.io/collector/config/configtelemetry v0.103.0/go.mod h1:WxWKNVAQJg/Io1nA3xLgn/DWLE/W1QOB2+/Js3ACi40= +go.opentelemetry.io/collector/config/configtls v0.103.0 h1:nbk8sJIHoYYQbpZtUkUQceTbjC4wEjoePKJ15v8cCcU= +go.opentelemetry.io/collector/config/configtls v0.103.0/go.mod h1:046dfdfHW8wWCMhzUaWJo7guRiCoSz5QzVjCSDzymdU= +go.opentelemetry.io/collector/config/internal v0.103.0 h1:pimS3uLHfOBbConZrviGoTwu+bkTNDoQBtbeWCg8U8k= +go.opentelemetry.io/collector/config/internal v0.103.0/go.mod h1:kJRkB+PgamWqPi/GWbYWvnRzVzS1rwDUh6+VSz4C7NQ= +go.opentelemetry.io/collector/confmap v0.103.0 h1:qKKZyWzropSKfgtGv12JzADOXNgThqH1Vx6qzblBE24= +go.opentelemetry.io/collector/confmap v0.103.0/go.mod h1:TlOmqe/Km3K6WgxyhEAdCb/V1Yp6eSU76fCoiluEa88= +go.opentelemetry.io/collector/confmap/converter/expandconverter v0.103.0 h1:zApcKLSosuu9I/4IRHTqlE1H6XNiZNAgd26YbzHwkto= +go.opentelemetry.io/collector/confmap/converter/expandconverter v0.103.0/go.mod h1:hoel+3CPjRhPSHzCrE1E+wCyoSLHlgW7662Ntwx2ujM= +go.opentelemetry.io/collector/confmap/provider/envprovider v0.103.0 h1:0XHQ/ffxSUx3sMbnYSf8a4jnVYLUrxo+/XwdhXkizgs= +go.opentelemetry.io/collector/confmap/provider/envprovider v0.103.0/go.mod h1:NiE4Fe42Sew1TyXuU1YEd0xZBDNI+w6IRkC2OTlJUak= +go.opentelemetry.io/collector/confmap/provider/fileprovider v0.103.0 h1:5dB2G7d6RKmWS8ptuAWvAEKGYODk2DTRm84bU9HooLQ= +go.opentelemetry.io/collector/confmap/provider/fileprovider v0.103.0/go.mod h1:GT/GBk17lDhc27762w6PNHvKYbA+TnHvNEyQHUsjKpY= +go.opentelemetry.io/collector/confmap/provider/httpprovider v0.103.0 h1:Hrp+nw4W9/jeJfi3GfJW6EYh7DeNkaC1wojOh4x8CbI= +go.opentelemetry.io/collector/confmap/provider/httpprovider v0.103.0/go.mod h1:kUst0pGVBlKDSlvJYDclrsApbkMv7ahRDh6/pE4LsBc= +go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.103.0 h1:JUDRYsMOhkIBxZqZli0BU+64zahIUgnEPZSe9wo2T0Q= +go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.103.0/go.mod h1:+mUrWjpdGIdSKMeeplLO+qXFSBc287as2oIPVdKMTxc= +go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.103.0 h1:boTv+ZRkn1h5eUbt5sLSU5lCrItCCxCen/laRmsHLyg= +go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.103.0/go.mod h1:0pZ7RD7SPg+yklgGPN+74Zzbps4R9x5bRPZX1D1gtGM= +go.opentelemetry.io/collector/connector v0.103.0 h1:jwmrgCT6ftz3U4o8mAqP+/yaQ5KsLMFXo2+OHXhy+tE= +go.opentelemetry.io/collector/connector v0.103.0/go.mod h1:6RDaeDMiXTKEXSy1eIaO0EiM+/91NVHdBxOc9e2++2A= +go.opentelemetry.io/collector/consumer v0.103.0 h1:L/7SA/U2ua5L4yTLChnI9I+IFGKYU5ufNQ76QKYcPYs= +go.opentelemetry.io/collector/consumer v0.103.0/go.mod h1:7jdYb9kSSOsu2R618VRX0VJ+Jt3OrDvvUsDToHTEOLI= +go.opentelemetry.io/collector/exporter v0.103.0 h1:g0nF/FAwuA7tTJf5uo1PvlQl7xFqCgvfH+FYqufBSiw= +go.opentelemetry.io/collector/exporter v0.103.0/go.mod h1:PC2OvciPEew2kaA/ZMyxRqfeOW8Wgi0CYR614PEyS/w= +go.opentelemetry.io/collector/extension v0.103.0 h1:vTsd+GElvT7qKk9Y9d6UKuuT2Ngx0mai8Q48hkKQMwM= +go.opentelemetry.io/collector/extension v0.103.0/go.mod h1:rp2l3xskNKWv0yBCyU69Pv34TnP1QVD1ijr0zSndnsM= +go.opentelemetry.io/collector/extension/auth v0.103.0 h1:i7cQl+Ewpve/DIN4rFMg1GiyUPE14LZsYWrJ1RqtP84= +go.opentelemetry.io/collector/extension/auth v0.103.0/go.mod h1:JdYBS/EkPAz2APAi8g7xTiSRlZTc7c4H82AQM9epzxw= +go.opentelemetry.io/collector/extension/zpagesextension v0.103.0 h1:jgSEQY++zOI6hFQygwuvS6ulJ/Yu4xXgUg+Ijoxx51I= +go.opentelemetry.io/collector/extension/zpagesextension v0.103.0/go.mod h1:2OUi0Hp+3zPUJmi7goJ6d1/kGgFAw3SDESRX7xQ0QHE= +go.opentelemetry.io/collector/featuregate v1.13.0 h1:rc84eCf5hesXQ8/bP6Zc15wqthbomfLBHmox5tT7AwM= +go.opentelemetry.io/collector/featuregate v1.13.0/go.mod h1:PsOINaGgTiFc+Tzu2K/X2jP+Ngmlp7YKGV1XrnBkH7U= +go.opentelemetry.io/collector/otelcol v0.103.0 h1:Skqnc2mxDdk3eiYioUuG7ST6ur5k83SOv7mIBt60fBw= +go.opentelemetry.io/collector/otelcol v0.103.0/go.mod h1:iJF3ghCv+nRZI6+hI7z3kGRZrgH///Fd9tNXY82X90g= +go.opentelemetry.io/collector/pdata v1.10.0 h1:oLyPLGvPTQrcRT64ZVruwvmH/u3SHTfNo01pteS4WOE= +go.opentelemetry.io/collector/pdata v1.10.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE= +go.opentelemetry.io/collector/pdata/testdata v0.103.0 h1:iI6NOE0L2je/bxlWzAWHQ/yCtnGupgv42Hl9Al1q/g4= +go.opentelemetry.io/collector/pdata/testdata v0.103.0/go.mod h1:tLzRhb/h37/9wFRQVr+CxjKi5qmhSRpCAiOlhwRkeEk= +go.opentelemetry.io/collector/processor v0.103.0 h1:YZ+LRuHKtOam7SCeLkJAP6bS1d6XxeYP22OyMN3VP0s= +go.opentelemetry.io/collector/processor v0.103.0/go.mod h1:/mxyh0NpJgpZycm7iHDpM7i5PdtWvKKdCZf0cyADJfU= +go.opentelemetry.io/collector/receiver v0.103.0 h1:V3JBKkX+7e/NYpDDZVyeu2VQB1/lLFuoJFPfupdCcZs= +go.opentelemetry.io/collector/receiver v0.103.0/go.mod h1:Yybv4ynKFdMOYViWWPMmjkugR89FSQN0P37wP6mX6qM= +go.opentelemetry.io/collector/semconv v0.103.0 h1:5tlVoZlo9USHAU2Bz4YrEste0Vm5AMufXkYJhAVve1Q= +go.opentelemetry.io/collector/semconv v0.103.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZTWsLf5YGZ7qwKulIl5hw= +go.opentelemetry.io/collector/service v0.103.0 h1:e4Eri4jo+YOuEK0+/JE9SUdT/NZaJ2jz/ROJlmLn96s= +go.opentelemetry.io/collector/service v0.103.0/go.mod h1:p1mlniiC1MuPN5FANYJYgf5V5CGFP0hNqWfI8t7Aw8M= +go.opentelemetry.io/contrib/bridges/otelzap v0.0.0-20240820072021-3fab5f5f20fb h1:ZqncifxU0B1q64FRbhKxsJugRsrEToakmYUsgQ5tGbY= +go.opentelemetry.io/contrib/bridges/otelzap v0.0.0-20240820072021-3fab5f5f20fb/go.mod h1:mzv0k5dTnSUE5/ZerXUwGiNKzcPJTakuCh6Wm1emNvU= +go.opentelemetry.io/contrib/config v0.8.0 h1:OD7aDMhL+2EpzdSHfkDmcdD/uUA+PgKM5faFyF9XFT0= +go.opentelemetry.io/contrib/config v0.8.0/go.mod h1:dGeVZWE//3wrxYHHP0iCBYJU1QmOmPcbV+FNB7pjDYI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= go.opentelemetry.io/contrib/propagators/b3 v1.27.0 h1:IjgxbomVrV9za6bRi8fWCNXENs0co37SZedQilP2hm0= go.opentelemetry.io/contrib/propagators/b3 v1.27.0/go.mod h1:Dv9obQz25lCisDvvs4dy28UPh974CxkahRDUPsY7y9E= go.opentelemetry.io/contrib/zpages v0.52.0 h1:MPgkMy0Cp3O5EdfVXP0ss3ujhEibysTM4eszx7E7d+E= go.opentelemetry.io/contrib/zpages v0.52.0/go.mod h1:fqG5AFdoYru3A3DnhibVuaaEfQV2WKxE7fYE1jgDRwk= -go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= -go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel/bridge/opencensus v1.27.0 h1:ao9aGGHd+G4YfjBpGs6vbkvt5hoC67STlJA9fCnOAcs= go.opentelemetry.io/otel/bridge/opencensus v1.27.0/go.mod h1:uRvWtAAXzyVOST0WMPX5JHGBaAvBws+2F8PcC5gMnTk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 h1:bFgvUr3/O4PHj3VQcFEuYKvRZJX1SJDQ+11JXuSB3/w= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0/go.mod h1:xJntEd2KL6Qdg5lwp97HMLQDVeAhrYxmzFseAMDPQ8I= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 h1:CIHWikMsN3wO+wq1Tp5VGdVRTcON+DmOJSfDjXypKOc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0/go.mod h1:TNupZ6cxqyFEpLXAZW7On+mLFL0/g0TE3unIYL91xWc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= -go.opentelemetry.io/otel/exporters/prometheus v0.49.0 h1:Er5I1g/YhfYv9Affk9nJLfH/+qCCVVg1f2R9AbJfqDQ= -go.opentelemetry.io/otel/exporters/prometheus v0.49.0/go.mod h1:KfQ1wpjf3zsHjzP149P4LyAwWRupc6c7t1ZJ9eXpKQM= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 h1:/jlt1Y8gXWiHG9FBx6cJaIC5hYx5Fe64nC8w5Cylt/0= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0/go.mod h1:bmToOGOBZ4hA9ghphIc1PAf66VA8KOtsuy3+ScStG20= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 h1:/0YaXu3755A/cFbtXp+21lkXgI0QE5avTWA2HjU9/WE= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0/go.mod h1:m7SFxp0/7IxmJPLIY3JhOcU9CoFzDaCPL6xxQIxhA+o= -go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= -go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= -go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= -go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= -go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= -go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= -go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= -go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0 h1:zBPZAISA9NOc5cE8zydqDiS0itvg/P/0Hn9m72a5gvM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.4.0/go.mod h1:gcj2fFjEsqpV3fXuzAA+0Ze1p2/4MJ4T7d77AmkvueQ= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 h1:aLmmtjRke7LPDQ3lvpFz+kNEH43faFhzW7v8BFIEydg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0/go.mod h1:TC1pyCt6G9Sjb4bQpShH+P5R53pO6ZuGnHuuln9xMeE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= +go.opentelemetry.io/otel/exporters/prometheus v0.50.0 h1:2Ewsda6hejmbhGFyUvWZjUThC98Cf8Zy6g0zkIimOng= +go.opentelemetry.io/otel/exporters/prometheus v0.50.0/go.mod h1:pMm5PkUo5YwbLiuEf7t2xg4wbP0/eSJrMxIMxKosynY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 h1:BJee2iLkfRfl9lc7aFmBwkWxY/RI1RDdXepSF6y8TPE= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0/go.mod h1:DIzlHs3DRscCIBU3Y9YSzPfScwnYnzfnCd4g8zA7bZc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 h1:EVSnY9JbEEW92bEkIYOVMw4q1WJxIAGoFTrtYOzWuRQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0/go.mod h1:Ea1N1QQryNXpCD0I1fdLibBAIpQuBkznMmkdKrapk1Y= +go.opentelemetry.io/otel/log v0.4.0 h1:/vZ+3Utqh18e8TPjuc3ecg284078KWrR8BRz+PQAj3o= +go.opentelemetry.io/otel/log v0.4.0/go.mod h1:DhGnQvky7pHy82MIRV43iXh3FlKN8UUKftn0KbLOq6I= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/sdk/log v0.4.0 h1:1mMI22L82zLqf6KtkjrRy5BbagOTWdJsqMY/HSqILAA= +go.opentelemetry.io/otel/sdk/log v0.4.0/go.mod h1:AYJ9FVF0hNOgAVzUG/ybg/QttnXhUePWAupmCqtdESo= +go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= +go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -1046,7 +1063,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1176,10 +1192,10 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1197,8 +1213,8 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1212,8 +1228,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000000..621cc073c6 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,31 @@ +package config + +import ( + "context" + + "go.signoz.io/signoz/pkg/instrumentation" +) + +// Config defines the entire configuration of signoz. +type Config struct { + Instrumentation instrumentation.Config `mapstructure:"instrumentation"` +} + +func New(ctx context.Context, settings ProviderSettings) (*Config, error) { + provider, err := NewProvider(settings) + if err != nil { + return nil, err + } + + return provider.Get(ctx) +} + +func byName(name string) (any, bool) { + switch name { + case "instrumentation": + return &instrumentation.Config{}, true + default: + return nil, false + } + +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000000..04ede418f0 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,54 @@ +package config + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/confmap" + contribsdkconfig "go.opentelemetry.io/contrib/config" + "go.signoz.io/signoz/pkg/confmap/provider/signozenvprovider" + "go.signoz.io/signoz/pkg/instrumentation" +) + +func TestNewWithSignozEnvProvider(t *testing.T) { + t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__ENABLED", "true") + t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__PROCESSORS__BATCH__EXPORTER__OTLP__ENDPOINT", "0.0.0.0:4317") + t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__PROCESSORS__BATCH__EXPORT_TIMEOUT", "10") + + config, err := New(context.Background(), ProviderSettings{ + ResolverSettings: confmap.ResolverSettings{ + URIs: []string{"signozenv:"}, + ProviderFactories: []confmap.ProviderFactory{ + signozenvprovider.NewFactory(), + }, + }, + }) + require.NoError(t, err) + + i := 10 + expected := &Config{ + Instrumentation: instrumentation.Config{ + Logs: instrumentation.LogsConfig{ + Enabled: true, + LoggerProvider: contribsdkconfig.LoggerProvider{ + Processors: []contribsdkconfig.LogRecordProcessor{ + contribsdkconfig.LogRecordProcessor{ + Batch: &contribsdkconfig.BatchLogRecordProcessor{ + ExportTimeout: &i, + Exporter: contribsdkconfig.LogRecordExporter{ + OTLP: &contribsdkconfig.OTLP{ + Endpoint: "0.0.0.0:4317", + }, + }, + }, + }, + }, + }, + }, + }, + } + + assert.Equal(t, expected, config) +} diff --git a/pkg/config/doc.go b/pkg/config/doc.go new file mode 100644 index 0000000000..324bd656fe --- /dev/null +++ b/pkg/config/doc.go @@ -0,0 +1,4 @@ +// Package config provides the configuration management for the Signoz application. +// It includes functionality to define, load, and validate the application's configuration +// using various providers and formats. +package config diff --git a/pkg/config/provider.go b/pkg/config/provider.go new file mode 100644 index 0000000000..c0164d1d03 --- /dev/null +++ b/pkg/config/provider.go @@ -0,0 +1,52 @@ +package config + +import ( + "context" + "fmt" + + "go.opentelemetry.io/collector/confmap" +) + +// Provides the configuration for signoz. +type Provider interface { + // Get returns the configuration, or error otherwise. + Get(ctx context.Context) (*Config, error) +} + +type provider struct { + resolver *confmap.Resolver +} + +// ProviderSettings are the settings to configure the behavior of the Provider. +type ProviderSettings struct { + // ResolverSettings are the settings to configure the behavior of the confmap.Resolver. + ResolverSettings confmap.ResolverSettings +} + +// NewProvider returns a new Provider that provides the entire configuration. +// See https://github.com/open-telemetry/opentelemetry-collector/blob/main/otelcol/configprovider.go for +// more details +func NewProvider(settings ProviderSettings) (Provider, error) { + resolver, err := confmap.NewResolver(settings.ResolverSettings) + if err != nil { + return nil, err + } + + return &provider{ + resolver: resolver, + }, nil +} + +func (provider *provider) Get(ctx context.Context) (*Config, error) { + conf, err := provider.resolver.Resolve(ctx) + if err != nil { + return nil, fmt.Errorf("cannot resolve configuration: %w", err) + } + + config, err := unmarshal(conf) + if err != nil { + return nil, fmt.Errorf("cannot unmarshal configuration: %w", err) + } + + return config, nil +} diff --git a/pkg/config/unmarshaler.go b/pkg/config/unmarshaler.go new file mode 100644 index 0000000000..8392f83462 --- /dev/null +++ b/pkg/config/unmarshaler.go @@ -0,0 +1,58 @@ +package config + +import ( + "fmt" + + "go.opentelemetry.io/collector/confmap" + signozconfmap "go.signoz.io/signoz/pkg/confmap" +) + +// unmarshal converts a confmap.Conf into a Config struct. +// It splits the input confmap into a map of key-value pairs, fetches the corresponding +// signozconfmap.Config interface by name, merges it with the default config, validates it, +// and then creates a new confmap from the parsed map to unmarshal into the Config struct. +func unmarshal(conf *confmap.Conf) (*Config, error) { + raw := make(map[string]any) + if err := conf.Unmarshal(&raw); err != nil { + return nil, err + } + + parsed := make(map[string]any) + + for k := range raw { + e, ok := byName(k) + if !ok { + return nil, fmt.Errorf("cannot find config with name %q", k) + } + i, ok := e.(signozconfmap.Config) + if !ok { + return nil, fmt.Errorf("config %q does not implement \"signozconfmap.Config\"", k) + } + + sub, err := conf.Sub(k) + if err != nil { + return nil, fmt.Errorf("cannot read config for %q: %w", k, err) + } + + d := i.NewWithDefaults() + if err := sub.Unmarshal(&d); err != nil { + return nil, fmt.Errorf("cannot merge config for %q: %w", k, err) + } + + err = d.Validate() + if err != nil { + return nil, fmt.Errorf("failed to validate config for for %q: %w", k, err) + } + + parsed[k] = d + } + + parsedConf := confmap.NewFromStringMap(parsed) + config := new(Config) + err := parsedConf.Unmarshal(config) + if err != nil { + return nil, fmt.Errorf("cannot unmarshal config: %w", err) + } + + return config, nil +} diff --git a/pkg/config/unmarshaler_test.go b/pkg/config/unmarshaler_test.go new file mode 100644 index 0000000000..1627bbcf96 --- /dev/null +++ b/pkg/config/unmarshaler_test.go @@ -0,0 +1,34 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/confmap" + "go.signoz.io/signoz/pkg/instrumentation" +) + +func TestUnmarshal(t *testing.T) { + input := confmap.NewFromStringMap( + map[string]any{ + "instrumentation": map[string]any{ + "logs": map[string]bool{ + "enabled": true, + }, + }, + }, + ) + expected := &Config{ + Instrumentation: instrumentation.Config{ + Logs: instrumentation.LogsConfig{ + Enabled: true, + }, + }, + } + cfg, err := unmarshal(input) + require.NoError(t, err) + + assert.Equal(t, expected, cfg) + +} diff --git a/pkg/confmap/config.go b/pkg/confmap/config.go new file mode 100644 index 0000000000..f3425faf44 --- /dev/null +++ b/pkg/confmap/config.go @@ -0,0 +1,9 @@ +package confmap + +// Config is an interface that defines methods for creating and validating configurations. +type Config interface { + // New creates a new instance of the configuration with default values. + NewWithDefaults() Config + // Validate the configuration and returns an error if invalid. + Validate() error +} diff --git a/pkg/confmap/doc.go b/pkg/confmap/doc.go new file mode 100644 index 0000000000..de21ed5aa0 --- /dev/null +++ b/pkg/confmap/doc.go @@ -0,0 +1,3 @@ +// Package confmap is a wrapper on top of the confmap defined here: +// https://github.com/open-telemetry/opentelemetry-collector/blob/main/otelcol/configprovider.go/ +package confmap diff --git a/pkg/confmap/provider/signozenvprovider/provider.go b/pkg/confmap/provider/signozenvprovider/provider.go new file mode 100644 index 0000000000..aee4c9b499 --- /dev/null +++ b/pkg/confmap/provider/signozenvprovider/provider.go @@ -0,0 +1,94 @@ +package signozenvprovider + +import ( + "context" + "fmt" + "os" + "regexp" + "sort" + "strings" + + "go.opentelemetry.io/collector/confmap" + "go.uber.org/zap" + "gopkg.in/yaml.v3" +) + +const ( + schemeName string = "signozenv" + envPrefix string = "signoz" + separator string = "__" + envPrefixWithOneSeparator string = "signoz_" + envRegexString string = `^[a-zA-Z][a-zA-Z0-9_]*$` +) + +var ( + envRegex = regexp.MustCompile(envRegexString) +) + +type provider struct { + logger *zap.Logger +} + +// NewFactory returns a factory for a confmap.Provider that reads the configuration from the environment. +// All variables starting with `SIGNOZ__` are read from the environment. +// The separator is `__` (2 underscores) in order to incorporate env variables having keys with a single `_` +func NewFactory() confmap.ProviderFactory { + return confmap.NewProviderFactory(newProvider) +} + +func newProvider(settings confmap.ProviderSettings) confmap.Provider { + return &provider{ + logger: settings.Logger, + } +} + +func (provider *provider) Retrieve(_ context.Context, uri string, _ confmap.WatcherFunc) (*confmap.Retrieved, error) { + if !strings.HasPrefix(uri, schemeName+":") { + return nil, fmt.Errorf("%q uri is not supported by %q provider", uri, schemeName) + } + + // Read and Sort environment variables for consistent output + envvars := os.Environ() + sort.Strings(envvars) + + // Create a map m containing key value pairs + m := make(map[string]any) + for _, envvar := range envvars { + parts := strings.SplitN(envvar, "=", 2) + if len(parts) != 2 { + continue + } + key := strings.ToLower(parts[0]) + val := parts[1] + + if strings.HasPrefix(key, envPrefixWithOneSeparator) { + // Remove the envPrefix from the key + key = strings.Replace(key, envPrefix+separator, "", 1) + + // Check whether the resulting key matches with the regex + if !envRegex.MatchString(key) { + provider.logger.Warn("Configuration references invalid environment variable key", zap.String("key", key)) + continue + } + + // Convert key into yaml format + key = strings.ToLower(strings.ReplaceAll(key, separator, confmap.KeyDelimiter)) + m[key] = val + } + } + + out, err := yaml.Marshal(m) + if err != nil { + return nil, err + } + + return confmap.NewRetrievedFromYAML(out) +} + +func (*provider) Scheme() string { + return schemeName +} + +func (*provider) Shutdown(context.Context) error { + return nil +} diff --git a/pkg/confmap/provider/signozenvprovider/provider_test.go b/pkg/confmap/provider/signozenvprovider/provider_test.go new file mode 100644 index 0000000000..5a0ef1ea57 --- /dev/null +++ b/pkg/confmap/provider/signozenvprovider/provider_test.go @@ -0,0 +1,40 @@ +package signozenvprovider + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/confmap/confmaptest" +) + +func createProvider() confmap.Provider { + return NewFactory().Create(confmaptest.NewNopProviderSettings()) +} + +func TestValidateProviderScheme(t *testing.T) { + assert.NoError(t, confmaptest.ValidateProviderScheme(createProvider())) +} + +func TestRetrieve(t *testing.T) { + t.Setenv("SIGNOZ__STORAGE__DSN", "localhost:9000") + t.Setenv("SIGNOZ__SIGNOZ_ENABLED", "true") + t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__ENABLED", "true") + expected := confmap.NewFromStringMap(map[string]any{ + "storage::dsn": "localhost:9000", + "signoz_enabled": "true", + "instrumentation::logs::enabled": "true", + }) + + signoz := createProvider() + retrieved, err := signoz.Retrieve(context.Background(), schemeName+":", nil) + require.NoError(t, err) + + actual, err := retrieved.AsConf() + require.NoError(t, err) + + assert.Equal(t, expected.ToStringMap(), actual.ToStringMap()) + assert.NoError(t, signoz.Shutdown(context.Background())) +} diff --git a/pkg/instrumentation/config.go b/pkg/instrumentation/config.go new file mode 100644 index 0000000000..20eb9b97bb --- /dev/null +++ b/pkg/instrumentation/config.go @@ -0,0 +1,62 @@ +package instrumentation + +import ( + contribsdkconfig "go.opentelemetry.io/contrib/config" + "go.signoz.io/signoz/pkg/confmap" + "go.uber.org/zap/zapcore" +) + +// Config satisfies the confmap.Config interface +var _ confmap.Config = (*Config)(nil) + +// Config holds the configuration for all instrumentation components. +type Config struct { + Logs LogsConfig `mapstructure:"logs"` + Traces TracesConfig `mapstructure:"traces"` + Metrics MetricsConfig `mapstructure:"metrics"` + Resource Resource `mapstructure:"resource"` +} + +// Resource defines the configuration for OpenTelemetry resource attributes. +type Resource struct { + Attributes contribsdkconfig.Attributes `mapstructure:"attributes"` +} + +// LogsConfig holds the configuration for the logging component. +type LogsConfig struct { + Enabled bool `mapstructure:"enabled"` + Level zapcore.Level `mapstructure:"level"` + contribsdkconfig.LoggerProvider `mapstructure:",squash"` +} + +// TracesConfig holds the configuration for the tracing component. +type TracesConfig struct { + Enabled bool `mapstructure:"enabled"` + contribsdkconfig.TracerProvider `mapstructure:",squash"` +} + +// MetricsConfig holds the configuration for the metrics component. +type MetricsConfig struct { + Enabled bool `mapstructure:"enabled"` + contribsdkconfig.MeterProvider `mapstructure:",squash"` +} + +func (c *Config) NewWithDefaults() confmap.Config { + return &Config{ + Logs: LogsConfig{ + Enabled: false, + Level: zapcore.InfoLevel, + }, + Traces: TracesConfig{ + Enabled: false, + }, + Metrics: MetricsConfig{ + Enabled: false, + }, + } + +} + +func (c *Config) Validate() error { + return nil +} diff --git a/pkg/instrumentation/doc.go b/pkg/instrumentation/doc.go new file mode 100644 index 0000000000..ebe97eb30a --- /dev/null +++ b/pkg/instrumentation/doc.go @@ -0,0 +1,5 @@ +// Package instrumentation provides utilities for initializing and managing +// OpenTelemetry resources, logging, tracing, and metering within the application. It +// leverages the OpenTelemetry SDK to facilitate the collection and +// export of telemetry data, to an OTLP (OpenTelemetry Protocol) endpoint. +package instrumentation diff --git a/pkg/instrumentation/instrumentation.go b/pkg/instrumentation/instrumentation.go new file mode 100644 index 0000000000..3f12fc9d63 --- /dev/null +++ b/pkg/instrumentation/instrumentation.go @@ -0,0 +1,95 @@ +package instrumentation + +import ( + "context" + "fmt" + + contribsdkconfig "go.opentelemetry.io/contrib/config" + sdklog "go.opentelemetry.io/otel/log" + sdkmetric "go.opentelemetry.io/otel/metric" + sdkresource "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + sdktrace "go.opentelemetry.io/otel/trace" + "go.signoz.io/signoz/pkg/version" + "go.uber.org/zap" +) + +// Instrumentation holds the core components for application instrumentation. +type Instrumentation struct { + LoggerProvider sdklog.LoggerProvider + Logger *zap.Logger + MeterProvider sdkmetric.MeterProvider + TracerProvider sdktrace.TracerProvider +} + +// New creates a new Instrumentation instance with configured providers. +// It sets up logging, tracing, and metrics based on the provided configuration. +func New(ctx context.Context, build version.Build, cfg Config) (*Instrumentation, error) { + // Set default resource attributes if not provided + if cfg.Resource.Attributes == nil { + cfg.Resource.Attributes = map[string]any{ + string(semconv.ServiceNameKey): build.Name, + string(semconv.ServiceVersionKey): build.Version, + } + } + + // Create a new resource with default detectors. + // The upstream contrib repository is not taking detectors into account. + // We are, therefore, using some sensible defaults here. + resource, err := sdkresource.New( + ctx, + sdkresource.WithContainer(), + sdkresource.WithFromEnv(), + sdkresource.WithHost(), + ) + if err != nil { + return nil, err + } + + // Prepare the resource configuration by merging + // resource and attributes. + sch := semconv.SchemaURL + configResource := contribsdkconfig.Resource{ + Attributes: attributes(cfg.Resource.Attributes, resource), + Detectors: nil, + SchemaUrl: &sch, + } + + loggerProvider, err := newLoggerProvider(ctx, cfg, configResource) + if err != nil { + return nil, fmt.Errorf("cannot create logger provider: %w", err) + } + + tracerProvider, err := newTracerProvider(ctx, cfg, configResource) + if err != nil { + return nil, fmt.Errorf("cannot create tracer provider: %w", err) + } + + meterProvider, err := newMeterProvider(ctx, cfg, configResource) + if err != nil { + return nil, fmt.Errorf("cannot create meter provider: %w", err) + } + + return &Instrumentation{ + LoggerProvider: loggerProvider, + TracerProvider: tracerProvider, + MeterProvider: meterProvider, + Logger: newLogger(cfg, loggerProvider), + }, nil +} + +// attributes merges the input attributes with the resource attributes. +func attributes(input map[string]any, resource *sdkresource.Resource) map[string]any { + output := make(map[string]any) + + for k, v := range input { + output[k] = v + } + + kvs := resource.Attributes() + for _, kv := range kvs { + output[string(kv.Key)] = kv.Value + } + + return output +} diff --git a/pkg/instrumentation/logger.go b/pkg/instrumentation/logger.go new file mode 100644 index 0000000000..255323bc1a --- /dev/null +++ b/pkg/instrumentation/logger.go @@ -0,0 +1,45 @@ +package instrumentation + +import ( + "context" + "os" + + "go.opentelemetry.io/contrib/bridges/otelzap" + contribsdkconfig "go.opentelemetry.io/contrib/config" + sdklog "go.opentelemetry.io/otel/log" + nooplog "go.opentelemetry.io/otel/log/noop" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// newLoggerProvider creates a new logger provider based on the configuration. +// If logging is disabled, it returns a no-op logger provider. +func newLoggerProvider(ctx context.Context, cfg Config, cfgResource contribsdkconfig.Resource) (sdklog.LoggerProvider, error) { + if !cfg.Logs.Enabled { + return nooplog.NewLoggerProvider(), nil + } + + sdk, err := contribsdkconfig.NewSDK( + contribsdkconfig.WithContext(ctx), + contribsdkconfig.WithOpenTelemetryConfiguration(contribsdkconfig.OpenTelemetryConfiguration{ + LoggerProvider: &cfg.Logs.LoggerProvider, + Resource: &cfgResource, + }), + ) + if err != nil { + return nil, err + } + + return sdk.LoggerProvider(), nil +} + +// newLogger creates a new Zap logger with the configured level and output. +// It combines a JSON encoder for stdout and an OpenTelemetry bridge. +func newLogger(cfg Config, provider sdklog.LoggerProvider) *zap.Logger { + core := zapcore.NewTee( + zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(os.Stdout), cfg.Logs.Level), + otelzap.NewCore("go.signoz.io/pkg/instrumentation", otelzap.WithLoggerProvider(provider)), + ) + + return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel)) +} diff --git a/pkg/instrumentation/meter.go b/pkg/instrumentation/meter.go new file mode 100644 index 0000000000..f1433ee29f --- /dev/null +++ b/pkg/instrumentation/meter.go @@ -0,0 +1,30 @@ +package instrumentation + +import ( + "context" + + contribsdkconfig "go.opentelemetry.io/contrib/config" + sdkmetric "go.opentelemetry.io/otel/metric" + noopmetric "go.opentelemetry.io/otel/metric/noop" +) + +// newMeterProvider creates a new meter provider based on the configuration. +// If metrics are disabled, it returns a no-op meter provider. +func newMeterProvider(ctx context.Context, cfg Config, cfgResource contribsdkconfig.Resource) (sdkmetric.MeterProvider, error) { + if !cfg.Metrics.Enabled { + return noopmetric.NewMeterProvider(), nil + } + + sdk, err := contribsdkconfig.NewSDK( + contribsdkconfig.WithContext(ctx), + contribsdkconfig.WithOpenTelemetryConfiguration(contribsdkconfig.OpenTelemetryConfiguration{ + MeterProvider: &cfg.Metrics.MeterProvider, + Resource: &cfgResource, + }), + ) + if err != nil { + return nil, err + } + + return sdk.MeterProvider(), nil +} diff --git a/pkg/instrumentation/tracer.go b/pkg/instrumentation/tracer.go new file mode 100644 index 0000000000..a2231217dc --- /dev/null +++ b/pkg/instrumentation/tracer.go @@ -0,0 +1,30 @@ +package instrumentation + +import ( + "context" + + contribsdkconfig "go.opentelemetry.io/contrib/config" + sdktrace "go.opentelemetry.io/otel/trace" + nooptrace "go.opentelemetry.io/otel/trace/noop" +) + +// newTracerProvider creates a new tracer provider based on the configuration. +// If tracing is disabled, it returns a no-op tracer provider. +func newTracerProvider(ctx context.Context, cfg Config, cfgResource contribsdkconfig.Resource) (sdktrace.TracerProvider, error) { + if !cfg.Traces.Enabled { + return nooptrace.NewTracerProvider(), nil + } + + sdk, err := contribsdkconfig.NewSDK( + contribsdkconfig.WithContext(ctx), + contribsdkconfig.WithOpenTelemetryConfiguration(contribsdkconfig.OpenTelemetryConfiguration{ + TracerProvider: &cfg.Traces.TracerProvider, + Resource: &cfgResource, + }), + ) + if err != nil { + return nil, err + } + + return sdk.TracerProvider(), nil +} diff --git a/pkg/query-service/collectorsimulator/inmemoryexporter/config_test.go b/pkg/query-service/collectorsimulator/inmemoryexporter/config_test.go index 29749757dc..c9628451d4 100644 --- a/pkg/query-service/collectorsimulator/inmemoryexporter/config_test.go +++ b/pkg/query-service/collectorsimulator/inmemoryexporter/config_test.go @@ -34,7 +34,7 @@ func TestValidate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - err := component.UnmarshalConfig(tt.rawConf, cfg) + err := tt.rawConf.Unmarshal(cfg) require.NoError(t, err, "could not UnmarshalConfig") err = component.ValidateConfig(cfg) diff --git a/pkg/query-service/collectorsimulator/inmemoryexporter/exporter_test.go b/pkg/query-service/collectorsimulator/inmemoryexporter/exporter_test.go index 4fe4753d72..a2d60439b4 100644 --- a/pkg/query-service/collectorsimulator/inmemoryexporter/exporter_test.go +++ b/pkg/query-service/collectorsimulator/inmemoryexporter/exporter_test.go @@ -6,7 +6,6 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" - "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componenttest" "go.opentelemetry.io/collector/confmap" "go.opentelemetry.io/collector/exporter" @@ -57,9 +56,7 @@ func makeTestExporter(exporterId string) (exporter.Logs, error) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - component.UnmarshalConfig(confmap.NewFromStringMap( - map[string]interface{}{"id": exporterId}), cfg, - ) + confmap.NewFromStringMap(map[string]any{"id": exporterId}).Unmarshal(&cfg) return factory.CreateLogsExporter( context.Background(), exporter.CreateSettings{}, cfg, diff --git a/pkg/query-service/collectorsimulator/inmemoryreceiver/config_test.go b/pkg/query-service/collectorsimulator/inmemoryreceiver/config_test.go index a0daf71c45..5a6144a379 100644 --- a/pkg/query-service/collectorsimulator/inmemoryreceiver/config_test.go +++ b/pkg/query-service/collectorsimulator/inmemoryreceiver/config_test.go @@ -34,7 +34,7 @@ func TestValidate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - err := component.UnmarshalConfig(tt.rawConf, cfg) + err := tt.rawConf.Unmarshal(&cfg) require.NoError(t, err, "could not UnmarshalConfig") err = component.ValidateConfig(cfg) diff --git a/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver_test.go b/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver_test.go index 4fe7169cc7..9205147156 100644 --- a/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver_test.go +++ b/pkg/query-service/collectorsimulator/inmemoryreceiver/receiver_test.go @@ -6,7 +6,6 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" - "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componenttest" "go.opentelemetry.io/collector/confmap" "go.opentelemetry.io/collector/consumer/consumertest" @@ -58,9 +57,8 @@ func makeTestLogReceiver(receiverId string) (receiver.Logs, error) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - component.UnmarshalConfig(confmap.NewFromStringMap( - map[string]interface{}{"id": receiverId}), cfg, - ) + + confmap.NewFromStringMap(map[string]any{"id": receiverId}).Unmarshal(&cfg) return factory.CreateLogsReceiver( context.Background(), receiver.CreateSettings{}, cfg, consumertest.NewNop(), diff --git a/pkg/version/doc.go b/pkg/version/doc.go new file mode 100644 index 0000000000..9b6483a085 --- /dev/null +++ b/pkg/version/doc.go @@ -0,0 +1,4 @@ +// Package version is used to track the build information of the application. +// This is typically set via ldflags at build time. +// Eg: -ldflags="-X 'pkg/version.Build.Version=v1.0.0'" +package version diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 0000000000..9cbd8c4436 --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,9 @@ +package version + +// Build contains information about the build environment. +type Build struct { + // The name of the current build. + Name string + // The version of the current build. + Version string +} From a20794040ac200785686e0153f9301d0638ae71e Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Wed, 21 Aug 2024 15:04:42 +0530 Subject: [PATCH 17/60] chore: added trace explorer test (#5531) * feat: added trace filter test cases * feat: added trace filter test cases - initial render * feat: added test cases - query sync, filter section behaviour etc * feat: deleted mock-data files * feat: added test cases of undefined filters and items * feat: deleted tsconfig * feat: added clear and rest btn test cases for traces filters * feat: added collapse and uncollapse test for traces filters * chore: added trace explorer tests --- .../ExplorerOptions/ExplorerOptions.tsx | 2 + .../ExplorerOptionsHideArea.tsx | 1 + .../__mockdata__/explorer_views.ts | 62 +++ .../mocks-server/__mockdata__/query_range.ts | 243 +++++++++++ frontend/src/mocks-server/handlers.ts | 10 + .../__test__/TracesExplorer.test.tsx | 392 ++++++++++++++---- .../TracesExplorer/__test__/testUtils.ts | 261 ++++++++++++ 7 files changed, 889 insertions(+), 82 deletions(-) create mode 100644 frontend/src/pages/TracesExplorer/__test__/testUtils.ts diff --git a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx index f34a4470a9..44378e602b 100644 --- a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx +++ b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx @@ -501,6 +501,7 @@ function ExplorerOptions({ shape="circle" onClick={hideToolbar} icon={} + data-testid="hide-toolbar" />
@@ -530,6 +531,7 @@ function ExplorerOptions({ icon={} onClick={onSaveHandler} disabled={isSaveViewLoading} + data-testid="save-view-btn" > Save this view , diff --git a/frontend/src/container/ExplorerOptions/ExplorerOptionsHideArea.tsx b/frontend/src/container/ExplorerOptions/ExplorerOptionsHideArea.tsx index a420c25ecc..efdaef1cd1 100644 --- a/frontend/src/container/ExplorerOptions/ExplorerOptionsHideArea.tsx +++ b/frontend/src/container/ExplorerOptions/ExplorerOptionsHideArea.tsx @@ -65,6 +65,7 @@ function ExplorerOptionsHideArea({ // style={{ alignSelf: 'center', marginRight: 'calc(10% - 20px)' }} className="explorer-show-btn" onClick={handleShowExplorerOption} + data-testid="show-explorer-option" >
diff --git a/frontend/src/mocks-server/__mockdata__/explorer_views.ts b/frontend/src/mocks-server/__mockdata__/explorer_views.ts index ae88071e55..b51eb2ee9c 100644 --- a/frontend/src/mocks-server/__mockdata__/explorer_views.ts +++ b/frontend/src/mocks-server/__mockdata__/explorer_views.ts @@ -77,5 +77,67 @@ export const explorerView = { }, extraData: '{"color":"#00ffd0"}', }, + { + uuid: '58b010b6-8be9-40d1-8d25-f73b5f7314ad', + name: 'success traces list view', + category: '', + createdAt: '2023-08-30T13:00:40.958011925Z', + createdBy: 'test-email', + updatedAt: '2024-04-29T13:09:06.175537361Z', + updatedBy: 'test-email', + sourcePage: 'traces', + tags: [''], + compositeQuery: { + builderQueries: { + A: { + queryName: 'A', + stepInterval: 60, + dataSource: 'traces', + aggregateOperator: 'noop', + aggregateAttribute: { + key: '', + dataType: '', + type: '', + isColumn: false, + isJSON: false, + }, + filters: { + op: 'AND', + items: [ + { + key: { + key: 'responseStatusCode', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + }, + value: '200', + op: '=', + }, + ], + }, + expression: 'A', + disabled: false, + limit: 0, + offset: 0, + pageSize: 0, + orderBy: [ + { + columnName: 'timestamp', + order: 'desc', + }, + ], + reduceTo: 'sum', + timeAggregation: 'rate', + spaceAggregation: 'sum', + ShiftBy: 0, + }, + }, + panelType: 'list', + queryType: 'builder', + }, + extraData: '{"color":"#bdff9d"}', + }, ], }; diff --git a/frontend/src/mocks-server/__mockdata__/query_range.ts b/frontend/src/mocks-server/__mockdata__/query_range.ts index 69ff7bfb66..ef55283a96 100644 --- a/frontend/src/mocks-server/__mockdata__/query_range.ts +++ b/frontend/src/mocks-server/__mockdata__/query_range.ts @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/no-duplicate-string */ import { PANEL_TYPES } from 'constants/queryBuilder'; import { QueryRangePayload } from 'types/api/metrics/getQueryRange'; import { EQueryType } from 'types/common/dashboard'; @@ -77,3 +78,245 @@ export const queryRangeSuccessResponse: QueryRangePayload = { start: 0, step: 0, }; + +export const queryRangeForTimeSeries = { + status: 'success', + data: { + resultType: '', + result: [ + { + queryName: 'A', + series: [ + { + labels: {}, + labelsArray: null, + values: [ + { + timestamp: 1721378340000, + value: '3074', + }, + { + timestamp: 1721378100000, + value: '2983', + }, + { + timestamp: 1721378040000, + value: '2978', + }, + { + timestamp: 1721378160000, + value: '2940', + }, + { + timestamp: 1721377980000, + value: '2904', + }, + { + timestamp: 1721378280000, + value: '2874', + }, + { + timestamp: 1721378220000, + value: '2667', + }, + ], + }, + ], + }, + ], + }, +}; + +export const queryRangeForListView = { + status: 'success', + data: { + resultType: '', + result: [ + { + queryName: 'A', + list: [ + { + timestamp: '2024-07-19T08:39:59.949129915Z', + data: { + dbName: '', + durationNano: 790949390, + httpMethod: '', + name: 'authenticate_check_db', + responseStatusCode: '', + serviceName: 'demo-app', + spanID: '5704353737b6778e', + statusCode: 0, + traceID: 'a364a8e15af3e9a8c866e0528db8b637', + }, + }, + { + timestamp: '2024-07-19T08:39:59.506524482Z', + data: { + dbName: '', + durationNano: 1375203118, + httpMethod: '', + name: 'check cart in cache', + responseStatusCode: '', + serviceName: 'demo-app', + spanID: '2134bb1165c928aa', + statusCode: 0, + traceID: '7b565bc351bac2a12c004d92d3a809b1', + }, + }, + { + timestamp: '2024-07-19T08:39:58.735245Z', + data: { + dbName: '', + durationNano: 55306000, + httpMethod: 'GET', + name: 'HTTP GET', + responseStatusCode: '200', + serviceName: 'frontend', + spanID: '772c4d29dd9076ac', + statusCode: 0, + traceID: '0000000000000000344ded1387b08a7e', + }, + }, + ], + }, + ], + }, +}; + +export const queryRangeForTableView = { + status: 'success', + data: { + resultType: '', + result: [ + { + queryName: 'A', + series: [ + { + labels: {}, + labelsArray: null, + values: [ + { + timestamp: 1721583834000, + value: '87798', + }, + ], + }, + ], + }, + ], + }, +}; + +export const queryRangeForTraceView = { + status: 'success', + data: { + resultType: '', + result: [ + { + queryName: 'A', + list: [ + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 7245231266, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '5765b60ba7cc4ddafe8bdaa9c1b4b246', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 7218609120, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '1593c896d96cc6b2478bb95dcc01e3f5', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 7217156051, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: 'dcd145ed13937795c5e2ee8618ec7e32', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 7054152134, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: 'd9ceed0a6b23ed4b3bff664e2b303382', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 7052324178, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: 'f76f1acc10a9149121c2bf715d1f92c5', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 6998186102, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '1e3acf6649147117836cfdde66e2bde5', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 6898849195, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '035b210595493adcef4c7f297a427bb0', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 6829435795, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '4ae4d4d082fc6d7a20d90ae0b1d0fff1', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 6790765891, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: '7975c032b430ac63479e5d578c1f0edd', + }, + }, + { + timestamp: '0001-01-01T00:00:00Z', + data: { + span_count: 8, + 'subQuery.durationNano': 6786616927, + 'subQuery.name': 'home', + 'subQuery.serviceName': 'demo-app', + traceID: 'ce9d3e5d66dbdd41d46d519b615cce52', + }, + }, + ], + }, + ], + }, +}; diff --git a/frontend/src/mocks-server/handlers.ts b/frontend/src/mocks-server/handlers.ts index 87287a0d1a..22978717a5 100644 --- a/frontend/src/mocks-server/handlers.ts +++ b/frontend/src/mocks-server/handlers.ts @@ -210,6 +210,16 @@ export const handlers = [ res(ctx.status(200), ctx.json(explorerView)), ), + rest.post('http://localhost/api/v1/explorer/views', (req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + status: 'success', + data: '7731ece1-3fa3-4ed4-8b1c-58b4c28723b2', + }), + ), + ), + rest.post('http://localhost/api/v1/event', (req, res, ctx) => res( ctx.status(200), diff --git a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx index 1dcaaaa4cf..0622a365c2 100644 --- a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx +++ b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx @@ -1,27 +1,56 @@ /* eslint-disable sonarjs/no-duplicate-string */ -/* eslint-disable no-restricted-syntax */ -/* eslint-disable no-await-in-loop */ import userEvent from '@testing-library/user-event'; import { initialQueriesMap, initialQueryBuilderFormValues, + PANEL_TYPES, } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam'; +import { + queryRangeForListView, + queryRangeForTableView, + queryRangeForTimeSeries, + queryRangeForTraceView, +} from 'mocks-server/__mockdata__/query_range'; +import { server } from 'mocks-server/server'; +import { rest } from 'msw'; import { QueryBuilderContext } from 'providers/QueryBuilder'; -import { fireEvent, render, screen, waitFor, within } from 'tests/test-utils'; -import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { + act, + fireEvent, + render, + screen, + waitFor, + within, +} from 'tests/test-utils'; import TracesExplorer from '..'; import { Filter } from '../Filter/Filter'; import { AllTraceFilterKeyValue } from '../Filter/filterUtils'; +import { + checkForSectionContent, + checkIfSectionIsNotOpen, + checkIfSectionIsOpen, + compositeQuery, + defaultClosedSections, + defaultOpenSections, + optionMenuReturn, + qbProviderValue, + redirectWithQueryBuilderData, +} from './testUtils'; + +const historyPush = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: (): { pathname: string } => ({ pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.TRACES_EXPLORER}/`, }), + useHistory: (): any => ({ + ...jest.requireActual('react-router-dom').useHistory(), + push: historyPush, + }), })); jest.mock('uplot', () => { @@ -40,6 +69,25 @@ jest.mock('uplot', () => { }; }); +jest.mock( + 'components/Uplot/Uplot', + () => + function MockUplot(): JSX.Element { + return
MockUplot
; + }, +); + +const successNotification = jest.fn(); +jest.mock('hooks/useNotifications', () => ({ + __esModule: true, + useNotifications: jest.fn(() => ({ + notifications: { + success: successNotification, + error: jest.fn(), + }, + })), +})); + jest.mock( 'container/TopNav/DateTimeSelectionV2/index.tsx', () => @@ -48,84 +96,12 @@ jest.mock( }, ); -function checkIfSectionIsOpen( - getByTestId: (testId: string) => HTMLElement, - panelName: string, -): void { - const section = getByTestId(`collapse-${panelName}`); - expect(section.querySelector('.ant-collapse-item-active')).not.toBeNull(); -} - -function checkIfSectionIsNotOpen( - getByTestId: (testId: string) => HTMLElement, - panelName: string, -): void { - const section = getByTestId(`collapse-${panelName}`); - expect(section.querySelector('.ant-collapse-item-active')).toBeNull(); -} - -const defaultOpenSections = ['hasError', 'durationNano', 'serviceName']; - -const defaultClosedSections = Object.keys(AllTraceFilterKeyValue).filter( - (section) => - ![...defaultOpenSections, 'durationNanoMin', 'durationNanoMax'].includes( - section, - ), -); - -async function checkForSectionContent(values: string[]): Promise { - for (const val of values) { - const sectionContent = await screen.findByText(val); - await waitFor(() => expect(sectionContent).toBeInTheDocument()); - } -} - -const redirectWithQueryBuilderData = jest.fn(); - -const compositeQuery: Query = { - ...initialQueriesMap.traces, - builder: { - ...initialQueriesMap.traces.builder, - queryData: [ - { - ...initialQueryBuilderFormValues, - filters: { - items: [ - { - id: '95564eb1', - key: { - key: 'name', - dataType: DataTypes.String, - type: 'tag', - isColumn: true, - isJSON: false, - id: 'name--string--tag--true', - }, - op: 'in', - value: ['HTTP GET /customer'], - }, - { - id: '3337951c', - key: { - key: 'serviceName', - dataType: DataTypes.String, - type: 'tag', - isColumn: true, - isJSON: false, - id: 'serviceName--string--tag--true', - }, - op: 'in', - value: ['demo-app'], - }, - ], - op: 'AND', - }, - }, - ], - }, -}; +jest.mock('container/OptionsMenu/useOptionsMenu', () => ({ + __esModule: true, + default: (): any => optionMenuReturn, +})); -describe('TracesExplorer - ', () => { +describe('TracesExplorer - Filters', () => { // Initial filter panel rendering // Test the initial state like which filters section are opened, default state of duration slider, etc. it('should render the Trace filter', async () => { @@ -457,3 +433,255 @@ describe('TracesExplorer - ', () => { ).toBeInTheDocument(); }); }); + +const handleExplorerTabChangeTest = jest.fn(); +jest.mock('hooks/useHandleExplorerTabChange', () => ({ + useHandleExplorerTabChange: jest.fn(() => ({ + handleExplorerTabChange: handleExplorerTabChangeTest, + })), +})); + +describe('TracesExplorer - ', () => { + it('should render the traces explorer page', async () => { + server.use( + rest.post('http://localhost/api/v3/query_range', (req, res, ctx) => + res(ctx.status(200), ctx.json(queryRangeForTimeSeries)), + ), + ); + const { findByText, getByText } = render(); + + // assert mocked date time selection + expect(await findByText('MockDateTimeSelection')).toBeInTheDocument(); + + // assert stage&Btn + expect(getByText('Stage & Run Query')).toBeInTheDocument(); + + // assert QB - will not write tests for QB as that would be covererd in QB tests separately + expect( + getByText( + 'Search Filter : select options from suggested values, for IN/NOT IN operators - press "Enter" after selecting options', + ), + ).toBeInTheDocument(); + expect(getByText('AGGREGATION INTERVAL')).toBeInTheDocument(); + expect(getByText('Metrics name')).toBeInTheDocument(); + expect(getByText('WHERE')).toBeInTheDocument(); + expect(getByText('Legend Format')).toBeInTheDocument(); + + // assert timeseries chart mock + expect(await screen.findByText('MockUplot')).toBeInTheDocument(); + }); + + it('check tab navigation', async () => { + const { getByText } = render(); + + // switch to list view + const listViewBtn = getByText('List View'); + expect(listViewBtn).toBeInTheDocument(); + fireEvent.click(listViewBtn); + + expect(handleExplorerTabChangeTest).toBeCalledWith(PANEL_TYPES.LIST); + + // switch to traces view + const tracesBtn = getByText('Traces'); + expect(tracesBtn).toBeInTheDocument(); + fireEvent.click(tracesBtn); + + expect(handleExplorerTabChangeTest).toBeCalledWith(PANEL_TYPES.TRACE); + + // switch to Table view + const TableBtn = getByText('Table View'); + expect(TableBtn).toBeInTheDocument(); + fireEvent.click(TableBtn); + + expect(handleExplorerTabChangeTest).toBeCalledWith(PANEL_TYPES.TABLE); + }); + + it('trace explorer - list view', async () => { + server.use( + rest.post('http://localhost/api/v3/query_range', (req, res, ctx) => + res(ctx.status(200), ctx.json(queryRangeForListView)), + ), + ); + + const { getByText } = render( + + + , + ); + + expect(await screen.findByText('Timestamp')).toBeInTheDocument(); + expect(getByText('options_menu.options')).toBeInTheDocument(); + + // test if pagination is there + expect(getByText('Previous')).toBeInTheDocument(); + expect(getByText('Next')).toBeInTheDocument(); + + // column interaction is covered in E2E tests as its a complex interaction + }); + + it('trace explorer - table view', async () => { + server.use( + rest.post('http://localhost/api/v3/query_range', (req, res, ctx) => + res(ctx.status(200), ctx.json(queryRangeForTableView)), + ), + ); + render( + + + , + ); + + expect(await screen.findByText('count')).toBeInTheDocument(); + expect(screen.getByText('87798.00')).toBeInTheDocument(); + }); + + it('trace explorer - trace view', async () => { + server.use( + rest.post('http://localhost/api/v3/query_range', (req, res, ctx) => + res(ctx.status(200), ctx.json(queryRangeForTraceView)), + ), + ); + const { getByText, getAllByText } = render( + + + , + ); + + expect(await screen.findByText('Root Service Name')).toBeInTheDocument(); + + // assert table headers + expect(getByText('Root Operation Name')).toBeInTheDocument(); + expect(getByText('Root Duration (in ms)')).toBeInTheDocument(); + expect(getByText('TraceID')).toBeInTheDocument(); + expect(getByText('No of Spans')).toBeInTheDocument(); + + // assert row values + ['demo-app', 'home', '8'].forEach((val) => + expect(getAllByText(val)[0]).toBeInTheDocument(), + ); + expect(getByText('7245.23ms')).toBeInTheDocument(); + + // assert traceId and redirection to trace details + const traceId = getByText('5765b60ba7cc4ddafe8bdaa9c1b4b246'); + fireEvent.click(traceId); + + // assert redirection - should go to /trace/:traceId + expect(window.location.href).toEqual( + 'http://localhost/trace/5765b60ba7cc4ddafe8bdaa9c1b4b246', + ); + }); + + it('test for explorer options', async () => { + const { getByText, getByTestId } = render(); + + // assert explorer options - action btns + [ + 'Save this view', + 'Create an Alert', + 'Add to Dashboard', + 'Select a view', + ].forEach((val) => expect(getByText(val)).toBeInTheDocument()); + + const hideExplorerOption = getByTestId('hide-toolbar'); + expect(hideExplorerOption).toBeInTheDocument(); + fireEvent.click(hideExplorerOption); + + // explorer options should hide and show btn should be present + expect(await screen.findByTestId('show-explorer-option')).toBeInTheDocument(); + expect(screen.queryByTestId('hide-toolbar')).toBeNull(); + + // show explorer options + const showExplorerOption = screen.getByTestId('show-explorer-option'); + expect(showExplorerOption).toBeInTheDocument(); + fireEvent.click(showExplorerOption); + + // explorer options should show and hide btn should be present + expect(await screen.findByTestId('hide-toolbar')).toBeInTheDocument(); + }); + + it('select a view options - assert and save this view', async () => { + const { container } = render(); + + await act(async () => { + fireEvent.mouseDown( + container.querySelector( + '.view-options .ant-select-selection-search-input', + ) as HTMLElement, + ); + }); + + const viewListOptions = await screen.findByRole('listbox'); + expect(viewListOptions).toBeInTheDocument(); + + expect( + within(viewListOptions).getByText('success traces list view'), + ).toBeInTheDocument(); + + expect(within(viewListOptions).getByText('Table View')).toBeInTheDocument(); + + // save this view + fireEvent.click(screen.getByText('Save this view')); + + const saveViewModalInput = await screen.findByPlaceholderText( + 'e.g. External http method view', + ); + expect(saveViewModalInput).toBeInTheDocument(); + + const saveViewModal = document.querySelector( + '.ant-modal-content', + ) as HTMLElement; + expect(saveViewModal).toBeInTheDocument(); + + await act(async () => + fireEvent.change(saveViewModalInput, { target: { value: 'test view' } }), + ); + + expect(saveViewModalInput).toHaveValue('test view'); + await act(async () => { + fireEvent.click(within(saveViewModal).getByTestId('save-view-btn')); + }); + + expect(successNotification).toHaveBeenCalledWith({ + message: 'View Saved Successfully', + }); + }); + + it('create a dashboard btn assert', async () => { + const { getByText } = render(); + + const createDashboardBtn = getByText('Add to Dashboard'); + expect(createDashboardBtn).toBeInTheDocument(); + fireEvent.click(createDashboardBtn); + + expect(await screen.findByText('Export Panel')).toBeInTheDocument(); + const createDashboardModal = document.querySelector( + '.ant-modal-content', + ) as HTMLElement; + expect(createDashboardModal).toBeInTheDocument(); + + // assert modal content + expect( + within(createDashboardModal).getByText('Select Dashboard'), + ).toBeInTheDocument(); + + expect( + within(createDashboardModal).getByText('New Dashboard'), + ).toBeInTheDocument(); + }); + + it('create an alert btn assert', async () => { + const { getByText } = render(); + + const createAlertBtn = getByText('Create an Alert'); + expect(createAlertBtn).toBeInTheDocument(); + fireEvent.click(createAlertBtn); + + expect(historyPush).toHaveBeenCalledWith( + expect.stringContaining(`${ROUTES.ALERTS_NEW}`), + ); + }); +}); diff --git a/frontend/src/pages/TracesExplorer/__test__/testUtils.ts b/frontend/src/pages/TracesExplorer/__test__/testUtils.ts new file mode 100644 index 0000000000..80d96c9cf3 --- /dev/null +++ b/frontend/src/pages/TracesExplorer/__test__/testUtils.ts @@ -0,0 +1,261 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-await-in-loop */ +import { + initialQueriesMap, + initialQueryBuilderFormValues, + PANEL_TYPES, +} from 'constants/queryBuilder'; +import { noop } from 'lodash-es'; +import { screen, waitFor } from 'tests/test-utils'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + +import { AllTraceFilterKeyValue } from '../Filter/filterUtils'; + +export const optionMenuReturn = { + options: { + selectColumns: [ + { + key: 'serviceName', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'serviceName--string--tag--true', + }, + { + key: 'name', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'name--string--tag--true', + }, + { + key: 'durationNano', + dataType: 'float64', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'durationNano--float64--tag--true', + }, + { + key: 'httpMethod', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'httpMethod--string--tag--true', + }, + { + key: 'responseStatusCode', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'responseStatusCode--string--tag--true', + }, + { + key: 'statusCode', + dataType: 'float64', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'statusCode--float64--tag--true', + }, + { + key: 'dbName', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'dbName--string--tag--true', + }, + ], + maxLines: 2, + format: 'list', + }, + handleOptionsChange: jest.fn(), + config: { + addColumn: { + isFetching: false, + value: [ + { + key: 'serviceName', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'serviceName--string--tag--true', + }, + { + key: 'name', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'name--string--tag--true', + }, + { + key: 'durationNano', + dataType: 'float64', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'durationNano--float64--tag--true', + }, + { + key: 'httpMethod', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'httpMethod--string--tag--true', + }, + { + key: 'responseStatusCode', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'responseStatusCode--string--tag--true', + }, + { + key: 'statusCode', + dataType: 'float64', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'statusCode--float64--tag--true', + }, + { + key: 'dbName', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'dbName--string--tag--true', + }, + ], + options: [], + }, + format: { + value: 'list', + }, + maxLines: { + value: 2, + }, + }, +}; + +export const compositeQuery: Query = { + ...initialQueriesMap.traces, + builder: { + ...initialQueriesMap.traces.builder, + queryData: [ + { + ...initialQueryBuilderFormValues, + filters: { + items: [ + { + id: '95564eb1', + key: { + key: 'name', + dataType: DataTypes.String, + type: 'tag', + isColumn: true, + isJSON: false, + id: 'name--string--tag--true', + }, + op: 'in', + value: ['HTTP GET /customer'], + }, + { + id: '3337951c', + key: { + key: 'serviceName', + dataType: DataTypes.String, + type: 'tag', + isColumn: true, + isJSON: false, + id: 'serviceName--string--tag--true', + }, + op: 'in', + value: ['demo-app'], + }, + ], + op: 'AND', + }, + }, + ], + }, +}; + +export const redirectWithQueryBuilderData = jest.fn(); + +export const qbProviderValue = { + currentQuery: { + ...initialQueriesMap.traces, + builder: { + ...initialQueriesMap.traces.builder, + queryData: [initialQueryBuilderFormValues], + }, + }, + redirectWithQueryBuilderData, + panelType: PANEL_TYPES.LIST, + setSupersetQuery: jest.fn(), + supersetQuery: initialQueriesMap.traces, + stagedQuery: initialQueriesMap.traces, + initialDataSource: null, + isEnabledQuery: false, + handleSetQueryData: noop, + handleSetFormulaData: noop, + handleSetQueryItemData: noop, + handleSetConfig: noop, + removeQueryBuilderEntityByIndex: noop, + removeQueryTypeItemByIndex: noop, + addNewBuilderQuery: noop, + cloneQuery: noop, + addNewFormula: noop, + addNewQueryItem: noop, + handleRunQuery: noop, + resetQuery: noop, + updateAllQueriesOperators: (): Query => initialQueriesMap.traces, + updateQueriesData: (): Query => initialQueriesMap.traces, + initQueryBuilderData: noop, + handleOnUnitsChange: noop, + isStagedQueryUpdated: (): boolean => false, +} as any; + +export function checkIfSectionIsOpen( + getByTestId: (testId: string) => HTMLElement, + panelName: string, +): void { + const section = getByTestId(`collapse-${panelName}`); + expect(section.querySelector('.ant-collapse-item-active')).not.toBeNull(); +} + +export function checkIfSectionIsNotOpen( + getByTestId: (testId: string) => HTMLElement, + panelName: string, +): void { + const section = getByTestId(`collapse-${panelName}`); + expect(section.querySelector('.ant-collapse-item-active')).toBeNull(); +} + +export const defaultOpenSections = ['hasError', 'durationNano', 'serviceName']; + +export const defaultClosedSections = Object.keys(AllTraceFilterKeyValue).filter( + (section) => + ![...defaultOpenSections, 'durationNanoMin', 'durationNanoMax'].includes( + section, + ), +); + +export async function checkForSectionContent(values: string[]): Promise { + for (const val of values) { + const sectionContent = await screen.findByText(val); + await waitFor(() => expect(sectionContent).toBeInTheDocument()); + } +} From 072693d57dc802d87d3f38146f2870ab68387da0 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Wed, 21 Aug 2024 17:55:16 +0530 Subject: [PATCH 18/60] fix: nan and inf values in formula result (#5733) --- pkg/query-service/postprocess/formula.go | 4 + pkg/query-service/postprocess/formula_test.go | 171 +++++++----------- 2 files changed, 65 insertions(+), 110 deletions(-) diff --git a/pkg/query-service/postprocess/formula.go b/pkg/query-service/postprocess/formula.go index 6c800b47c4..4d928a6a48 100644 --- a/pkg/query-service/postprocess/formula.go +++ b/pkg/query-service/postprocess/formula.go @@ -146,6 +146,10 @@ func joinAndCalculate( return nil, fmt.Errorf("expected float64, got %T", newValue) } + if math.IsNaN(val) || math.IsInf(val, 0) { + continue + } + resultSeries.Points = append(resultSeries.Points, v3.Point{ Timestamp: timestamp, Value: val, diff --git a/pkg/query-service/postprocess/formula_test.go b/pkg/query-service/postprocess/formula_test.go index d80519b105..22fc9f61db 100644 --- a/pkg/query-service/postprocess/formula_test.go +++ b/pkg/query-service/postprocess/formula_test.go @@ -1,7 +1,6 @@ package postprocess import ( - "math" "reflect" "testing" @@ -204,9 +203,10 @@ func TestFindUniqueLabelSets(t *testing.T) { func TestProcessResults(t *testing.T) { tests := []struct { - name string - results []*v3.Result - want *v3.Result + name string + results []*v3.Result + want *v3.Result + expression string }{ { name: "test1", @@ -288,12 +288,68 @@ func TestProcessResults(t *testing.T) { }, }, }, + expression: "A + B", + }, + { + name: "test2", + results: []*v3.Result{ + { + QueryName: "A", + Series: []*v3.Series{ + { + Labels: map[string]string{}, + Points: []v3.Point{ + { + Timestamp: 1, + Value: 10, + }, + { + Timestamp: 2, + Value: 0, + }, + }, + }, + }, + }, + { + QueryName: "B", + Series: []*v3.Series{ + { + Labels: map[string]string{}, + Points: []v3.Point{ + { + Timestamp: 1, + Value: 0, + }, + { + Timestamp: 3, + Value: 10, + }, + }, + }, + }, + }, + }, + want: &v3.Result{ + Series: []*v3.Series{ + { + Labels: map[string]string{}, + Points: []v3.Point{ + { + Timestamp: 3, + Value: 0, + }, + }, + }, + }, + }, + expression: "A/B", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - expression, err := govaluate.NewEvaluableExpression("A + B") + expression, err := govaluate.NewEvaluableExpression(tt.expression) if err != nil { t.Errorf("Error parsing expression: %v", err) } @@ -835,10 +891,6 @@ func TestFormula(t *testing.T) { Timestamp: 5, Value: 1, }, - { - Timestamp: 7, - Value: math.Inf(1), - }, }, }, { @@ -855,10 +907,6 @@ func TestFormula(t *testing.T) { Timestamp: 2, Value: 0.6923076923076923, }, - { - Timestamp: 3, - Value: math.Inf(1), - }, { Timestamp: 4, Value: 1, @@ -997,62 +1045,6 @@ func TestFormula(t *testing.T) { }, want: &v3.Result{ Series: []*v3.Series{ - { - Labels: map[string]string{ - "host_name": "ip-10-420-69-1", - "state": "running", - }, - Points: []v3.Point{ - { - Timestamp: 1, - Value: math.Inf(0), - }, - { - Timestamp: 2, - Value: math.Inf(0), - }, - { - Timestamp: 4, - Value: math.Inf(0), - }, - { - Timestamp: 5, - Value: math.Inf(0), - }, - { - Timestamp: 7, - Value: math.Inf(0), - }, - }, - }, - { - Labels: map[string]string{ - "host_name": "ip-10-420-69-2", - "state": "idle", - }, - Points: []v3.Point{ - { - Timestamp: 1, - Value: math.Inf(0), - }, - { - Timestamp: 2, - Value: math.Inf(0), - }, - { - Timestamp: 3, - Value: math.Inf(0), - }, - { - Timestamp: 4, - Value: math.Inf(0), - }, - { - Timestamp: 5, - Value: math.Inf(0), - }, - }, - }, { Labels: map[string]string{ "host_name": "ip-10-420-69-1", @@ -1262,39 +1254,6 @@ func TestFormula(t *testing.T) { Timestamp: 5, Value: 1, }, - { - Timestamp: 7, - Value: math.Inf(1), - }, - }, - }, - { - Labels: map[string]string{ - "host_name": "ip-10-420-69-2", - "state": "idle", - "os.type": "linux", - }, - Points: []v3.Point{ - { - Timestamp: 1, - Value: math.Inf(0), - }, - { - Timestamp: 2, - Value: math.Inf(0), - }, - { - Timestamp: 3, - Value: math.Inf(0), - }, - { - Timestamp: 4, - Value: math.Inf(0), - }, - { - Timestamp: 5, - Value: math.Inf(0), - }, }, }, { @@ -1537,10 +1496,6 @@ func TestFormula(t *testing.T) { Timestamp: 5, Value: 51, }, - { - Timestamp: 7, - Value: math.Inf(1), - }, }, }, { @@ -1558,10 +1513,6 @@ func TestFormula(t *testing.T) { Timestamp: 2, Value: 45.6923076923076923, }, - { - Timestamp: 3, - Value: math.Inf(1), - }, { Timestamp: 4, Value: 41, From e7b5410c5b250c5a5553781f7d5705b7c33a11be Mon Sep 17 00:00:00 2001 From: Vibhu Pandey Date: Thu, 22 Aug 2024 14:24:02 +0530 Subject: [PATCH 19/60] feat(packages): add registry and http packages (#5740) ### Summary Add packages for Registry and HTTP #### Related Issues / PR's https://github.com/SigNoz/signoz/pull/5710 --- .gitignore | 3 + ee/query-service/app/server.go | 8 ++ pkg/http/doc.go | 3 + pkg/http/middleware/doc.go | 2 + pkg/http/middleware/logging.go | 72 ++++++++++++++++ pkg/http/middleware/middleware.go | 20 +++++ pkg/http/middleware/response.go | 122 +++++++++++++++++++++++++++ pkg/http/middleware/timeout.go | 78 +++++++++++++++++ pkg/http/middleware/timeout_test.go | 80 ++++++++++++++++++ pkg/http/server/config.go | 27 ++++++ pkg/http/server/doc.go | 2 + pkg/http/server/server.go | 79 +++++++++++++++++ pkg/query-service/app/server.go | 9 ++ pkg/query-service/app/server_test.go | 1 + pkg/registry/doc.go | 3 + pkg/registry/registry.go | 84 ++++++++++++++++++ pkg/registry/registry_test.go | 56 ++++++++++++ pkg/registry/service.go | 16 ++++ pkg/registry/service_test.go | 49 +++++++++++ 19 files changed, 714 insertions(+) create mode 100644 pkg/http/doc.go create mode 100644 pkg/http/middleware/doc.go create mode 100644 pkg/http/middleware/logging.go create mode 100644 pkg/http/middleware/middleware.go create mode 100644 pkg/http/middleware/response.go create mode 100644 pkg/http/middleware/timeout.go create mode 100644 pkg/http/middleware/timeout_test.go create mode 100644 pkg/http/server/config.go create mode 100644 pkg/http/server/doc.go create mode 100644 pkg/http/server/server.go create mode 100644 pkg/registry/doc.go create mode 100644 pkg/registry/registry.go create mode 100644 pkg/registry/registry_test.go create mode 100644 pkg/registry/service.go create mode 100644 pkg/registry/service_test.go diff --git a/.gitignore b/.gitignore index 8fe54dcf3d..2bd9238255 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ e2e/.auth # go vendor/ **/main/** + +# git-town +.git-branches.toml diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 082ddcd358..d77e71cc28 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -376,6 +376,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e }, nil } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddleware is used for logging public api calls func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -387,6 +388,7 @@ func loggingMiddleware(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddlewarePrivate is used for logging private api calls // from internal services like alert manager func loggingMiddlewarePrivate(next http.Handler) http.Handler { @@ -399,27 +401,32 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go type loggingResponseWriter struct { http.ResponseWriter statusCode int } +// TODO(remove): Implemented at pkg/http/middleware/logging.go func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { // WriteHeader(int) is not called if our response implicitly returns 200 OK, so // we default to that status code. return &loggingResponseWriter{w, http.StatusOK} } +// TODO(remove): Implemented at pkg/http/middleware/logging.go func (lrw *loggingResponseWriter) WriteHeader(code int) { lrw.statusCode = code lrw.ResponseWriter.WriteHeader(code) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // Flush implements the http.Flush interface. func (lrw *loggingResponseWriter) Flush() { lrw.ResponseWriter.(http.Flusher).Flush() } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // Support websockets func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { h, ok := lrw.ResponseWriter.(http.Hijacker) @@ -565,6 +572,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/timeout.go func setTimeoutMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/pkg/http/doc.go b/pkg/http/doc.go new file mode 100644 index 0000000000..5e62c61b15 --- /dev/null +++ b/pkg/http/doc.go @@ -0,0 +1,3 @@ +// package http contains all http related functions such +// as servers, middlewares, routers and renders. +package http diff --git a/pkg/http/middleware/doc.go b/pkg/http/middleware/doc.go new file mode 100644 index 0000000000..911746777c --- /dev/null +++ b/pkg/http/middleware/doc.go @@ -0,0 +1,2 @@ +// package middleware contains an implementation of all middlewares. +package middleware diff --git a/pkg/http/middleware/logging.go b/pkg/http/middleware/logging.go new file mode 100644 index 0000000000..ef755f6648 --- /dev/null +++ b/pkg/http/middleware/logging.go @@ -0,0 +1,72 @@ +package middleware + +import ( + "bytes" + "net" + "net/http" + "time" + + "github.com/gorilla/mux" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.uber.org/zap" +) + +const ( + logMessage string = "::RECEIVED-REQUEST::" +) + +type Logging struct { + logger *zap.Logger +} + +func NewLogging(logger *zap.Logger) *Logging { + if logger == nil { + panic("cannot build logging, logger is empty") + } + + return &Logging{ + logger: logger.Named(pkgname), + } +} + +func (middleware *Logging) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + start := time.Now() + host, port, _ := net.SplitHostPort(req.Host) + path, err := mux.CurrentRoute(req).GetPathTemplate() + if err != nil { + path = req.URL.Path + } + + fields := []zap.Field{ + zap.Any("context", ctx), + zap.String(string(semconv.ClientAddressKey), req.RemoteAddr), + zap.String(string(semconv.UserAgentOriginalKey), req.UserAgent()), + zap.String(string(semconv.ServerAddressKey), host), + zap.String(string(semconv.ServerPortKey), port), + zap.Int64(string(semconv.HTTPRequestSizeKey), req.ContentLength), + zap.String(string(semconv.HTTPRouteKey), path), + } + + buf := new(bytes.Buffer) + writer := newBadResponseLoggingWriter(rw, buf) + next.ServeHTTP(writer, req) + + statusCode, err := writer.StatusCode(), writer.WriteError() + fields = append(fields, + zap.Int(string(semconv.HTTPResponseStatusCodeKey), statusCode), + zap.Duration(string(semconv.HTTPServerRequestDurationName), time.Since(start)), + ) + if err != nil { + fields = append(fields, zap.Error(err)) + middleware.logger.Error(logMessage, fields...) + } else { + if buf.Len() != 0 { + fields = append(fields, zap.String("response.body", buf.String())) + } + + middleware.logger.Info(logMessage, fields...) + } + }) +} diff --git a/pkg/http/middleware/middleware.go b/pkg/http/middleware/middleware.go new file mode 100644 index 0000000000..6313089aa4 --- /dev/null +++ b/pkg/http/middleware/middleware.go @@ -0,0 +1,20 @@ +package middleware + +import "net/http" + +const ( + pkgname string = "go.signoz.io/pkg/http/middleware" +) + +// Wrapper is an interface implemented by all middlewares +type Wrapper interface { + Wrap(http.Handler) http.Handler +} + +// WrapperFunc is to Wrapper as http.HandlerFunc is to http.Handler +type WrapperFunc func(http.Handler) http.Handler + +// WrapperFunc implements Wrapper +func (m WrapperFunc) Wrap(next http.Handler) http.Handler { + return m(next) +} diff --git a/pkg/http/middleware/response.go b/pkg/http/middleware/response.go new file mode 100644 index 0000000000..deb0f3dd81 --- /dev/null +++ b/pkg/http/middleware/response.go @@ -0,0 +1,122 @@ +package middleware + +import ( + "bufio" + "fmt" + "io" + "net" + "net/http" +) + +const ( + maxResponseBodyInLogs = 4096 // At most 4k bytes from response bodies in our logs. +) + +type badResponseLoggingWriter interface { + http.ResponseWriter + // Get the status code. + StatusCode() int + // Get the error while writing. + WriteError() error +} + +func newBadResponseLoggingWriter(rw http.ResponseWriter, buffer io.Writer) badResponseLoggingWriter { + b := nonFlushingBadResponseLoggingWriter{ + rw: rw, + buffer: buffer, + logBody: false, + bodyBytesLeft: maxResponseBodyInLogs, + statusCode: http.StatusOK, + } + + if f, ok := rw.(http.Flusher); ok { + return &flushingBadResponseLoggingWriter{b, f} + } + + return &b +} + +type nonFlushingBadResponseLoggingWriter struct { + rw http.ResponseWriter + buffer io.Writer + logBody bool + bodyBytesLeft int + statusCode int + writeError error // The error returned when downstream Write() fails. +} + +// Extends nonFlushingBadResponseLoggingWriter that implements http.Flusher +type flushingBadResponseLoggingWriter struct { + nonFlushingBadResponseLoggingWriter + f http.Flusher +} + +// Unwrap method is used by http.ResponseController to get access to original http.ResponseWriter. +func (writer *nonFlushingBadResponseLoggingWriter) Unwrap() http.ResponseWriter { + return writer.rw +} + +// Header returns the header map that will be sent by WriteHeader. +// Implements ResponseWriter. +func (writer *nonFlushingBadResponseLoggingWriter) Header() http.Header { + return writer.rw.Header() +} + +// WriteHeader writes the HTTP response header. +func (writer *nonFlushingBadResponseLoggingWriter) WriteHeader(statusCode int) { + writer.statusCode = statusCode + if statusCode >= 500 || statusCode == 400 { + writer.logBody = true + } + writer.rw.WriteHeader(statusCode) +} + +// Writes HTTP response data. +func (writer *nonFlushingBadResponseLoggingWriter) Write(data []byte) (int, error) { + if writer.statusCode == 0 { + // WriteHeader has (probably) not been called, so we need to call it with StatusOK to fulfill the interface contract. + // https://godoc.org/net/http#ResponseWriter + writer.WriteHeader(http.StatusOK) + } + n, err := writer.rw.Write(data) + if writer.logBody { + writer.captureResponseBody(data) + } + if err != nil { + writer.writeError = err + } + return n, err +} + +// Hijack hijacks the first response writer that is a Hijacker. +func (writer *nonFlushingBadResponseLoggingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hj, ok := writer.rw.(http.Hijacker) + if ok { + return hj.Hijack() + } + return nil, nil, fmt.Errorf("cannot cast underlying response writer to Hijacker") +} + +func (writer *nonFlushingBadResponseLoggingWriter) StatusCode() int { + return writer.statusCode +} + +func (writer *nonFlushingBadResponseLoggingWriter) WriteError() error { + return writer.writeError +} + +func (writer *flushingBadResponseLoggingWriter) Flush() { + writer.f.Flush() +} + +func (writer *nonFlushingBadResponseLoggingWriter) captureResponseBody(data []byte) { + if len(data) > writer.bodyBytesLeft { + _, _ = writer.buffer.Write(data[:writer.bodyBytesLeft]) + _, _ = io.WriteString(writer.buffer, "...") + writer.bodyBytesLeft = 0 + writer.logBody = false + } else { + _, _ = writer.buffer.Write(data) + writer.bodyBytesLeft -= len(data) + } +} diff --git a/pkg/http/middleware/timeout.go b/pkg/http/middleware/timeout.go new file mode 100644 index 0000000000..50e9d82f22 --- /dev/null +++ b/pkg/http/middleware/timeout.go @@ -0,0 +1,78 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + "time" + + "go.uber.org/zap" +) + +const ( + headerName string = "timeout" +) + +type Timeout struct { + logger *zap.Logger + excluded map[string]struct{} + // The default timeout + defaultTimeout time.Duration + // The max allowed timeout + maxTimeout time.Duration +} + +func NewTimeout(logger *zap.Logger, excluded map[string]struct{}, defaultTimeout time.Duration, maxTimeout time.Duration) *Timeout { + if logger == nil { + panic("cannot build timeout, logger is empty") + } + + if excluded == nil { + excluded = make(map[string]struct{}) + } + + if defaultTimeout.Seconds() == 0 { + defaultTimeout = 60 * time.Second + } + + if maxTimeout == 0 { + maxTimeout = 600 * time.Second + } + + return &Timeout{ + logger: logger.Named(pkgname), + excluded: excluded, + defaultTimeout: defaultTimeout, + maxTimeout: maxTimeout, + } +} + +func (middleware *Timeout) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if _, ok := middleware.excluded[req.URL.Path]; !ok { + actual := middleware.defaultTimeout + incoming := req.Header.Get(headerName) + if incoming != "" { + parsed, err := time.ParseDuration(strings.TrimSpace(incoming) + "s") + if err != nil { + middleware.logger.Warn("cannot parse timeout in header, using default timeout", zap.String("timeout", incoming), zap.Error(err), zap.Any("context", req.Context())) + } else { + if parsed > middleware.maxTimeout { + actual = middleware.maxTimeout + } else { + actual = parsed + } + } + } + + ctx, cancel := context.WithTimeout(req.Context(), actual) + defer cancel() + + req = req.WithContext(ctx) + next.ServeHTTP(rw, req) + return + } + + next.ServeHTTP(rw, req) + }) +} diff --git a/pkg/http/middleware/timeout_test.go b/pkg/http/middleware/timeout_test.go new file mode 100644 index 0000000000..2575bfe7d9 --- /dev/null +++ b/pkg/http/middleware/timeout_test.go @@ -0,0 +1,80 @@ +package middleware + +import ( + "net" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestTimeout(t *testing.T) { + t.Parallel() + + writeTimeout := 6 * time.Second + defaultTimeout := 2 * time.Second + maxTimeout := 4 * time.Second + m := NewTimeout(zap.NewNop(), map[string]struct{}{"/excluded": {}}, defaultTimeout, maxTimeout) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + WriteTimeout: writeTimeout, + Handler: m.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, ok := r.Context().Deadline() + if ok { + <-r.Context().Done() + require.Error(t, r.Context().Err()) + } + w.WriteHeader(204) + })), + } + + go func() { + require.NoError(t, server.Serve(listener)) + }() + + testCases := []struct { + name string + wait time.Duration + header string + path string + }{ + { + name: "WaitTillNoTimeoutForExcludedPath", + wait: 1 * time.Nanosecond, + header: "4", + path: "excluded", + }, + { + name: "WaitTillHeaderTimeout", + wait: 3 * time.Second, + header: "3", + path: "header-timeout", + }, + { + name: "WaitTillMaxTimeout", + wait: 4 * time.Second, + header: "5", + path: "max-timeout", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + start := time.Now() + req, err := http.NewRequest("GET", "http://"+listener.Addr().String()+"/"+tc.path, nil) + require.NoError(t, err) + req.Header.Add(headerName, tc.header) + + _, err = http.DefaultClient.Do(req) + require.NoError(t, err) + + // confirm that we waited at least till the "wait" time + require.GreaterOrEqual(t, time.Since(start), tc.wait) + }) + } +} diff --git a/pkg/http/server/config.go b/pkg/http/server/config.go new file mode 100644 index 0000000000..fb8eb5be11 --- /dev/null +++ b/pkg/http/server/config.go @@ -0,0 +1,27 @@ +package server + +import ( + "go.signoz.io/signoz/pkg/confmap" +) + +// Config satisfies the confmap.Config interface +var _ confmap.Config = (*Config)(nil) + +// Config holds the configuration for http. +type Config struct { + //Address specifies the TCP address for the server to listen on, in the form "host:port". + // If empty, ":http" (port 80) is used. The service names are defined in RFC 6335 and assigned by IANA. + // See net.Dial for details of the address format. + Address string `mapstructure:"address"` +} + +func (c *Config) NewWithDefaults() confmap.Config { + return &Config{ + Address: "0.0.0.0:8080", + } + +} + +func (c *Config) Validate() error { + return nil +} diff --git a/pkg/http/server/doc.go b/pkg/http/server/doc.go new file mode 100644 index 0000000000..9bf12b5ae5 --- /dev/null +++ b/pkg/http/server/doc.go @@ -0,0 +1,2 @@ +// package server contains an implementation of the http server. +package server diff --git a/pkg/http/server/server.go b/pkg/http/server/server.go new file mode 100644 index 0000000000..fbeca1c3a9 --- /dev/null +++ b/pkg/http/server/server.go @@ -0,0 +1,79 @@ +package server + +import ( + "context" + "fmt" + "net/http" + "time" + + "go.signoz.io/signoz/pkg/registry" + "go.uber.org/zap" +) + +var _ registry.NamedService = (*Server)(nil) + +type Server struct { + srv *http.Server + logger *zap.Logger + handler http.Handler + cfg Config + name string +} + +func New(logger *zap.Logger, name string, cfg Config, handler http.Handler) (*Server, error) { + if handler == nil { + return nil, fmt.Errorf("cannot build http server, handler is required") + } + + if logger == nil { + return nil, fmt.Errorf("cannot build http server, logger is required") + } + + if name == "" { + return nil, fmt.Errorf("cannot build http server, name is required") + } + + srv := &http.Server{ + Addr: cfg.Address, + Handler: handler, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + return &Server{ + srv: srv, + logger: logger.Named("go.signoz.io/pkg/http/server"), + handler: handler, + cfg: cfg, + name: name, + }, nil +} + +func (server *Server) Name() string { + return server.name +} + +func (server *Server) Start(ctx context.Context) error { + server.logger.Info("starting http server", zap.String("address", server.srv.Addr)) + if err := server.srv.ListenAndServe(); err != nil { + if err != http.ErrServerClosed { + server.logger.Error("failed to start server", zap.Error(err), zap.Any("context", ctx)) + return err + } + } + return nil +} + +func (server *Server) Stop(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + if err := server.srv.Shutdown(ctx); err != nil { + server.logger.Error("failed to stop server", zap.Error(err), zap.Any("context", ctx)) + return err + } + + server.logger.Info("server stopped gracefully", zap.Any("context", ctx)) + return nil +} diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 4fb4d9ad22..77caa9170b 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -322,6 +322,7 @@ func (s *Server) createPublicServer(api *APIHandler) (*http.Server, error) { }, nil } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddleware is used for logging public api calls func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -392,6 +393,7 @@ func LogCommentEnricher(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // loggingMiddlewarePrivate is used for logging private api calls // from internal services like alert manager func loggingMiddlewarePrivate(next http.Handler) http.Handler { @@ -404,27 +406,32 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go type loggingResponseWriter struct { http.ResponseWriter statusCode int } +// TODO(remove): Implemented at pkg/http/middleware/logging.go func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { // WriteHeader(int) is not called if our response implicitly returns 200 OK, so // we default to that status code. return &loggingResponseWriter{w, http.StatusOK} } +// TODO(remove): Implemented at pkg/http/middleware/logging.go func (lrw *loggingResponseWriter) WriteHeader(code int) { lrw.statusCode = code lrw.ResponseWriter.WriteHeader(code) } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // Flush implements the http.Flush interface. func (lrw *loggingResponseWriter) Flush() { lrw.ResponseWriter.(http.Flusher).Flush() } +// TODO(remove): Implemented at pkg/http/middleware/logging.go // Support websockets func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { h, ok := lrw.ResponseWriter.(http.Hijacker) @@ -538,6 +545,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler { }) } +// TODO(remove): Implemented at pkg/http/middleware/timeout.go func getRouteContextTimeout(overrideTimeout string) time.Duration { var timeout time.Duration var err error @@ -554,6 +562,7 @@ func getRouteContextTimeout(overrideTimeout string) time.Duration { return constants.ContextTimeout } +// TODO(remove): Implemented at pkg/http/middleware/timeout.go func setTimeoutMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/pkg/query-service/app/server_test.go b/pkg/query-service/app/server_test.go index afdc06fa33..49fc6191fb 100644 --- a/pkg/query-service/app/server_test.go +++ b/pkg/query-service/app/server_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO(remove): Implemented at pkg/http/middleware/timeout_test.go func TestGetRouteContextTimeout(t *testing.T) { var testGetRouteContextTimeoutData = []struct { Name string diff --git a/pkg/registry/doc.go b/pkg/registry/doc.go new file mode 100644 index 0000000000..ff2debbefe --- /dev/null +++ b/pkg/registry/doc.go @@ -0,0 +1,3 @@ +// package registry contains a simple implementation of https://github.com/google/guava/wiki/ServiceExplained +// Here the the "ServiceManager" is called the "Registry" +package registry diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go new file mode 100644 index 0000000000..850a4389a9 --- /dev/null +++ b/pkg/registry/registry.go @@ -0,0 +1,84 @@ +package registry + +import ( + "context" + "errors" + "fmt" + "os" + "os/signal" + "syscall" + + "go.uber.org/zap" +) + +type Registry struct { + services []NamedService + logger *zap.Logger + startCh chan error + stopCh chan error +} + +// New creates a new registry of services. It needs at least one service in the input. +func New(logger *zap.Logger, services ...NamedService) (*Registry, error) { + if logger == nil { + return nil, fmt.Errorf("cannot build registry, logger is required") + } + + if len(services) == 0 { + return nil, fmt.Errorf("cannot build registry, at least one service is required") + } + + return &Registry{ + logger: logger.Named("go.signoz.io/pkg/registry"), + services: services, + startCh: make(chan error, 1), + stopCh: make(chan error, len(services)), + }, nil +} + +func (r *Registry) Start(ctx context.Context) error { + for _, s := range r.services { + go func(s Service) { + err := s.Start(ctx) + r.startCh <- err + }(s) + } + + return nil +} + +func (r *Registry) Wait(ctx context.Context) error { + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM) + + select { + case <-ctx.Done(): + r.logger.Info("caught context error, exiting", zap.Any("context", ctx)) + case s := <-interrupt: + r.logger.Info("caught interrupt signal, exiting", zap.Any("context", ctx), zap.Any("signal", s)) + case err := <-r.startCh: + r.logger.Info("caught service error, exiting", zap.Any("context", ctx), zap.Error(err)) + return err + } + + return nil +} + +func (r *Registry) Stop(ctx context.Context) error { + for _, s := range r.services { + go func(s Service) { + err := s.Stop(ctx) + r.stopCh <- err + }(s) + } + + errs := make([]error, len(r.services)) + for i := 0; i < len(r.services); i++ { + err := <-r.stopCh + if err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) +} diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go new file mode 100644 index 0000000000..12ae1d8862 --- /dev/null +++ b/pkg/registry/registry_test.go @@ -0,0 +1,56 @@ +package registry + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestRegistryWith2HttpServers(t *testing.T) { + http1, err := newHttpService("http1") + require.NoError(t, err) + + http2, err := newHttpService("http2") + require.NoError(t, err) + + registry, err := New(zap.NewNop(), http1, http2) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + require.NoError(t, registry.Start(ctx)) + require.NoError(t, registry.Wait(ctx)) + require.NoError(t, registry.Stop(ctx)) + }() + cancel() + + wg.Wait() +} + +func TestRegistryWith2HttpServersWithoutWait(t *testing.T) { + http1, err := newHttpService("http1") + require.NoError(t, err) + + http2, err := newHttpService("http2") + require.NoError(t, err) + + registry, err := New(zap.NewNop(), http1, http2) + require.NoError(t, err) + + ctx := context.Background() + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + require.NoError(t, registry.Start(ctx)) + require.NoError(t, registry.Stop(ctx)) + }() + + wg.Wait() +} diff --git a/pkg/registry/service.go b/pkg/registry/service.go new file mode 100644 index 0000000000..38df4f0a4f --- /dev/null +++ b/pkg/registry/service.go @@ -0,0 +1,16 @@ +package registry + +import "context" + +type Service interface { + // Starts a service. The service should return an error if it cannot be started. + Start(context.Context) error + // Stops a service. + Stop(context.Context) error +} + +type NamedService interface { + // Identifier of a service. It should be unique across all services. + Name() string + Service +} diff --git a/pkg/registry/service_test.go b/pkg/registry/service_test.go new file mode 100644 index 0000000000..dc0621e962 --- /dev/null +++ b/pkg/registry/service_test.go @@ -0,0 +1,49 @@ +package registry + +import ( + "context" + "net" + "net/http" +) + +var _ NamedService = (*httpService)(nil) + +type httpService struct { + Listener net.Listener + Server *http.Server + name string +} + +func newHttpService(name string) (*httpService, error) { + return &httpService{ + name: name, + Server: &http.Server{}, + }, nil +} + +func (service *httpService) Name() string { + return service.name +} + +func (service *httpService) Start(ctx context.Context) error { + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + return err + } + service.Listener = listener + + if err := service.Server.Serve(service.Listener); err != nil { + if err != http.ErrServerClosed { + return err + } + } + return nil +} + +func (service *httpService) Stop(ctx context.Context) error { + if err := service.Server.Shutdown(ctx); err != nil { + return err + } + + return nil +} From c322fc72d9c71c08db93d7a96047da7ff07c954b Mon Sep 17 00:00:00 2001 From: Vibhu Pandey Date: Thu, 22 Aug 2024 15:19:32 +0530 Subject: [PATCH 20/60] feat(errors): add errors package (#5741) ### Summary Add errors package #### Related Issues / PR's https://github.com/SigNoz/signoz/pull/5710 --- pkg/errors/doc.go | 3 + pkg/errors/errors.go | 118 ++++++++++++++++++++++++++++++++++++++ pkg/errors/errors_test.go | 53 +++++++++++++++++ pkg/errors/type.go | 14 +++++ pkg/errors/type_test.go | 18 ++++++ 5 files changed, 206 insertions(+) create mode 100644 pkg/errors/doc.go create mode 100644 pkg/errors/errors.go create mode 100644 pkg/errors/errors_test.go create mode 100644 pkg/errors/type.go create mode 100644 pkg/errors/type_test.go diff --git a/pkg/errors/doc.go b/pkg/errors/doc.go new file mode 100644 index 0000000000..f9cd863c72 --- /dev/null +++ b/pkg/errors/doc.go @@ -0,0 +1,3 @@ +// package error contains error related utilities. Use this package when +// a well-defined error has to be shown. +package errors diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 0000000000..7bd3e1b97e --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,118 @@ +package errors + +import ( + "fmt" +) + +// base is the fundamental struct that implements the error interface. +// The order of the struct is 'TCMEUA'. +type base struct { + // t denotes the custom type of the error. + t typ + // c denotes the short code for the error message. + c string + // m contains error message passed through errors.New. + m string + // e is the actual error being wrapped. + e error + // u denotes the url for the documentation (if present) for the error. + u string + // a denotes any additional error messages (if present). + a []string +} + +// base implements Error interface. +func (b *base) Error() string { + if b.e != nil { + return b.e.Error() + } + + return fmt.Sprintf("%s(%s): %s", b.t.s, b.c, b.m) +} + +// New returns a base error. It requires type, code and message as input. +func New(t typ, code string, message string) *base { + return &base{ + t: t, + c: code, + m: message, + e: nil, + u: "", + a: []string{}, + } +} + +// Newf returns a new base by formatting the error message with the supplied format specifier. +func Newf(t typ, code string, format string, args ...interface{}) *base { + return &base{ + t: t, + c: code, + m: fmt.Sprintf(format, args...), + e: nil, + } +} + +// Wrapf returns a new error by formatting the error message with the supplied format specifier +// and wrapping another error with base. +func Wrapf(cause error, t typ, code string, format string, args ...interface{}) *base { + return &base{ + t: t, + c: code, + m: fmt.Sprintf(format, args...), + e: cause, + } +} + +// WithUrl adds a url to the base error and returns a new base error. +func (b *base) WithUrl(u string) *base { + return &base{ + t: b.t, + c: b.c, + m: b.m, + e: b.e, + u: u, + a: b.a, + } +} + +// WithUrl adds additional messages to the base error and returns a new base error. +func (b *base) WithAdditional(a ...string) *base { + return &base{ + t: b.t, + c: b.c, + m: b.m, + e: b.e, + u: b.u, + a: a, + } +} + +// Unwrapb is a combination of built-in errors.As and type casting. +// It finds the first error in cause that matches base, +// and if one is found, returns the individual fields of base. +// Otherwise, it returns TypeInternal, the original error string +// and the error itself. +// +//lint:ignore ST1008 we want to return arguments in the 'TCMEUA' order of the struct +func Unwrapb(cause error) (typ, string, string, error, string, []string) { + base, ok := cause.(*base) + if ok { + return base.t, base.c, base.m, base.e, base.u, base.a + } + + return TypeInternal, "", cause.Error(), cause, "", []string{} +} + +// Ast checks if the provided error matches the specified custom error type. +func Ast(cause error, typ typ) bool { + t, _, _, _, _, _ := Unwrapb(cause) + + return t == typ +} + +// Ast checks if the provided error matches the specified custom error code. +func Asc(cause error, code string) bool { + _, c, _, _, _, _ := Unwrapb(cause) + + return c == code +} diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go new file mode 100644 index 0000000000..4b04e876af --- /dev/null +++ b/pkg/errors/errors_test.go @@ -0,0 +1,53 @@ +package errors + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNew(t *testing.T) { + typ := typ{"test-error"} + err := New(typ, "code", "test error info") + assert.NotNil(t, err) +} + +func TestNewf(t *testing.T) { + typ := typ{"test-error"} + err := Newf(typ, "test-code", "test error info with %s", "string") + assert.NotNil(t, err) + assert.Equal(t, "test-error(test-code): test error info with string", err.Error()) +} + +func TestWrapf(t *testing.T) { + typ := typ{"test-error"} + err := Wrapf(errors.New("original error"), typ, "test-code", "info for err %d", 2) + assert.NotNil(t, err) +} + +func TestError(t *testing.T) { + typ := typ{"test-error"} + err1 := New(typ, "test-code", "info for err1") + assert.Equal(t, "test-error(test-code): info for err1", err1.Error()) + + err2 := Wrapf(err1, typ, "test-code", "info for err2") + assert.Equal(t, "test-error(test-code): info for err1", err2.Error()) +} + +func TestUnwrapb(t *testing.T) { + typ := typ{"test-error"} + oerr := errors.New("original error") + berr := Wrapf(oerr, typ, "test-code", "this is a base err").WithUrl("https://docs").WithAdditional("additional err") + + atyp, acode, amessage, aerr, au, aa := Unwrapb(berr) + assert.Equal(t, typ, atyp) + assert.Equal(t, "test-code", acode) + assert.Equal(t, "this is a base err", amessage) + assert.Equal(t, oerr, aerr) + assert.Equal(t, "https://docs", au) + assert.Equal(t, []string{"additional err"}, aa) + + atyp, _, _, _, _, _ = Unwrapb(oerr) + assert.Equal(t, TypeInternal, atyp) +} diff --git a/pkg/errors/type.go b/pkg/errors/type.go new file mode 100644 index 0000000000..6800a8bc71 --- /dev/null +++ b/pkg/errors/type.go @@ -0,0 +1,14 @@ +package errors + +var ( + TypeInvalidInput typ = typ{"invalid-input"} + TypeInternal = typ{"internal"} + TypeUnsupported = typ{"unsupported"} + TypeNotFound = typ{"not-found"} + TypeMethodNotAllowed = typ{"method-not-allowed"} + TypeAlreadyExists = typ{"already-exists"} + TypeUnauthenticated = typ{"unauthenticated"} +) + +// Defines custom error types +type typ struct{ s string } diff --git a/pkg/errors/type_test.go b/pkg/errors/type_test.go new file mode 100644 index 0000000000..7b7d3a26b3 --- /dev/null +++ b/pkg/errors/type_test.go @@ -0,0 +1,18 @@ +package errors + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestString(t *testing.T) { + typ := typ{"test-error"} + assert.Equal(t, typ.s, "test-error") +} + +func TestEquals(t *testing.T) { + typ1 := typ{"test-error"} + typ2 := typ{"test-error"} + assert.True(t, typ1 == typ2) +} From bfeceb0ed298c4c977707a847aba342e1e97bd80 Mon Sep 17 00:00:00 2001 From: Vibhu Pandey Date: Thu, 22 Aug 2024 20:56:15 +0530 Subject: [PATCH 21/60] feat(web): add web package (#5743) ### Summary Add a web package for serving frontend #### Related Issues / PR's https://github.com/SigNoz/signoz/pull/5710 --- pkg/config/config.go | 4 + pkg/config/config_test.go | 9 +- pkg/http/middleware/cache.go | 28 ++++++ pkg/http/middleware/cache_test.go | 56 +++++++++++ pkg/web/config.go | 29 ++++++ pkg/web/testdata/index.html | 1 + pkg/web/web.go | 94 ++++++++++++++++++ pkg/web/web_test.go | 159 ++++++++++++++++++++++++++++++ 8 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 pkg/http/middleware/cache.go create mode 100644 pkg/http/middleware/cache_test.go create mode 100644 pkg/web/config.go create mode 100644 pkg/web/testdata/index.html create mode 100644 pkg/web/web.go create mode 100644 pkg/web/web_test.go diff --git a/pkg/config/config.go b/pkg/config/config.go index 621cc073c6..02cc7c37c2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,11 +4,13 @@ import ( "context" "go.signoz.io/signoz/pkg/instrumentation" + "go.signoz.io/signoz/pkg/web" ) // Config defines the entire configuration of signoz. type Config struct { Instrumentation instrumentation.Config `mapstructure:"instrumentation"` + Web web.Config `mapstructure:"web"` } func New(ctx context.Context, settings ProviderSettings) (*Config, error) { @@ -24,6 +26,8 @@ func byName(name string) (any, bool) { switch name { case "instrumentation": return &instrumentation.Config{}, true + case "web": + return &web.Config{}, true default: return nil, false } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 04ede418f0..b3e3007bb4 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -10,12 +10,15 @@ import ( contribsdkconfig "go.opentelemetry.io/contrib/config" "go.signoz.io/signoz/pkg/confmap/provider/signozenvprovider" "go.signoz.io/signoz/pkg/instrumentation" + "go.signoz.io/signoz/pkg/web" ) func TestNewWithSignozEnvProvider(t *testing.T) { t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__ENABLED", "true") t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__PROCESSORS__BATCH__EXPORTER__OTLP__ENDPOINT", "0.0.0.0:4317") t.Setenv("SIGNOZ__INSTRUMENTATION__LOGS__PROCESSORS__BATCH__EXPORT_TIMEOUT", "10") + t.Setenv("SIGNOZ__WEB__PREFIX", "/web") + t.Setenv("SIGNOZ__WEB__DIRECTORY", "/build") config, err := New(context.Background(), ProviderSettings{ ResolverSettings: confmap.ResolverSettings{ @@ -34,7 +37,7 @@ func TestNewWithSignozEnvProvider(t *testing.T) { Enabled: true, LoggerProvider: contribsdkconfig.LoggerProvider{ Processors: []contribsdkconfig.LogRecordProcessor{ - contribsdkconfig.LogRecordProcessor{ + { Batch: &contribsdkconfig.BatchLogRecordProcessor{ ExportTimeout: &i, Exporter: contribsdkconfig.LogRecordExporter{ @@ -48,6 +51,10 @@ func TestNewWithSignozEnvProvider(t *testing.T) { }, }, }, + Web: web.Config{ + Prefix: "/web", + Directory: "/build", + }, } assert.Equal(t, expected, config) diff --git a/pkg/http/middleware/cache.go b/pkg/http/middleware/cache.go new file mode 100644 index 0000000000..ff66288354 --- /dev/null +++ b/pkg/http/middleware/cache.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "net/http" + "strconv" + "time" +) + +type Cache struct { + maxAge time.Duration +} + +func NewCache(maxAge time.Duration) *Cache { + if maxAge == 0 { + maxAge = 7 * 24 * time.Hour + } + + return &Cache{ + maxAge: maxAge, + } +} + +func (middleware *Cache) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("Cache-Control", "max-age="+strconv.Itoa(int(middleware.maxAge.Seconds()))) + next.ServeHTTP(rw, req) + }) +} diff --git a/pkg/http/middleware/cache_test.go b/pkg/http/middleware/cache_test.go new file mode 100644 index 0000000000..bef0e3b36b --- /dev/null +++ b/pkg/http/middleware/cache_test.go @@ -0,0 +1,56 @@ +package middleware + +import ( + "net" + "net/http" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestCache(t *testing.T) { + t.Parallel() + + age := 20 * 24 * time.Hour + m := NewCache(age) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + Handler: m.Wrap(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(204) + })), + } + + go func() { + require.NoError(t, server.Serve(listener)) + }() + + testCases := []struct { + name string + age time.Duration + }{ + { + name: "Success", + age: age, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest("GET", "http://"+listener.Addr().String(), nil) + require.NoError(t, err) + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + actual := res.Header.Get("Cache-control") + require.NoError(t, err) + + require.Equal(t, "max-age="+strconv.Itoa(int(age.Seconds())), string(actual)) + }) + } +} diff --git a/pkg/web/config.go b/pkg/web/config.go new file mode 100644 index 0000000000..a0e0c531de --- /dev/null +++ b/pkg/web/config.go @@ -0,0 +1,29 @@ +package web + +import ( + "go.signoz.io/signoz/pkg/confmap" +) + +// Config satisfies the confmap.Config interface +var _ confmap.Config = (*Config)(nil) + +// Config holds the configuration for web. +type Config struct { + // The prefix to serve the files from + Prefix string `mapstructure:"prefix"` + // The directory containing the static build files. The root of this directory should + // have an index.html file. + Directory string `mapstructure:"directory"` +} + +func (c *Config) NewWithDefaults() confmap.Config { + return &Config{ + Prefix: "/", + Directory: "/etc/signoz/web", + } + +} + +func (c *Config) Validate() error { + return nil +} diff --git a/pkg/web/testdata/index.html b/pkg/web/testdata/index.html new file mode 100644 index 0000000000..49c8c8383a --- /dev/null +++ b/pkg/web/testdata/index.html @@ -0,0 +1 @@ +

Welcome to test data!!!

\ No newline at end of file diff --git a/pkg/web/web.go b/pkg/web/web.go new file mode 100644 index 0000000000..2e29b000f9 --- /dev/null +++ b/pkg/web/web.go @@ -0,0 +1,94 @@ +package web + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gorilla/mux" + "go.signoz.io/signoz/pkg/http/middleware" + "go.uber.org/zap" +) + +var _ http.Handler = (*Web)(nil) + +const ( + indexFileName string = "index.html" +) + +type Web struct { + logger *zap.Logger + cfg Config +} + +func New(logger *zap.Logger, cfg Config) (*Web, error) { + if logger == nil { + return nil, fmt.Errorf("cannot build web, logger is required") + } + + fi, err := os.Stat(cfg.Directory) + if err != nil { + return nil, fmt.Errorf("cannot access web directory: %w", err) + } + + ok := fi.IsDir() + if !ok { + return nil, fmt.Errorf("web directory is not a directory") + } + + fi, err = os.Stat(filepath.Join(cfg.Directory, indexFileName)) + if err != nil { + return nil, fmt.Errorf("cannot access %q in web directory: %w", indexFileName, err) + } + + if os.IsNotExist(err) || fi.IsDir() { + return nil, fmt.Errorf("%q does not exist", indexFileName) + } + + return &Web{ + logger: logger.Named("go.signoz.io/pkg/web"), + cfg: cfg, + }, nil +} + +func (web *Web) AddToRouter(router *mux.Router) error { + cache := middleware.NewCache(7 * 24 * time.Hour) + err := router.PathPrefix(web.cfg.Prefix). + Handler( + http.StripPrefix( + web.cfg.Prefix, + cache.Wrap(http.HandlerFunc(web.ServeHTTP)), + ), + ).GetError() + if err != nil { + return fmt.Errorf("unable to add web to router: %w", err) + } + + return nil +} + +func (web *Web) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + // Join internally call path.Clean to prevent directory traversal + path := filepath.Join(web.cfg.Directory, req.URL.Path) + + // check whether a file exists or is a directory at the given path + fi, err := os.Stat(path) + if os.IsNotExist(err) || fi.IsDir() { + // file does not exist or path is a directory, serve index.html + http.ServeFile(rw, req, filepath.Join(web.cfg.Directory, indexFileName)) + return + } + + if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + // TODO: Put down a crash html page here + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + // otherwise, use http.FileServer to serve the static file + http.FileServer(http.Dir(web.cfg.Directory)).ServeHTTP(rw, req) +} diff --git a/pkg/web/web_test.go b/pkg/web/web_test.go new file mode 100644 index 0000000000..d5111cf747 --- /dev/null +++ b/pkg/web/web_test.go @@ -0,0 +1,159 @@ +package web + +import ( + "io" + "net" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestServeHttpWithoutPrefix(t *testing.T) { + t.Parallel() + fi, err := os.Open(filepath.Join("testdata", indexFileName)) + require.NoError(t, err) + + expected, err := io.ReadAll(fi) + require.NoError(t, err) + + web, err := New(zap.NewNop(), Config{Prefix: "/", Directory: filepath.Join("testdata")}) + require.NoError(t, err) + + router := mux.NewRouter() + err = web.AddToRouter(router) + require.NoError(t, err) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + Handler: router, + } + + go func() { + _ = server.Serve(listener) + }() + defer func() { + _ = server.Close() + }() + + testCases := []struct { + name string + path string + }{ + { + name: "Root", + path: "/", + }, + { + name: "Index", + path: "/" + indexFileName, + }, + { + name: "DoesNotExist", + path: "/does-not-exist", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path) + require.NoError(t, err) + + defer func() { + _ = res.Body.Close() + }() + + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, expected, actual) + }) + } + +} + +func TestServeHttpWithPrefix(t *testing.T) { + t.Parallel() + fi, err := os.Open(filepath.Join("testdata", indexFileName)) + require.NoError(t, err) + + expected, err := io.ReadAll(fi) + require.NoError(t, err) + + web, err := New(zap.NewNop(), Config{Prefix: "/web", Directory: filepath.Join("testdata")}) + require.NoError(t, err) + + router := mux.NewRouter() + err = web.AddToRouter(router) + require.NoError(t, err) + + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + server := &http.Server{ + Handler: router, + } + + go func() { + _ = server.Serve(listener) + }() + defer func() { + _ = server.Close() + }() + + testCases := []struct { + name string + path string + found bool + }{ + { + name: "Root", + path: "/web", + found: true, + }, + { + name: "Index", + path: "/web/" + indexFileName, + found: true, + }, + { + name: "FileDoesNotExist", + path: "/web/does-not-exist", + found: true, + }, + { + name: "DoesNotExist", + path: "/does-not-exist", + found: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path) + require.NoError(t, err) + + defer func() { + _ = res.Body.Close() + }() + + if tc.found { + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, expected, actual) + } else { + assert.Equal(t, http.StatusNotFound, res.StatusCode) + } + + }) + } + +} From 96b81817e03fe8fb3c1abac6f32e82f7e1d355ba Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Thu, 22 Aug 2024 23:56:51 +0530 Subject: [PATCH 22/60] feat: add support for changing the font size in logs (#5739) * feat: add support for changing the font size in logs * fix: build issues and logs context * chore: fix build issues * feat: scale all the spaces * chore: handle light mode designs * feat: set small as the default --- .../components/Logs/AddToQueryHOC.styles.scss | 13 + .../src/components/Logs/AddToQueryHOC.tsx | 6 +- .../Logs/ListLogView/ListLogView.styles.scss | 69 ++++ .../src/components/Logs/ListLogView/index.tsx | 43 ++- .../src/components/Logs/ListLogView/styles.ts | 33 +- .../LogStateIndicator.styles.scss | 15 +- .../LogStateIndicator.test.tsx | 15 +- .../LogStateIndicator/LogStateIndicator.tsx | 5 +- .../src/components/Logs/RawLogView/index.tsx | 3 + .../src/components/Logs/RawLogView/styles.ts | 16 +- .../src/components/Logs/RawLogView/types.ts | 3 + .../src/components/Logs/TableView/styles.ts | 9 + .../src/components/Logs/TableView/types.ts | 2 + .../Logs/TableView/useTableView.styles.scss | 33 ++ .../Logs/TableView/useTableView.tsx | 12 +- .../LogsFormatOptionsMenu.styles.scss | 153 +++++++- .../LogsFormatOptionsMenu.tsx | 336 +++++++++++------- .../container/LiveLogs/LiveLogsList/index.tsx | 4 + .../ContextView/ContextLogRenderer.tsx | 41 ++- .../ContextView/useContextLogData.ts | 23 +- .../container/LogDetailedView/LogContext.tsx | 2 + .../container/LogDetailedView/TableView.tsx | 3 +- .../src/container/LogsContextList/configs.ts | 1 + .../src/container/LogsContextList/index.tsx | 2 + .../container/LogsExplorerContext/index.tsx | 2 + .../InfinityTableView/TableRow.tsx | 4 + .../InfinityTableView/config.ts | 15 + .../InfinityTableView/index.tsx | 15 +- .../InfinityTableView/styles.ts | 23 +- .../src/container/LogsExplorerList/index.tsx | 21 +- frontend/src/container/LogsTable/index.tsx | 4 + .../src/container/OptionsMenu/constants.ts | 3 +- frontend/src/container/OptionsMenu/types.ts | 12 + .../container/OptionsMenu/useOptionsMenu.ts | 34 +- 34 files changed, 797 insertions(+), 178 deletions(-) diff --git a/frontend/src/components/Logs/AddToQueryHOC.styles.scss b/frontend/src/components/Logs/AddToQueryHOC.styles.scss index 42baabd02a..b65c3cb17e 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.styles.scss +++ b/frontend/src/components/Logs/AddToQueryHOC.styles.scss @@ -1,3 +1,16 @@ .addToQueryContainer { cursor: pointer; + display: flex; + align-items: center; + &.small { + height: 16px; + } + + &.medium { + height: 20px; + } + + &.large { + height: 24px; + } } diff --git a/frontend/src/components/Logs/AddToQueryHOC.tsx b/frontend/src/components/Logs/AddToQueryHOC.tsx index 8391a23b81..df222b7552 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.tsx +++ b/frontend/src/components/Logs/AddToQueryHOC.tsx @@ -1,13 +1,16 @@ import './AddToQueryHOC.styles.scss'; import { Popover } from 'antd'; +import cx from 'classnames'; import { OPERATORS } from 'constants/queryBuilder'; +import { FontSize } from 'container/OptionsMenu/types'; import { memo, MouseEvent, ReactNode, useMemo } from 'react'; function AddToQueryHOC({ fieldKey, fieldValue, onAddToQuery, + fontSize, children, }: AddToQueryHOCProps): JSX.Element { const handleQueryAdd = (event: MouseEvent): void => { @@ -21,7 +24,7 @@ function AddToQueryHOC({ return ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -
+
{children} @@ -33,6 +36,7 @@ export interface AddToQueryHOCProps { fieldKey: string; fieldValue: string; onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void; + fontSize: FontSize; children: ReactNode; } diff --git a/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss b/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss index 3caf6a3282..21dcf171ce 100644 --- a/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss +++ b/frontend/src/components/Logs/ListLogView/ListLogView.styles.scss @@ -6,6 +6,21 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .log-value { color: var(--text-vanilla-400, #c0c1c3); @@ -14,6 +29,21 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .log-line { display: flex; @@ -40,6 +70,20 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .selected-log-value { @@ -52,12 +96,37 @@ line-height: 18px; letter-spacing: -0.07px; font-size: 14px; + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .selected-log-kv { min-height: 24px; display: flex; align-items: center; + &.small { + min-height: 16px; + } + + &.medium { + min-height: 20px; + } + + &.large { + min-height: 24px; + } } } diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index fa8a2fb608..d1461b0caa 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -3,8 +3,10 @@ import './ListLogView.styles.scss'; import { blue } from '@ant-design/colors'; import Convert from 'ansi-to-html'; import { Typography } from 'antd'; +import cx from 'classnames'; import LogDetail from 'components/LogDetail'; import { VIEW_TYPES } from 'components/LogDetail/constants'; +import { FontSize } from 'container/OptionsMenu/types'; import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useActiveLog } from 'hooks/logs/useActiveLog'; @@ -39,6 +41,7 @@ interface LogFieldProps { fieldKey: string; fieldValue: string; linesPerRow?: number; + fontSize: FontSize; } type LogSelectedFieldProps = Omit & @@ -48,6 +51,7 @@ function LogGeneralField({ fieldKey, fieldValue, linesPerRow = 1, + fontSize, }: LogFieldProps): JSX.Element { const html = useMemo( () => ({ @@ -62,12 +66,12 @@ function LogGeneralField({ return ( - + {`${fieldKey} : `} 1 ? linesPerRow : undefined} /> @@ -78,6 +82,7 @@ function LogSelectedField({ fieldKey = '', fieldValue = '', onAddToQuery, + fontSize, }: LogSelectedFieldProps): JSX.Element { return (
@@ -85,16 +90,22 @@ function LogSelectedField({ fieldKey={fieldKey} fieldValue={fieldValue} onAddToQuery={onAddToQuery} + fontSize={fontSize} > - + {fieldKey} - - {': '} - {fieldValue || "''"} + + {': '} + + {fieldValue || "''"} +
); @@ -107,6 +118,7 @@ type ListLogViewProps = { onAddToQuery: AddToQueryHOCProps['onAddToQuery']; activeLog?: ILog | null; linesPerRow: number; + fontSize: FontSize; }; function ListLogView({ @@ -116,6 +128,7 @@ function ListLogView({ onAddToQuery, activeLog, linesPerRow, + fontSize, }: ListLogViewProps): JSX.Element { const flattenLogData = useMemo(() => FlatLogData(logData), [logData]); @@ -185,6 +198,7 @@ function ListLogView({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} onClick={handleDetailedView} + fontSize={fontSize} >
- + {flattenLogData.stream && ( - + )} - + {updatedSelecedFields.map((field) => isValidLogField(flattenLogData[field.name] as never) ? ( @@ -212,6 +236,7 @@ function ListLogView({ fieldKey={field.name} fieldValue={flattenLogData[field.name] as never} onAddToQuery={onAddToQuery} + fontSize={fontSize} /> ) : null, )} diff --git a/frontend/src/components/Logs/ListLogView/styles.ts b/frontend/src/components/Logs/ListLogView/styles.ts index 52cc2b20d4..d2a6342c77 100644 --- a/frontend/src/components/Logs/ListLogView/styles.ts +++ b/frontend/src/components/Logs/ListLogView/styles.ts @@ -1,21 +1,46 @@ +/* eslint-disable no-nested-ternary */ import { Color } from '@signozhq/design-tokens'; import { Card, Typography } from 'antd'; +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; interface LogTextProps { linesPerRow?: number; } +interface LogContainerProps { + fontSize: FontSize; +} + export const Container = styled(Card)<{ $isActiveLog: boolean; $isDarkMode: boolean; + fontSize: FontSize; }>` width: 100% !important; margin-bottom: 0.3rem; + + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `margin-bottom:0.1rem;` + : fontSize === FontSize.MEDIUM + ? `margin-bottom: 0.2rem;` + : fontSize === FontSize.LARGE + ? `margin-bottom:0.3rem;` + : ``} cursor: pointer; .ant-card-body { padding: 0.3rem 0.6rem; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `padding:0.1rem 0.6rem;` + : fontSize === FontSize.MEDIUM + ? `padding: 0.2rem 0.6rem;` + : fontSize === FontSize.LARGE + ? `padding:0.3rem 0.6rem;` + : ``} + ${({ $isActiveLog, $isDarkMode }): string => $isActiveLog ? `background-color: ${ @@ -38,11 +63,17 @@ export const TextContainer = styled.div` width: 100%; `; -export const LogContainer = styled.div` +export const LogContainer = styled.div` margin-left: 0.5rem; display: flex; flex-direction: column; gap: 6px; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `gap: 2px;` + : fontSize === FontSize.MEDIUM + ? ` gap:4px;` + : `gap:6px;`} `; export const LogText = styled.div` diff --git a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss index a00c7f6761..61870abc71 100644 --- a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss +++ b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.styles.scss @@ -9,11 +9,24 @@ border-radius: 50px; background-color: transparent; + &.small { + min-height: 16px; + } + + &.medium { + min-height: 20px; + } + + &.large { + min-height: 24px; + } + &.INFO { background-color: var(--bg-slate-400); } - &.WARNING, &.WARN { + &.WARNING, + &.WARN { background-color: var(--bg-amber-500); } diff --git a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx index d924c27426..06cc9d3ec4 100644 --- a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx +++ b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.test.tsx @@ -1,10 +1,13 @@ import { render } from '@testing-library/react'; +import { FontSize } from 'container/OptionsMenu/types'; import LogStateIndicator from './LogStateIndicator'; describe('LogStateIndicator', () => { it('renders correctly with default props', () => { - const { container } = render(); + const { container } = render( + , + ); const indicator = container.firstChild as HTMLElement; expect(indicator.classList.contains('log-state-indicator')).toBe(true); expect(indicator.classList.contains('isActive')).toBe(false); @@ -15,28 +18,30 @@ describe('LogStateIndicator', () => { }); it('renders correctly when isActive is true', () => { - const { container } = render(); + const { container } = render( + , + ); const indicator = container.firstChild as HTMLElement; expect(indicator.classList.contains('isActive')).toBe(true); }); it('renders correctly with different types', () => { const { container: containerInfo } = render( - , + , ); expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe( true, ); const { container: containerWarning } = render( - , + , ); expect( containerWarning.querySelector('.line')?.classList.contains('WARNING'), ).toBe(true); const { container: containerError } = render( - , + , ); expect( containerError.querySelector('.line')?.classList.contains('ERROR'), diff --git a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx index ebad7bd116..b9afa5b7a2 100644 --- a/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx +++ b/frontend/src/components/Logs/LogStateIndicator/LogStateIndicator.tsx @@ -1,6 +1,7 @@ import './LogStateIndicator.styles.scss'; import cx from 'classnames'; +import { FontSize } from 'container/OptionsMenu/types'; export const SEVERITY_TEXT_TYPE = { TRACE: 'TRACE', @@ -44,13 +45,15 @@ export const LogType = { function LogStateIndicator({ type, isActive, + fontSize, }: { type: string; + fontSize: FontSize; isActive?: boolean; }): JSX.Element { return (
-
+
); } diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index d1ae19fe99..b4b3eb7783 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -39,6 +39,7 @@ function RawLogView({ linesPerRow, isTextOverflowEllipsisDisabled, selectedFields = [], + fontSize, }: RawLogViewProps): JSX.Element { const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink( data.id, @@ -168,6 +169,7 @@ function RawLogView({ activeContextLog?.id === data.id || isActiveLog } + fontSize={fontSize} /> diff --git a/frontend/src/components/Logs/RawLogView/styles.ts b/frontend/src/components/Logs/RawLogView/styles.ts index d86de435c2..35b853d929 100644 --- a/frontend/src/components/Logs/RawLogView/styles.ts +++ b/frontend/src/components/Logs/RawLogView/styles.ts @@ -1,6 +1,8 @@ +/* eslint-disable no-nested-ternary */ import { blue } from '@ant-design/colors'; import { Color } from '@signozhq/design-tokens'; import { Col, Row, Space } from 'antd'; +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs'; @@ -48,10 +50,15 @@ export const ExpandIconWrapper = styled(Col)` export const RawLogContent = styled.div` margin-bottom: 0; + display: flex !important; + align-items: center; font-family: 'SF Mono', monospace; font-family: 'Geist Mono'; font-size: 13px; font-weight: 400; + line-height: 24px; + letter-spacing: -0.07px; + padding: 4px; text-align: left; color: ${({ $isDarkMode }): string => $isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}; @@ -66,9 +73,12 @@ export const RawLogContent = styled.div` line-clamp: ${linesPerRow}; -webkit-box-orient: vertical;`}; - line-height: 24px; - letter-spacing: -0.07px; - padding: 4px; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `font-size:11px; line-height:16px; padding:1px;` + : fontSize === FontSize.MEDIUM + ? `font-size:13px; line-height:20px; padding:1px;` + : `font-size:14px; line-height:24px; padding:2px;`} cursor: ${({ $isActiveLog, $isReadOnly }): string => $isActiveLog || $isReadOnly ? 'initial' : 'pointer'}; diff --git a/frontend/src/components/Logs/RawLogView/types.ts b/frontend/src/components/Logs/RawLogView/types.ts index a9c85c2ad6..ed73725dcc 100644 --- a/frontend/src/components/Logs/RawLogView/types.ts +++ b/frontend/src/components/Logs/RawLogView/types.ts @@ -1,3 +1,4 @@ +import { FontSize } from 'container/OptionsMenu/types'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; @@ -7,11 +8,13 @@ export interface RawLogViewProps { isTextOverflowEllipsisDisabled?: boolean; data: ILog; linesPerRow: number; + fontSize: FontSize; selectedFields?: IField[]; } export interface RawLogContentProps { linesPerRow: number; + fontSize: FontSize; $isReadOnly?: boolean; $isActiveLog?: boolean; $isDarkMode?: boolean; diff --git a/frontend/src/components/Logs/TableView/styles.ts b/frontend/src/components/Logs/TableView/styles.ts index 9213021971..a79db04a76 100644 --- a/frontend/src/components/Logs/TableView/styles.ts +++ b/frontend/src/components/Logs/TableView/styles.ts @@ -1,7 +1,10 @@ +/* eslint-disable no-nested-ternary */ +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; interface TableBodyContentProps { linesPerRow: number; + fontSize: FontSize; isDarkMode?: boolean; } @@ -20,4 +23,10 @@ export const TableBodyContent = styled.div` -webkit-line-clamp: ${(props): number => props.linesPerRow}; line-clamp: ${(props): number => props.linesPerRow}; -webkit-box-orient: vertical; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `font-size:11px; line-height:16px;` + : fontSize === FontSize.MEDIUM + ? `font-size:13px; line-height:20px;` + : `font-size:14px; line-height:24px;`} `; diff --git a/frontend/src/components/Logs/TableView/types.ts b/frontend/src/components/Logs/TableView/types.ts index 36a796ac0f..b2d3670dd8 100644 --- a/frontend/src/components/Logs/TableView/types.ts +++ b/frontend/src/components/Logs/TableView/types.ts @@ -1,4 +1,5 @@ import { ColumnsType, ColumnType } from 'antd/es/table'; +import { FontSize } from 'container/OptionsMenu/types'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; @@ -10,6 +11,7 @@ export type LogsTableViewProps = { logs: ILog[]; fields: IField[]; linesPerRow: number; + fontSize: FontSize; onClickExpand?: (log: ILog) => void; }; diff --git a/frontend/src/components/Logs/TableView/useTableView.styles.scss b/frontend/src/components/Logs/TableView/useTableView.styles.scss index 3723ecc705..9592d0ae12 100644 --- a/frontend/src/components/Logs/TableView/useTableView.styles.scss +++ b/frontend/src/components/Logs/TableView/useTableView.styles.scss @@ -5,6 +5,21 @@ font-weight: 400; line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; + + &.small { + font-size: 11px; + line-height: 16px; + } + + &.medium { + font-size: 13px; + line-height: 20px; + } + + &.large { + font-size: 14px; + line-height: 24px; + } } .table-timestamp { @@ -25,3 +40,21 @@ color: var(--bg-slate-400); } } + +.paragraph { + padding: 0px !important; + &.small { + font-size: 11px !important; + line-height: 16px !important; + } + + &.medium { + font-size: 13px !important; + line-height: 20px !important; + } + + &.large { + font-size: 14px !important; + line-height: 24px !important; + } +} diff --git a/frontend/src/components/Logs/TableView/useTableView.tsx b/frontend/src/components/Logs/TableView/useTableView.tsx index fd37132110..3a3ad54e3b 100644 --- a/frontend/src/components/Logs/TableView/useTableView.tsx +++ b/frontend/src/components/Logs/TableView/useTableView.tsx @@ -3,6 +3,7 @@ import './useTableView.styles.scss'; import Convert from 'ansi-to-html'; import { Typography } from 'antd'; import { ColumnsType } from 'antd/es/table'; +import cx from 'classnames'; import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -31,6 +32,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { logs, fields, linesPerRow, + fontSize, appendTo = 'center', activeContextLog, activeLog, @@ -57,7 +59,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { : getDefaultCellStyle(isDarkMode), }, children: ( - + {field} ), @@ -87,8 +92,9 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { isActive={ activeLog?.id === item.id || activeContextLog?.id === item.id } + fontSize={fontSize} /> - + {date}
@@ -114,6 +120,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { }), ), }} + fontSize={fontSize} linesPerRow={linesPerRow} isDarkMode={isDarkMode} /> @@ -130,6 +137,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => { linesPerRow, activeLog?.id, activeContextLog?.id, + fontSize, ]); return { columns, dataSource: flattenLogData }; diff --git a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss index af325a2d25..070d440781 100644 --- a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss +++ b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss @@ -17,17 +17,126 @@ box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); backdrop-filter: blur(20px); + .font-size-dropdown { + display: flex; + flex-direction: column; + + .back-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 12px; + border: none !important; + box-shadow: none !important; + + .icon { + flex-shrink: 0; + } + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: 0.14px; + } + } + + .back-btn:hover { + background-color: unset !important; + } + + .content { + display: flex; + flex-direction: column; + .option-btn { + display: flex; + align-items: center; + padding: 12px; + border: none !important; + box-shadow: none !important; + justify-content: space-between; + + .icon { + flex-shrink: 0; + } + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: normal; /* 142.857% */ + letter-spacing: 0.14px; + text-transform: capitalize; + } + + .text:hover { + color: var(--bg-vanilla-300); + } + } + + .option-btn:hover { + background-color: unset !important; + } + } + } + + .font-size-container { + padding: 12px; + display: flex; + flex-direction: column; + gap: 12px; + + .title { + color: var(--bg-slate-50); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + } + + .value { + display: flex; + height: 20px; + padding: 4px 0px; + justify-content: space-between; + align-items: center; + border: none !important; + .font-value { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + text-transform: capitalize; + } + .icon { + } + } + + .value:hover { + background-color: unset !important; + } + } + .menu-container { padding: 12px; .title { font-family: Inter; font-size: 11px; - font-weight: 600; + font-weight: 500; line-height: 18px; letter-spacing: 0.08em; text-align: left; - color: #52575c; + color: var(--bg-slate-50); } .menu-items { @@ -65,11 +174,11 @@ padding: 12px; .title { - color: #52575c; + color: var(--bg-slate-50); font-family: Inter; font-size: 11px; font-style: normal; - font-weight: 600; + font-weight: 500; line-height: 18px; /* 163.636% */ letter-spacing: 0.88px; text-transform: uppercase; @@ -149,11 +258,11 @@ } .title { - color: #52575c; + color: var(--bg-slate-50); font-family: Inter; font-size: 11px; font-style: normal; - font-weight: 600; + font-weight: 500; line-height: 18px; /* 163.636% */ letter-spacing: 0.88px; text-transform: uppercase; @@ -299,6 +408,38 @@ box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2); + .font-size-dropdown { + .back-btn { + .text { + color: var(--bg-ink-400); + } + } + + .content { + .option-btn { + .text { + color: var(--bg-ink-400); + } + + .text:hover { + color: var(--bg-ink-300); + } + } + } + } + + .font-size-container { + .title { + color: var(--bg-ink-100); + } + + .value { + .font-value { + color: var(--bg-ink-400); + } + } + } + .horizontal-line { background: var(--bg-vanilla-300); } diff --git a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx index 3a42e9a0b0..527c77c6af 100644 --- a/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx +++ b/frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx @@ -3,12 +3,12 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ import './LogsFormatOptionsMenu.styles.scss'; -import { Divider, Input, InputNumber, Tooltip } from 'antd'; +import { Button, Divider, Input, InputNumber, Tooltip, Typography } from 'antd'; import cx from 'classnames'; import { LogViewMode } from 'container/LogsTable'; -import { OptionsMenuConfig } from 'container/OptionsMenu/types'; +import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types'; import useDebouncedFn from 'hooks/useDebouncedFunction'; -import { Check, Minus, Plus, X } from 'lucide-react'; +import { Check, ChevronLeft, ChevronRight, Minus, Plus, X } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; interface LogsFormatOptionsMenuProps { @@ -24,10 +24,16 @@ export default function LogsFormatOptionsMenu({ selectedOptionFormat, config, }: LogsFormatOptionsMenuProps): JSX.Element { - const { maxLines, format, addColumn } = config; + const { maxLines, format, addColumn, fontSize } = config; const [selectedItem, setSelectedItem] = useState(selectedOptionFormat); const maxLinesNumber = (maxLines?.value as number) || 1; const [maxLinesPerRow, setMaxLinesPerRow] = useState(maxLinesNumber); + const [fontSizeValue, setFontSizeValue] = useState( + fontSize?.value || FontSize.SMALL, + ); + const [isFontSizeOptionsOpen, setIsFontSizeOptionsOpen] = useState( + false, + ); const [addNewColumn, setAddNewColumn] = useState(false); @@ -88,6 +94,12 @@ export default function LogsFormatOptionsMenu({ } }, [maxLinesPerRow]); + useEffect(() => { + if (fontSizeValue && config && config.fontSize?.onChange) { + config.fontSize.onChange(fontSizeValue); + } + }, [fontSizeValue]); + return (
-
-
{title}
- -
- {items.map( - (item: any): JSX.Element => ( -
handleMenuItemClick(item.key)} - > -
- {item.label} - - {selectedItem === item.key && } -
-
- ), - )} + {isFontSizeOptionsOpen ? ( +
+ +
+
+ + + +
-
- - {selectedItem && ( + ) : ( <> - <> -
-
-
max lines per row
-
- - - -
-
- - -
- {!addNewColumn &&
} - - {addNewColumn && ( -
-
- {' '} - columns - {' '} -
+
+
Font Size
+ +
+
+
+
{title}
- -
- )} +
+ {items.map( + (item: any): JSX.Element => ( +
handleMenuItemClick(item.key)} + > +
+ {item.label} -
- {!addNewColumn && ( -
- columns - {' '} -
+ {selectedItem === item.key && } +
+
+ ), )} +
+
-
- {addColumn?.value?.map(({ key, id }) => ( -
-
- - {key} - -
- addColumn.onRemove(id as string)} + {selectedItem && ( + <> + <> +
+
+
max lines per row
+
+ + +
- ))} -
+
+ - {addColumn?.isFetching && ( -
Loading ...
- )} +
+ {!addNewColumn &&
} + + {addNewColumn && ( +
+
+ {' '} + columns + {' '} +
- {addNewColumn && - addColumn && - addColumn.value.length > 0 && - addColumn.options && - addColumn?.options?.length > 0 && ( - + +
)} - {addNewColumn && ( -
- {addColumn?.options?.map(({ label, value }) => ( -
{ - eve.stopPropagation(); - - if (addColumn && addColumn?.onSelect) { - addColumn?.onSelect(value, { label, disabled: false }); - } - }} - > -
- - {label} - +
+ {!addNewColumn && ( +
+ columns + {' '} +
+ )} + +
+ {addColumn?.value?.map(({ key, id }) => ( +
+
+ + {key} + +
+ addColumn.onRemove(id as string)} + />
+ ))} +
+ + {addColumn?.isFetching && ( +
Loading ...
+ )} + + {addNewColumn && + addColumn && + addColumn.value.length > 0 && + addColumn.options && + addColumn?.options?.length > 0 && ( + + )} + + {addNewColumn && ( +
+ {addColumn?.options?.map(({ label, value }) => ( +
{ + eve.stopPropagation(); + + if (addColumn && addColumn?.onSelect) { + addColumn?.onSelect(value, { label, disabled: false }); + } + }} + > +
+ + {label} + +
+
+ ))}
- ))} + )}
- )} -
-
+
+ + )} )}
diff --git a/frontend/src/container/LiveLogs/LiveLogsList/index.tsx b/frontend/src/container/LiveLogs/LiveLogsList/index.tsx index f872b55deb..50beda2953 100644 --- a/frontend/src/container/LiveLogs/LiveLogsList/index.tsx +++ b/frontend/src/container/LiveLogs/LiveLogsList/index.tsx @@ -63,6 +63,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { data={log} linesPerRow={options.maxLines} selectedFields={selectedFields} + fontSize={options.fontSize} /> ); } @@ -75,12 +76,14 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { linesPerRow={options.maxLines} onAddToQuery={onAddToQuery} onSetActiveLog={onSetActiveLog} + fontSize={options.fontSize} /> ); }, [ onAddToQuery, onSetActiveLog, + options.fontSize, options.format, options.maxLines, selectedFields, @@ -123,6 +126,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { logs, fields: selectedFields, linesPerRow: options.maxLines, + fontSize: options.fontSize, appendTo: 'end', activeLogIndex, }} diff --git a/frontend/src/container/LogDetailedView/ContextView/ContextLogRenderer.tsx b/frontend/src/container/LogDetailedView/ContextView/ContextLogRenderer.tsx index d5c9f68547..39b55d21a0 100644 --- a/frontend/src/container/LogDetailedView/ContextView/ContextLogRenderer.tsx +++ b/frontend/src/container/LogDetailedView/ContextView/ContextLogRenderer.tsx @@ -3,12 +3,17 @@ import './ContextLogRenderer.styles.scss'; import { Skeleton } from 'antd'; import RawLogView from 'components/Logs/RawLogView'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; +import { LOCALSTORAGE } from 'constants/localStorage'; import ShowButton from 'container/LogsContextList/ShowButton'; +import { useOptionsMenu } from 'container/OptionsMenu'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; -import { useCallback, useEffect, useState } from 'react'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Virtuoso } from 'react-virtuoso'; import { ILog } from 'types/api/logs/log'; import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource, StringOperators } from 'types/common/queryBuilder'; import { useContextLogData } from './useContextLogData'; @@ -22,6 +27,20 @@ function ContextLogRenderer({ const [afterLogPage, setAfterLogPage] = useState(1); const [logs, setLogs] = useState([log]); + const { initialDataSource, stagedQuery } = useQueryBuilder(); + + const listQuery = useMemo(() => { + if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null; + + return stagedQuery.builder.queryData.find((item) => !item.disabled) || null; + }, [stagedQuery]); + + const { options } = useOptionsMenu({ + storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS, + dataSource: initialDataSource || DataSource.METRICS, + aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP, + }); + const { logs: previousLogs, isFetching: isPreviousLogsFetching, @@ -34,6 +53,7 @@ function ContextLogRenderer({ order: ORDERBY_FILTERS.ASC, page: prevLogPage, setPage: setPrevLogPage, + fontSize: options.fontSize, }); const { @@ -48,6 +68,7 @@ function ContextLogRenderer({ order: ORDERBY_FILTERS.DESC, page: afterLogPage, setPage: setAfterLogPage, + fontSize: options.fontSize, }); useEffect(() => { @@ -65,6 +86,19 @@ function ContextLogRenderer({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [filters]); + const lengthMultipier = useMemo(() => { + switch (options.fontSize) { + case FontSize.SMALL: + return 24; + case FontSize.MEDIUM: + return 28; + case FontSize.LARGE: + return 32; + default: + return 32; + } + }, [options.fontSize]); + const getItemContent = useCallback( (_: number, logTorender: ILog): JSX.Element => ( ), - [log.id], + [log.id, options.fontSize], ); return ( @@ -101,7 +136,7 @@ function ContextLogRenderer({ initialTopMostItemIndex={0} data={logs} itemContent={getItemContent} - style={{ height: `calc(${logs.length} * 32px)` }} + style={{ height: `calc(${logs.length} * ${lengthMultipier}px)` }} /> {isAfterLogsFetching && ( diff --git a/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts b/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts index 91c7fdf3f8..3d07ea0af9 100644 --- a/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts +++ b/frontend/src/container/LogDetailedView/ContextView/useContextLogData.ts @@ -4,9 +4,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import { getOrderByTimestamp, INITIAL_PAGE_SIZE, + INITIAL_PAGE_SIZE_SMALL_FONT, LOGS_MORE_PAGE_SIZE, } from 'container/LogsContextList/configs'; import { getRequestData } from 'container/LogsContextList/utils'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { @@ -30,6 +32,7 @@ export const useContextLogData = ({ filters, page, setPage, + fontSize, }: { log: ILog; query: Query; @@ -38,6 +41,7 @@ export const useContextLogData = ({ filters: TagFilter | null; page: number; setPage: Dispatch>; + fontSize?: FontSize; }): { logs: ILog[]; handleShowNextLines: () => void; @@ -54,9 +58,14 @@ export const useContextLogData = ({ const logsMorePageSize = useMemo(() => (page - 1) * LOGS_MORE_PAGE_SIZE, [ page, ]); + + const initialPageSize = + fontSize && fontSize === FontSize.SMALL + ? INITIAL_PAGE_SIZE_SMALL_FONT + : INITIAL_PAGE_SIZE; const pageSize = useMemo( - () => (page <= 1 ? INITIAL_PAGE_SIZE : logsMorePageSize + INITIAL_PAGE_SIZE), - [page, logsMorePageSize], + () => (page <= 1 ? initialPageSize : logsMorePageSize + initialPageSize), + [page, initialPageSize, logsMorePageSize], ); const isDisabledFetch = useMemo(() => logs.length < pageSize, [ logs.length, @@ -77,8 +86,16 @@ export const useContextLogData = ({ log: lastLog, orderByTimestamp, page, + pageSize: initialPageSize, }), - [currentStagedQueryData, query, lastLog, orderByTimestamp, page], + [ + currentStagedQueryData, + query, + lastLog, + orderByTimestamp, + page, + initialPageSize, + ], ); const [requestData, setRequestData] = useState( diff --git a/frontend/src/container/LogDetailedView/LogContext.tsx b/frontend/src/container/LogDetailedView/LogContext.tsx index 90d4a6e5bb..6997c65171 100644 --- a/frontend/src/container/LogDetailedView/LogContext.tsx +++ b/frontend/src/container/LogDetailedView/LogContext.tsx @@ -2,6 +2,7 @@ import './LogContext.styles.scss'; import RawLogView from 'components/Logs/RawLogView'; import LogsContextList from 'container/LogsContextList'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import { ILog } from 'types/api/logs/log'; import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData'; @@ -37,6 +38,7 @@ function LogContext({ isTextOverflowEllipsisDisabled={false} data={log} linesPerRow={1} + fontSize={FontSize.SMALL} /> {renderedField} diff --git a/frontend/src/container/LogsContextList/configs.ts b/frontend/src/container/LogsContextList/configs.ts index baa3b39420..9b70dfc5be 100644 --- a/frontend/src/container/LogsContextList/configs.ts +++ b/frontend/src/container/LogsContextList/configs.ts @@ -1,6 +1,7 @@ import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData'; export const INITIAL_PAGE_SIZE = 10; +export const INITIAL_PAGE_SIZE_SMALL_FONT = 12; export const LOGS_MORE_PAGE_SIZE = 10; export const getOrderByTimestamp = (order: string): OrderByPayload => ({ diff --git a/frontend/src/container/LogsContextList/index.tsx b/frontend/src/container/LogsContextList/index.tsx index 270291a33e..d215386c03 100644 --- a/frontend/src/container/LogsContextList/index.tsx +++ b/frontend/src/container/LogsContextList/index.tsx @@ -5,6 +5,7 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import Spinner from 'components/Spinner'; import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { PANEL_TYPES } from 'constants/queryBuilder'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -167,6 +168,7 @@ function LogsContextList({ key={log.id} data={log} linesPerRow={1} + fontSize={FontSize.SMALL} /> ), [], diff --git a/frontend/src/container/LogsExplorerContext/index.tsx b/frontend/src/container/LogsExplorerContext/index.tsx index d62cdb274b..32075097e5 100644 --- a/frontend/src/container/LogsExplorerContext/index.tsx +++ b/frontend/src/container/LogsExplorerContext/index.tsx @@ -2,6 +2,7 @@ import { EditFilled } from '@ant-design/icons'; import { Modal, Typography } from 'antd'; import RawLogView from 'components/Logs/RawLogView'; import LogsContextList from 'container/LogsContextList'; +import { FontSize } from 'container/OptionsMenu/types'; import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -99,6 +100,7 @@ function LogsExplorerContext({ isTextOverflowEllipsisDisabled data={log} linesPerRow={1} + fontSize={FontSize.SMALL} /> void; logs: ILog[]; hasActions: boolean; + fontSize: FontSize; } export default function TableRow({ @@ -33,6 +35,7 @@ export default function TableRow({ handleSetActiveContextLog, logs, hasActions, + fontSize, }: TableRowProps): JSX.Element { const isDarkMode = useIsDarkMode(); @@ -78,6 +81,7 @@ export default function TableRow({ $isDragColumn={false} $isDarkMode={isDarkMode} key={column.key} + fontSize={fontSize} > {cloneElement(children, props)} diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts b/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts index ec16ba1024..c235cbd5e7 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/config.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-nested-ternary */ +import { FontSize } from 'container/OptionsMenu/types'; import { CSSProperties } from 'react'; export const infinityDefaultStyles: CSSProperties = { @@ -5,3 +7,16 @@ export const infinityDefaultStyles: CSSProperties = { overflowX: 'scroll', marginTop: '15px', }; + +export function getInfinityDefaultStyles(fontSize: FontSize): CSSProperties { + return { + width: '100%', + overflowX: 'scroll', + marginTop: + fontSize === FontSize.SMALL + ? '10px' + : fontSize === FontSize.MEDIUM + ? '12px' + : '15px', + }; +} diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx index 2875575162..c678da49db 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx @@ -15,7 +15,7 @@ import { } from 'react-virtuoso'; import { ILog } from 'types/api/logs/log'; -import { infinityDefaultStyles } from './config'; +import { getInfinityDefaultStyles } from './config'; import { LogsCustomTable } from './LogsCustomTable'; import { TableHeaderCellStyled, TableRowStyled } from './styles'; import TableRow from './TableRow'; @@ -95,9 +95,15 @@ const InfinityTable = forwardRef( handleSetActiveContextLog={handleSetActiveContextLog} logs={tableViewProps.logs} hasActions + fontSize={tableViewProps.fontSize} /> ), - [handleSetActiveContextLog, tableColumns, tableViewProps.logs], + [ + handleSetActiveContextLog, + tableColumns, + tableViewProps.fontSize, + tableViewProps.logs, + ], ); const tableHeader = useCallback( @@ -112,6 +118,7 @@ const InfinityTable = forwardRef( $isDarkMode={isDarkMode} $isDragColumn={isDragColumn} key={column.key} + fontSize={tableViewProps?.fontSize} // eslint-disable-next-line react/jsx-props-no-spreading {...(isDragColumn && { className: 'dragHandler' })} > @@ -121,7 +128,7 @@ const InfinityTable = forwardRef( })} ), - [tableColumns, isDarkMode], + [tableColumns, isDarkMode, tableViewProps?.fontSize], ); const handleClickExpand = (index: number): void => { @@ -137,7 +144,7 @@ const InfinityTable = forwardRef( initialTopMostItemIndex={ tableViewProps.activeLogIndex !== -1 ? tableViewProps.activeLogIndex : 0 } - style={infinityDefaultStyles} + style={getInfinityDefaultStyles(tableViewProps.fontSize)} data={dataSource} components={{ // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts index e2719dbc3f..89c9592dd4 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts @@ -1,5 +1,7 @@ +/* eslint-disable no-nested-ternary */ import { Color } from '@signozhq/design-tokens'; import { themeColors } from 'constants/theme'; +import { FontSize } from 'container/OptionsMenu/types'; import styled from 'styled-components'; import { getActiveLogBackground } from 'utils/logs'; @@ -7,6 +9,7 @@ interface TableHeaderCellStyledProps { $isDragColumn: boolean; $isDarkMode: boolean; $isTimestamp?: boolean; + fontSize?: FontSize; } export const TableStyled = styled.table` @@ -15,6 +18,14 @@ export const TableStyled = styled.table` export const TableCellStyled = styled.td` padding: 0.5rem; + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `padding:0.3rem;` + : fontSize === FontSize.MEDIUM + ? `padding:0.4rem;` + : fontSize === FontSize.LARGE + ? `padding:0.5rem;` + : ``} background-color: ${(props): string => props.$isDarkMode ? 'inherit' : themeColors.whiteCream}; @@ -33,7 +44,7 @@ export const TableRowStyled = styled.tr<{ ? `background-color: ${ $isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300 } !important` - : ''} + : ''}; } cursor: pointer; @@ -66,9 +77,17 @@ export const TableHeaderCellStyled = styled.th` line-height: 18px; letter-spacing: -0.07px; background: ${(props): string => (props.$isDarkMode ? '#0b0c0d' : '#fdfdfd')}; - ${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')} ${({ $isDragColumn }): string => ($isDragColumn ? 'cursor: col-resize;' : '')} + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `font-size:11px; line-height:16px; padding: 0.1rem;` + : fontSize === FontSize.MEDIUM + ? `font-size:13px; line-height:20px; padding:0.3rem;` + : fontSize === FontSize.LARGE + ? `font-size:14px; line-height:24px; padding: 0.5rem;` + : ``}; + ${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')} color: ${(props): string => props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey}; `; diff --git a/frontend/src/container/LogsExplorerList/index.tsx b/frontend/src/container/LogsExplorerList/index.tsx index 760f3fea30..18f6ba6d92 100644 --- a/frontend/src/container/LogsExplorerList/index.tsx +++ b/frontend/src/container/LogsExplorerList/index.tsx @@ -14,6 +14,7 @@ import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch'; import LogsError from 'container/LogsError/LogsError'; import { LogsLoading } from 'container/LogsLoading/LogsLoading'; import { useOptionsMenu } from 'container/OptionsMenu'; +import { FontSize } from 'container/OptionsMenu/types'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; @@ -79,6 +80,7 @@ function LogsExplorerList({ data={log} linesPerRow={options.maxLines} selectedFields={selectedFields} + fontSize={options.fontSize} /> ); } @@ -91,6 +93,7 @@ function LogsExplorerList({ onAddToQuery={onAddToQuery} onSetActiveLog={onSetActiveLog} activeLog={activeLog} + fontSize={options.fontSize} linesPerRow={options.maxLines} /> ); @@ -99,6 +102,7 @@ function LogsExplorerList({ activeLog, onAddToQuery, onSetActiveLog, + options.fontSize, options.format, options.maxLines, selectedFields, @@ -121,6 +125,7 @@ function LogsExplorerList({ logs, fields: selectedFields, linesPerRow: options.maxLines, + fontSize: options.fontSize, appendTo: 'end', activeLogIndex, }} @@ -129,9 +134,22 @@ function LogsExplorerList({ ); } + function getMarginTop(): string { + switch (options.fontSize) { + case FontSize.SMALL: + return '10px'; + case FontSize.MEDIUM: + return '12px'; + case FontSize.LARGE: + return '15px'; + default: + return '15px'; + } + } + return ( @@ -151,6 +169,7 @@ function LogsExplorerList({ isLoading, options.format, options.maxLines, + options.fontSize, activeLogIndex, logs, onEndReached, diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index 6b20986d1f..b7b9de84b9 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -10,6 +10,7 @@ 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 { FontSize } from 'container/OptionsMenu/types'; import { useActiveLog } from 'hooks/logs/useActiveLog'; import { memo, useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; @@ -66,6 +67,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { data={log} linesPerRow={linesPerRow} selectedFields={selected} + fontSize={FontSize.SMALL} /> ); } @@ -78,6 +80,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { linesPerRow={linesPerRow} onAddToQuery={onAddToQuery} onSetActiveLog={onSetActiveLog} + fontSize={FontSize.SMALL} /> ); }, @@ -92,6 +95,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { logs={logs} fields={selected} linesPerRow={linesPerRow} + fontSize={FontSize.SMALL} /> ); } diff --git a/frontend/src/container/OptionsMenu/constants.ts b/frontend/src/container/OptionsMenu/constants.ts index 7a454de8ca..7b591cd4c5 100644 --- a/frontend/src/container/OptionsMenu/constants.ts +++ b/frontend/src/container/OptionsMenu/constants.ts @@ -1,6 +1,6 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { OptionsQuery } from './types'; +import { FontSize, OptionsQuery } from './types'; export const URL_OPTIONS = 'options'; @@ -8,6 +8,7 @@ export const defaultOptionsQuery: OptionsQuery = { selectColumns: [], maxLines: 2, format: 'list', + fontSize: FontSize.SMALL, }; export const defaultTraceSelectedColumns = [ diff --git a/frontend/src/container/OptionsMenu/types.ts b/frontend/src/container/OptionsMenu/types.ts index 57b81364d6..2c57d66b28 100644 --- a/frontend/src/container/OptionsMenu/types.ts +++ b/frontend/src/container/OptionsMenu/types.ts @@ -2,10 +2,21 @@ import { InputNumberProps, RadioProps, SelectProps } from 'antd'; import { LogViewMode } from 'container/LogsTable'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +export enum FontSize { + SMALL = 'small', + MEDIUM = 'medium', + LARGE = 'large', +} + +interface FontSizeProps { + value: FontSize; + onChange: (val: FontSize) => void; +} export interface OptionsQuery { selectColumns: BaseAutocompleteData[]; maxLines: number; format: LogViewMode; + fontSize: FontSize; } export interface InitialOptions @@ -18,6 +29,7 @@ export type OptionsMenuConfig = { onChange: (value: LogViewMode) => void; }; maxLines?: Pick; + fontSize?: FontSizeProps; addColumn?: Pick< SelectProps, 'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur' diff --git a/frontend/src/container/OptionsMenu/useOptionsMenu.ts b/frontend/src/container/OptionsMenu/useOptionsMenu.ts index 97fbbbb006..6ed445a773 100644 --- a/frontend/src/container/OptionsMenu/useOptionsMenu.ts +++ b/frontend/src/container/OptionsMenu/useOptionsMenu.ts @@ -21,7 +21,12 @@ import { defaultTraceSelectedColumns, URL_OPTIONS, } from './constants'; -import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types'; +import { + FontSize, + InitialOptions, + OptionsMenuConfig, + OptionsQuery, +} from './types'; import { getOptionsFromKeys } from './utils'; interface UseOptionsMenuProps { @@ -248,6 +253,17 @@ const useOptionsMenu = ({ }, [handleRedirectWithOptionsData, optionsQueryData], ); + const handleFontSizeChange = useCallback( + (value: FontSize) => { + const optionsData: OptionsQuery = { + ...optionsQueryData, + fontSize: value, + }; + + handleRedirectWithOptionsData(optionsData); + }, + [handleRedirectWithOptionsData, optionsQueryData], + ); const handleSearchAttribute = useCallback((value: string) => { setSearchText(value); @@ -282,18 +298,24 @@ const useOptionsMenu = ({ value: optionsQueryData.maxLines || defaultOptionsQuery.maxLines, onChange: handleMaxLinesChange, }, + fontSize: { + value: optionsQueryData?.fontSize || defaultOptionsQuery.fontSize, + onChange: handleFontSizeChange, + }, }), [ - optionsFromAttributeKeys, - optionsQueryData?.maxLines, - optionsQueryData?.format, - optionsQueryData?.selectColumns, isSearchedAttributesFetching, - handleSearchAttribute, + optionsQueryData?.selectColumns, + optionsQueryData.format, + optionsQueryData.maxLines, + optionsQueryData?.fontSize, + optionsFromAttributeKeys, handleSelectColumns, handleRemoveSelectedColumn, + handleSearchAttribute, handleFormatChange, handleMaxLinesChange, + handleFontSizeChange, ], ); From ab1caf13fc201bf99e0317c2d5f445f485882b10 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Thu, 22 Aug 2024 23:59:22 +0530 Subject: [PATCH 23/60] feat: add support for group by attribute in log details (#5753) * feat: add support for group by attribute in log details * feat: auto shift to qb from search on adding groupBY * feat: update icon and styles --- frontend/public/Icons/groupBy.svg | 1 + .../src/assets/CustomIcons/GroupByIcon.tsx | 27 +++ .../LogDetail/LogDetail.interfaces.ts | 6 + frontend/src/components/LogDetail/index.tsx | 2 + .../src/components/Logs/ListLogView/index.tsx | 2 + .../src/components/Logs/RawLogView/index.tsx | 2 + .../container/LiveLogs/LiveLogsList/index.tsx | 2 + .../container/LogDetailedView/Overview.tsx | 9 + .../LogDetailedView/TableView.styles.scss | 8 +- .../container/LogDetailedView/TableView.tsx | 106 +++--------- .../TableView/TableViewActions.styles.scss | 61 +++++++ .../TableView/TableViewActions.tsx | 156 ++++++++++++++++++ .../InfinityTableView/index.tsx | 3 + .../src/container/LogsExplorerList/index.tsx | 2 + .../src/container/LogsExplorerViews/index.tsx | 5 +- .../LogsPanelTable/LogsPanelComponent.tsx | 2 + frontend/src/container/LogsTable/index.tsx | 2 + .../Preview/components/LogsList/index.tsx | 2 + frontend/src/hooks/logs/types.ts | 5 + frontend/src/hooks/logs/useActiveLog.ts | 49 ++++++ frontend/src/pages/LogsExplorer/index.tsx | 19 ++- 21 files changed, 379 insertions(+), 92 deletions(-) create mode 100644 frontend/public/Icons/groupBy.svg create mode 100644 frontend/src/assets/CustomIcons/GroupByIcon.tsx create mode 100644 frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss create mode 100644 frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx diff --git a/frontend/public/Icons/groupBy.svg b/frontend/public/Icons/groupBy.svg new file mode 100644 index 0000000000..e668ef176a --- /dev/null +++ b/frontend/public/Icons/groupBy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/CustomIcons/GroupByIcon.tsx b/frontend/src/assets/CustomIcons/GroupByIcon.tsx new file mode 100644 index 0000000000..4cfceef3c8 --- /dev/null +++ b/frontend/src/assets/CustomIcons/GroupByIcon.tsx @@ -0,0 +1,27 @@ +import { Color } from '@signozhq/design-tokens'; +import { useIsDarkMode } from 'hooks/useDarkMode'; + +function GroupByIcon(): JSX.Element { + const isDarkMode = useIsDarkMode(); + return ( + + + + + + + + + + + + ); +} + +export default GroupByIcon; diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts index 2a2dd56855..2c56d58fd1 100644 --- a/frontend/src/components/LogDetail/LogDetail.interfaces.ts +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -3,12 +3,18 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC'; import { ActionItemProps } from 'container/LogDetailedView/ActionItem'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { VIEWS } from './constants'; export type LogDetailProps = { log: ILog | null; selectedTab: VIEWS; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; isListViewPanel?: boolean; listViewPanelSelectedFields?: IField[] | null; } & Pick & diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index 650d474736..5421672529 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -37,6 +37,7 @@ function LogDetail({ log, onClose, onAddToQuery, + onGroupByAttribute, onClickActionItem, selectedTab, isListViewPanel = false, @@ -209,6 +210,7 @@ function LogDetail({ logData={log} onAddToQuery={onAddToQuery} onClickActionItem={onClickActionItem} + onGroupByAttribute={onGroupByAttribute} isListViewPanel={isListViewPanel} selectedOptions={options} listViewPanelSelectedFields={listViewPanelSelectedFields} diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index d1461b0caa..34b3fddd19 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -141,6 +141,7 @@ function ListLogView({ onAddToQuery: handleAddToQuery, onSetActiveLog: handleSetActiveContextLog, onClearActiveLog: handleClearActiveContextLog, + onGroupByAttribute, } = useActiveLog(); const isDarkMode = useIsDarkMode(); @@ -257,6 +258,7 @@ function ListLogView({ onAddToQuery={handleAddToQuery} selectedTab={VIEW_TYPES.CONTEXT} onClose={handlerClearActiveContextLog} + onGroupByAttribute={onGroupByAttribute} /> )} diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index b4b3eb7783..935f423393 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -55,6 +55,7 @@ function RawLogView({ onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const [hasActionButtons, setHasActionButtons] = useState(false); @@ -202,6 +203,7 @@ function RawLogView({ onClose={handleCloseLogDetail} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} /> )} diff --git a/frontend/src/container/LiveLogs/LiveLogsList/index.tsx b/frontend/src/container/LiveLogs/LiveLogsList/index.tsx index 50beda2953..0be9334849 100644 --- a/frontend/src/container/LiveLogs/LiveLogsList/index.tsx +++ b/frontend/src/container/LiveLogs/LiveLogsList/index.tsx @@ -38,6 +38,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { activeLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, onSetActiveLog, } = useActiveLog(); @@ -151,6 +152,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element { log={activeLog} onClose={onClearActiveLog} onAddToQuery={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} onClickActionItem={onAddToQuery} /> diff --git a/frontend/src/container/LogDetailedView/Overview.tsx b/frontend/src/container/LogDetailedView/Overview.tsx index 9054217c33..1abfa5a526 100644 --- a/frontend/src/container/LogDetailedView/Overview.tsx +++ b/frontend/src/container/LogDetailedView/Overview.tsx @@ -18,6 +18,7 @@ import { ChevronDown, ChevronRight, Search } from 'lucide-react'; import { ReactNode, useState } from 'react'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { ActionItemProps } from './ActionItem'; import TableView from './TableView'; @@ -27,6 +28,11 @@ interface OverviewProps { isListViewPanel?: boolean; selectedOptions: OptionsQuery; listViewPanelSelectedFields?: IField[] | null; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; } type Props = OverviewProps & @@ -39,6 +45,7 @@ function Overview({ onClickActionItem, isListViewPanel = false, selectedOptions, + onGroupByAttribute, listViewPanelSelectedFields, }: Props): JSX.Element { const [isWrapWord, setIsWrapWord] = useState(true); @@ -204,6 +211,7 @@ function Overview({ logData={logData} onAddToQuery={onAddToQuery} fieldSearchInput={fieldSearchInput} + onGroupByAttribute={onGroupByAttribute} onClickActionItem={onClickActionItem} isListViewPanel={isListViewPanel} selectedOptions={selectedOptions} @@ -222,6 +230,7 @@ function Overview({ Overview.defaultProps = { isListViewPanel: false, listViewPanelSelectedFields: null, + onGroupByAttribute: undefined, }; export default Overview; diff --git a/frontend/src/container/LogDetailedView/TableView.styles.scss b/frontend/src/container/LogDetailedView/TableView.styles.scss index 2f092dc04d..322a5cd638 100644 --- a/frontend/src/container/LogDetailedView/TableView.styles.scss +++ b/frontend/src/container/LogDetailedView/TableView.styles.scss @@ -11,7 +11,7 @@ top: 50%; right: 16px; transform: translateY(-50%); - gap: 8px; + gap: 4px; } } } @@ -76,8 +76,10 @@ box-shadow: none; border-radius: 2px; background: var(--bg-slate-400); - - height: 24px; + padding: 2px 3px; + gap: 3px; + height: 18px; + width: 20px; } } } diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 0508701af5..591109ac3c 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -4,13 +4,12 @@ import './TableView.styles.scss'; import { LinkOutlined } from '@ant-design/icons'; import { Color } from '@signozhq/design-tokens'; -import { Button, Space, Spin, Tooltip, Tree, Typography } from 'antd'; +import { Button, Space, Tooltip, Typography } from 'antd'; import { ColumnsType } from 'antd/es/table'; import cx from 'classnames'; import AddToQueryHOC, { AddToQueryHOCProps, } from 'components/Logs/AddToQueryHOC'; -import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC'; import { ResizeTable } from 'components/ResizeTable'; import { OPERATORS } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; @@ -19,8 +18,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode'; import history from 'lib/history'; import { fieldSearchFilter } from 'lib/logs/fieldSearch'; import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes'; -import { isEmpty } from 'lodash-es'; -import { ArrowDownToDot, ArrowUpFromDot, Pin } from 'lucide-react'; +import { Pin } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { generatePath } from 'react-router-dom'; @@ -29,17 +27,12 @@ import AppActions from 'types/actions'; import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { ActionItemProps } from './ActionItem'; import FieldRenderer from './FieldRenderer'; -import { - filterKeyForField, - findKeyPath, - flattenObject, - jsonToDataNodes, - recursiveParseJSON, - removeEscapeCharacters, -} from './utils'; +import { TableViewActions } from './TableView/TableViewActions'; +import { filterKeyForField, findKeyPath, flattenObject } from './utils'; // Fields which should be restricted from adding it to query const RESTRICTED_FIELDS = ['timestamp']; @@ -50,6 +43,11 @@ interface TableViewProps { selectedOptions: OptionsQuery; isListViewPanel?: boolean; listViewPanelSelectedFields?: IField[] | null; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; } type Props = TableViewProps & @@ -63,6 +61,7 @@ function TableView({ onClickActionItem, isListViewPanel = false, selectedOptions, + onGroupByAttribute, listViewPanelSelectedFields, }: Props): JSX.Element | null { const dispatch = useDispatch>(); @@ -271,75 +270,17 @@ function TableView({ width: 70, ellipsis: false, className: 'value-field-container attribute-value', - render: (fieldData: Record, record): JSX.Element => { - const textToCopy = fieldData.value.slice(1, -1); - - if (record.field === 'body') { - const parsedBody = recursiveParseJSON(fieldData.value); - if (!isEmpty(parsedBody)) { - return ( - - ); - } - } - - const fieldFilterKey = filterKeyForField(fieldData.field); - - return ( -
- - - {removeEscapeCharacters(fieldData.value)} - - - - {!isListViewPanel && ( - - -
- ); - }, + render: (fieldData: Record, record): JSX.Element => ( + + ), }, ]; function sortPinnedAttributes( @@ -380,9 +321,10 @@ function TableView({ TableView.defaultProps = { isListViewPanel: false, listViewPanelSelectedFields: null, + onGroupByAttribute: undefined, }; -interface DataType { +export interface DataType { key: string; field: string; value: string; diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss b/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss new file mode 100644 index 0000000000..f5a45ef416 --- /dev/null +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.styles.scss @@ -0,0 +1,61 @@ +.open-popover { + &.value-field { + .action-btn { + display: flex !important; + position: absolute !important; + top: 50% !important; + right: 16px !important; + transform: translateY(-50%) !important; + gap: 4px !important; + } + } +} + +.table-view-actions-content { + .ant-popover-inner { + border-radius: 4px; + border: 1px solid var(--bg-slate-400); + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ); + box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(20px); + padding: 0px; + .group-by-clause { + display: flex; + align-items: center; + gap: 4px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.14px; + padding: 12px 18px 12px 14px; + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + + .group-by-clause:hover { + background-color: unset !important; + } + } +} + +.lightMode { + .table-view-actions-content { + .ant-popover-inner { + border: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-100) !important; + + .group-by-clause { + color: var(--bg-ink-400); + } + } + } +} diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx new file mode 100644 index 0000000000..b239e64d3a --- /dev/null +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx @@ -0,0 +1,156 @@ +import './TableViewActions.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Button, Popover, Spin, Tooltip, Tree } from 'antd'; +import GroupByIcon from 'assets/CustomIcons/GroupByIcon'; +import cx from 'classnames'; +import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC'; +import { OPERATORS } from 'constants/queryBuilder'; +import ROUTES from 'constants/routes'; +import { isEmpty } from 'lodash-es'; +import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +import { DataType } from '../TableView'; +import { + filterKeyForField, + jsonToDataNodes, + recursiveParseJSON, + removeEscapeCharacters, +} from '../utils'; + +interface ITableViewActionsProps { + fieldData: Record; + record: DataType; + isListViewPanel: boolean; + isfilterInLoading: boolean; + isfilterOutLoading: boolean; + onGroupByAttribute?: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; + onClickHandler: ( + operator: string, + fieldKey: string, + fieldValue: string, + ) => () => void; +} + +export function TableViewActions( + props: ITableViewActionsProps, +): React.ReactElement { + const { + fieldData, + record, + isListViewPanel, + isfilterInLoading, + isfilterOutLoading, + onClickHandler, + onGroupByAttribute, + } = props; + + const { pathname } = useLocation(); + + // there is no option for where clause in old logs explorer and live logs page + const isOldLogsExplorerOrLiveLogsPage = useMemo( + () => pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS, + [pathname], + ); + + const [isOpen, setIsOpen] = useState(false); + const textToCopy = fieldData.value.slice(1, -1); + + if (record.field === 'body') { + const parsedBody = recursiveParseJSON(fieldData.value); + if (!isEmpty(parsedBody)) { + return ( + + ); + } + } + + const fieldFilterKey = filterKeyForField(fieldData.field); + + return ( +
+ + + {removeEscapeCharacters(fieldData.value)} + + + + {!isListViewPanel && ( + + + +
+ } + rootClassName="table-view-actions-content" + trigger="hover" + placement="bottomLeft" + > +
+ ); +} + +TableViewActions.defaultProps = { + onGroupByAttribute: undefined, +}; diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx index c678da49db..fe2d2ba1d4 100644 --- a/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx @@ -59,6 +59,7 @@ const InfinityTable = forwardRef( onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const { dataSource, columns } = useTableView({ @@ -172,6 +173,7 @@ const InfinityTable = forwardRef( onClose={handleClearActiveContextLog} onAddToQuery={handleAddToQuery} selectedTab={VIEW_TYPES.CONTEXT} + onGroupByAttribute={onGroupByAttribute} /> )} ( onClose={onClearActiveLog} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} /> ); diff --git a/frontend/src/container/LogsExplorerList/index.tsx b/frontend/src/container/LogsExplorerList/index.tsx index 18f6ba6d92..42cffb92cd 100644 --- a/frontend/src/container/LogsExplorerList/index.tsx +++ b/frontend/src/container/LogsExplorerList/index.tsx @@ -51,6 +51,7 @@ function LogsExplorerList({ activeLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, onSetActiveLog, } = useActiveLog(); @@ -208,6 +209,7 @@ function LogsExplorerList({ log={activeLog} onClose={onClearActiveLog} onAddToQuery={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} onClickActionItem={onAddToQuery} /> diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index bc7002e7dc..bfe203d352 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -263,10 +263,7 @@ function LogsExplorerViews({ }, undefined, listQueryKeyRef, - { - ...(!isEmpty(queryId) && - selectedPanelType !== PANEL_TYPES.LIST && { 'X-SIGNOZ-QUERY-ID': queryId }), - }, + {}, ); const getRequestData = useCallback( diff --git a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx index 3835386cd3..a7598b5c76 100644 --- a/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx +++ b/frontend/src/container/LogsPanelTable/LogsPanelComponent.tsx @@ -108,6 +108,7 @@ function LogsPanelComponent({ onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const handleRow = useCallback( @@ -244,6 +245,7 @@ function LogsPanelComponent({ onClose={onClearActiveLog} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} isListViewPanel listViewPanelSelectedFields={widget?.selectedLogFields} /> diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index b7b9de84b9..5717136304 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -36,6 +36,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { activeLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, onSetActiveLog, } = useActiveLog(); @@ -130,6 +131,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { selectedTab={VIEW_TYPES.OVERVIEW} log={activeLog} onClose={onClearActiveLog} + onGroupByAttribute={onGroupByAttribute} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} /> diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx index f7d3af3a88..5bbe6de737 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx @@ -13,6 +13,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element { onSetActiveLog, onClearActiveLog, onAddToQuery, + onGroupByAttribute, } = useActiveLog(); const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log); @@ -42,6 +43,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element { onClose={onClearActiveLog} onAddToQuery={onAddToQuery} onClickActionItem={onAddToQuery} + onGroupByAttribute={onGroupByAttribute} />
); diff --git a/frontend/src/hooks/logs/types.ts b/frontend/src/hooks/logs/types.ts index 3dbcbb61f1..508c3254ee 100644 --- a/frontend/src/hooks/logs/types.ts +++ b/frontend/src/hooks/logs/types.ts @@ -28,4 +28,9 @@ export type UseActiveLog = { isJSON?: boolean, dataType?: DataTypes, ) => void; + onGroupByAttribute: ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => Promise; }; diff --git a/frontend/src/hooks/logs/useActiveLog.ts b/frontend/src/hooks/logs/useActiveLog.ts index a56c13c72e..0a968c4650 100644 --- a/frontend/src/hooks/logs/useActiveLog.ts +++ b/frontend/src/hooks/logs/useActiveLog.ts @@ -128,6 +128,54 @@ export const useActiveLog = (): UseActiveLog => { [currentQuery, notifications, queryClient, redirectWithQueryBuilderData], ); + const onGroupByAttribute = useCallback( + async ( + fieldKey: string, + isJSON?: boolean, + dataType?: DataTypes, + ): Promise => { + try { + const keysAutocompleteResponse = await queryClient.fetchQuery( + [QueryBuilderKeys.GET_AGGREGATE_KEYS, fieldKey], + // eslint-disable-next-line sonarjs/no-identical-functions + async () => + getAggregateKeys({ + searchText: fieldKey, + aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator, + dataSource: currentQuery.builder.queryData[0].dataSource, + aggregateAttribute: + currentQuery.builder.queryData[0].aggregateAttribute.key, + }), + ); + + const keysAutocomplete: BaseAutocompleteData[] = + keysAutocompleteResponse.payload?.attributeKeys || []; + + const existAutocompleteKey = chooseAutocompleteFromCustomValue( + keysAutocomplete, + fieldKey, + isJSON, + dataType, + ); + + const nextQuery: Query = { + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: currentQuery.builder.queryData.map((item) => ({ + ...item, + groupBy: [...item.groupBy, existAutocompleteKey], + })), + }, + }; + + redirectWithQueryBuilderData(nextQuery); + } catch { + notifications.error({ message: SOMETHING_WENT_WRONG }); + } + }, + [currentQuery, notifications, queryClient, redirectWithQueryBuilderData], + ); const onAddToQueryLogs = useCallback( (fieldKey: string, fieldValue: string, operator: string) => { const updatedQueryString = getGeneratedFilterQueryString( @@ -147,5 +195,6 @@ export const useActiveLog = (): UseActiveLog => { onSetActiveLog, onClearActiveLog, onAddToQuery: isLogsPage ? onAddToQueryLogs : onAddToQueryExplorer, + onGroupByAttribute, }; }; diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index 505851da10..0c7e33c52a 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -42,7 +42,13 @@ function LogsExplorer(): JSX.Element { if (currentQuery.builder.queryData.length > 1) { handleChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER); } - }, [currentQuery.builder.queryData.length]); + if ( + currentQuery.builder.queryData.length === 1 && + currentQuery.builder.queryData[0].groupBy.length > 0 + ) { + handleChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER); + } + }, [currentQuery.builder.queryData, currentQuery.builder.queryData.length]); const isMultipleQueries = useMemo( () => @@ -51,12 +57,19 @@ function LogsExplorer(): JSX.Element { [currentQuery], ); + const isGroupByPresent = useMemo( + () => + currentQuery.builder.queryData.length === 1 && + currentQuery.builder.queryData[0].groupBy.length > 0, + [currentQuery.builder.queryData], + ); + const toolbarViews = useMemo( () => ({ search: { name: 'search', label: 'Search', - disabled: isMultipleQueries, + disabled: isMultipleQueries || isGroupByPresent, show: true, }, queryBuilder: { @@ -72,7 +85,7 @@ function LogsExplorer(): JSX.Element { show: false, }, }), - [isMultipleQueries], + [isGroupByPresent, isMultipleQueries], ); return ( From 758b10f1bfb6b9b5bce98cecb357b472796b2bab Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Fri, 23 Aug 2024 00:54:30 +0530 Subject: [PATCH 24/60] fix: raw view css condense fix for line clamp (#5755) --- .../src/components/Logs/RawLogView/index.tsx | 1 + .../src/components/Logs/RawLogView/styles.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index 935f423393..292d7e029a 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -162,6 +162,7 @@ function RawLogView({ $isActiveLog={isActiveLog} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} + fontSize={fontSize} > ` position: relative; width: 100%; @@ -24,6 +25,13 @@ export const RawLogViewContainer = styled(Row)<{ .log-state-indicator { margin: 4px 0; + + ${({ fontSize }): string => + fontSize === FontSize.SMALL + ? `margin: 1px 0;` + : fontSize === FontSize.MEDIUM + ? `margin: 1px 0;` + : `margin: 2px 0;`} } ${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)} @@ -50,13 +58,8 @@ export const ExpandIconWrapper = styled(Col)` export const RawLogContent = styled.div` margin-bottom: 0; - display: flex !important; - align-items: center; font-family: 'SF Mono', monospace; font-family: 'Geist Mono'; - font-size: 13px; - font-weight: 400; - line-height: 24px; letter-spacing: -0.07px; padding: 4px; text-align: left; @@ -73,6 +76,9 @@ export const RawLogContent = styled.div` line-clamp: ${linesPerRow}; -webkit-box-orient: vertical;`}; + font-size: 13px; + font-weight: 400; + line-height: 24px; ${({ fontSize }): string => fontSize === FontSize.SMALL ? `font-size:11px; line-height:16px; padding:1px;` From 43ed49f9d99c5715d7158261739374320be7c085 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Fri, 23 Aug 2024 12:24:06 +0530 Subject: [PATCH 25/60] fix: dashboard names invisible due to same background color (#5758) --- .../src/container/ListOfDashboard/DashboardList.styles.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss index 7fff109d2a..ef4a7c0a67 100644 --- a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss +++ b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss @@ -1067,7 +1067,7 @@ color: var(--bg-ink-500); } .subtitle { - color: var(--bg-vanilla-400); + color: var(--bg-ink-300); } .ant-table-row { @@ -1087,6 +1087,10 @@ .dashboard-title { color: var(--bg-slate-300); + + .title { + color: var(--bg-ink-500); + } } .title-with-action { From bd7d14b1cabd851fd74f222674837c2925890e6f Mon Sep 17 00:00:00 2001 From: Vibhu Pandey Date: Fri, 23 Aug 2024 13:07:10 +0530 Subject: [PATCH 26/60] feat(render): add render package (#5751) ### Summary Add `render` package #### Related Issues / PR's https://github.com/SigNoz/signoz/pull/5710 --- pkg/errors/errors.go | 6 +- pkg/http/render/render.go | 83 ++++++++++++++++++ pkg/http/render/render_test.go | 116 ++++++++++++++++++++++++++ pkg/http/render/status.go | 9 ++ pkg/query-service/app/http_handler.go | 6 ++ 5 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 pkg/http/render/render.go create mode 100644 pkg/http/render/render_test.go create mode 100644 pkg/http/render/status.go diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 7bd3e1b97e..6ed9728e4c 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -4,6 +4,10 @@ import ( "fmt" ) +const ( + codeUnknown string = "unknown" +) + // base is the fundamental struct that implements the error interface. // The order of the struct is 'TCMEUA'. type base struct { @@ -100,7 +104,7 @@ func Unwrapb(cause error) (typ, string, string, error, string, []string) { return base.t, base.c, base.m, base.e, base.u, base.a } - return TypeInternal, "", cause.Error(), cause, "", []string{} + return TypeInternal, codeUnknown, cause.Error(), cause, "", []string{} } // Ast checks if the provided error matches the specified custom error type. diff --git a/pkg/http/render/render.go b/pkg/http/render/render.go new file mode 100644 index 0000000000..405bb76ed1 --- /dev/null +++ b/pkg/http/render/render.go @@ -0,0 +1,83 @@ +package render + +import ( + "net/http" + + jsoniter "github.com/json-iterator/go" + "go.signoz.io/signoz/pkg/errors" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +type response struct { + Status string `json:"status"` + Data interface{} `json:"data,omitempty"` + Error *responseerror `json:"error,omitempty"` +} + +type responseerror struct { + Code string `json:"code"` + Message string `json:"message"` + Url string `json:"url,omitempty"` + Errors []responseerroradditional `json:"errors,omitempty"` +} + +type responseerroradditional struct { + Message string `json:"message"` +} + +func Success(rw http.ResponseWriter, httpCode int, data interface{}) { + body, err := json.Marshal(&response{Status: StatusSuccess.s, Data: data}) + if err != nil { + Error(rw, err) + return + } + + if httpCode == 0 { + httpCode = http.StatusOK + } + + rw.WriteHeader(httpCode) + _, _ = rw.Write(body) +} + +func Error(rw http.ResponseWriter, cause error) { + // See if this is an instance of the base error or not + t, c, m, _, u, a := errors.Unwrapb(cause) + + // Derive the http code from the error type + httpCode := http.StatusInternalServerError + switch t { + case errors.TypeInvalidInput: + httpCode = http.StatusBadRequest + case errors.TypeNotFound: + httpCode = http.StatusNotFound + case errors.TypeAlreadyExists: + httpCode = http.StatusConflict + case errors.TypeUnauthenticated: + httpCode = http.StatusUnauthorized + } + + rea := make([]responseerroradditional, len(a)) + for k, v := range a { + rea[k] = responseerroradditional{v} + } + + body, err := json.Marshal(&response{ + Status: StatusError.s, + Error: &responseerror{ + Code: c, + Url: u, + Message: m, + Errors: rea, + }, + }) + if err != nil { + // this should never be the case + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.WriteHeader(httpCode) + _, _ = rw.Write(body) +} diff --git a/pkg/http/render/render_test.go b/pkg/http/render/render_test.go new file mode 100644 index 0000000000..79943157f3 --- /dev/null +++ b/pkg/http/render/render_test.go @@ -0,0 +1,116 @@ +package render + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.signoz.io/signoz/pkg/errors" +) + +func TestSuccess(t *testing.T) { + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + data := map[string]any{ + "int64": int64(9), + "string": "string", + "bool": true, + } + + marshalled, err := json.Marshal(data) + require.NoError(t, err) + + expected := []byte(fmt.Sprintf(`{"status":"success","data":%s}`, string(marshalled))) + + server := &http.Server{ + Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + Success(rw, http.StatusAccepted, data) + }), + } + + go func() { + _ = server.Serve(listener) + }() + + defer func() { + _ = server.Shutdown(context.Background()) + }() + + req, err := http.NewRequest("GET", "http://"+listener.Addr().String(), nil) + require.NoError(t, err) + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, http.StatusAccepted, res.StatusCode) + assert.Equal(t, expected, actual) +} + +func TestError(t *testing.T) { + listener, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + + testCases := map[string]struct { + name string + statusCode int + err error + expected []byte + }{ + "/already_exists": { + name: "AlreadyExists", + statusCode: http.StatusConflict, + err: errors.New(errors.TypeAlreadyExists, "already_exists", "already exists").WithUrl("https://already_exists"), + expected: []byte(`{"status":"error","error":{"code":"already_exists","message":"already exists","url":"https://already_exists"}}`), + }, + "/unauthenticated": { + name: "Unauthenticated", + statusCode: http.StatusUnauthorized, + err: errors.New(errors.TypeUnauthenticated, "not_allowed", "not allowed").WithUrl("https://unauthenticated").WithAdditional("a1", "a2"), + expected: []byte(`{"status":"error","error":{"code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1"},{"message":"a2"}]}}`), + }, + } + + server := &http.Server{ + Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + tc, ok := testCases[req.URL.Path] + if ok { + Error(rw, tc.err) + return + } + }), + } + + go func() { + _ = server.Serve(listener) + }() + + defer func() { + _ = server.Shutdown(context.Background()) + }() + + for path, tc := range testCases { + t.Run("", func(t *testing.T) { + req, err := http.NewRequest("GET", "http://"+listener.Addr().String()+path, nil) + require.NoError(t, err) + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + actual, err := io.ReadAll(res.Body) + require.NoError(t, err) + + assert.Equal(t, tc.statusCode, res.StatusCode) + assert.Equal(t, tc.expected, actual) + }) + } + +} diff --git a/pkg/http/render/status.go b/pkg/http/render/status.go new file mode 100644 index 0000000000..dc4f8720ff --- /dev/null +++ b/pkg/http/render/status.go @@ -0,0 +1,9 @@ +package render + +var ( + StatusSuccess status = status{"success"} + StatusError = status{"error"} +) + +// Defines custom error types +type status struct{ s string } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 5064cc359b..d37bb1eeea 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -238,6 +238,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) { return aH, nil } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure type structuredResponse struct { Data interface{} `json:"data"` Total int `json:"total"` @@ -246,11 +247,13 @@ type structuredResponse struct { Errors []structuredError `json:"errors"` } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure type structuredError struct { Code int `json:"code,omitempty"` Msg string `json:"msg"` } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure type ApiResponse struct { Status status `json:"status"` Data interface{} `json:"data,omitempty"` @@ -258,6 +261,7 @@ type ApiResponse struct { Error string `json:"error,omitempty"` } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interface{}) { json := jsoniter.ConfigCompatibleWithStandardLibrary b, err := json.Marshal(&ApiResponse{ @@ -301,6 +305,7 @@ func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interfa } } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure func writeHttpResponse(w http.ResponseWriter, data interface{}) { json := jsoniter.ConfigCompatibleWithStandardLibrary b, err := json.Marshal(&ApiResponse{ @@ -351,6 +356,7 @@ func (aH *APIHandler) RegisterQueryRangeV4Routes(router *mux.Router, am *AuthMid subRouter.HandleFunc("/metric/metric_metadata", am.ViewAccess(aH.getMetricMetadata)).Methods(http.MethodGet) } +// todo(remove): Implemented at render package (go.signoz.io/signoz/pkg/http/render) with the new error structure func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) { writeHttpResponse(w, data) } From 947b5bdefb646ecc5ec23c3cb96a408be5589873 Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:15:30 +0530 Subject: [PATCH 27/60] fix: handled defaultTraceSelected for traces list view (#5752) * fix: handled defaultTraceSelected for traces list view * fix: added metaData id --- .../container/OptionsMenu/useOptionsMenu.ts | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/frontend/src/container/OptionsMenu/useOptionsMenu.ts b/frontend/src/container/OptionsMenu/useOptionsMenu.ts index 6ed445a773..7b3cfce035 100644 --- a/frontend/src/container/OptionsMenu/useOptionsMenu.ts +++ b/frontend/src/container/OptionsMenu/useOptionsMenu.ts @@ -7,6 +7,10 @@ import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; import useDebounce from 'hooks/useDebounce'; import { useNotifications } from 'hooks/useNotifications'; import useUrlQueryData from 'hooks/useUrlQueryData'; +import { + AllTraceFilterKeys, + AllTraceFilterKeyValue, +} from 'pages/TracesExplorer/Filter/filterUtils'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useQueries } from 'react-query'; import { ErrorResponse, SuccessResponse } from 'types/api'; @@ -111,15 +115,40 @@ const useOptionsMenu = ({ [] as BaseAutocompleteData[], ); - return ( - (initialOptions.selectColumns - ?.map((column) => attributesData.find(({ key }) => key === column)) - .filter(Boolean) as BaseAutocompleteData[]) || [] - ); + let initialSelected = initialOptions.selectColumns + ?.map((column) => attributesData.find(({ key }) => key === column)) + .filter(Boolean) as BaseAutocompleteData[]; + + if (dataSource === DataSource.TRACES) { + initialSelected = initialSelected + ?.map((col) => { + if (col && Object.keys(AllTraceFilterKeyValue).includes(col?.key)) { + const metaData = defaultTraceSelectedColumns.find( + (coln) => coln.key === (col.key as AllTraceFilterKeys), + ); + + return { + ...metaData, + key: metaData?.key, + dataType: metaData?.dataType, + type: metaData?.type, + isColumn: metaData?.isColumn, + isJSON: metaData?.isJSON, + id: metaData?.id, + }; + } + return col; + }) + .filter(Boolean) as BaseAutocompleteData[]; + } + + return initialSelected || []; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ isFetchedInitialAttributes, initialOptions?.selectColumns, initialAttributesResult, + dataSource, ]); const { From 33541a2ac081fc3d1c9c558bd297271df00de7fb Mon Sep 17 00:00:00 2001 From: Sudeep MP Date: Fri, 23 Aug 2024 11:25:04 +0100 Subject: [PATCH 28/60] feat: add view templates option to dashboard menu (#5696) * feat: add view templates option to dashboard menu * feat: increase dropdown overlay width Set the dropdown overlay width to 200px to provide breathing space for the dropdown button. Added flex to wrap the dropdown button to create space between the right icon and the left elements. --------- Co-authored-by: Pranay Prateek --- frontend/public/locales/en/dashboard.json | 1 + .../ListOfDashboard/DashboardList.styles.scss | 2 + .../ListOfDashboard/DashboardsList.tsx | 26 +++++++ .../ListOfDashboard/ImportJSON/index.tsx | 70 +++++++++++++------ 4 files changed, 77 insertions(+), 22 deletions(-) diff --git a/frontend/public/locales/en/dashboard.json b/frontend/public/locales/en/dashboard.json index d2e90237a9..26e8f289a9 100644 --- a/frontend/public/locales/en/dashboard.json +++ b/frontend/public/locales/en/dashboard.json @@ -1,6 +1,7 @@ { "create_dashboard": "Create Dashboard", "import_json": "Import Dashboard JSON", + "view_template": "View templates", "import_grafana_json": "Import Grafana JSON", "copy_to_clipboard": "Copy To ClipBoard", "download_json": "Download JSON", diff --git a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss index ef4a7c0a67..cf9ec283d2 100644 --- a/frontend/src/container/ListOfDashboard/DashboardList.styles.scss +++ b/frontend/src/container/ListOfDashboard/DashboardList.styles.scss @@ -590,6 +590,8 @@ } .new-dashboard-menu { + width: 200px; + .create-dashboard-menu-item { display: flex; align-items: center; diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx index 8de34fdaf1..421c7e31c4 100644 --- a/frontend/src/container/ListOfDashboard/DashboardsList.tsx +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -45,6 +45,8 @@ import { Ellipsis, EllipsisVertical, Expand, + ExternalLink, + Github, HdmiPort, LayoutGrid, Link2, @@ -53,6 +55,8 @@ import { RotateCw, Search, } from 'lucide-react'; +// #TODO: lucide will be removing brand icons like Github in future, in that case we can use simple icons +// see more: https://github.com/lucide-icons/lucide/issues/94 import { handleContactSupport } from 'pages/Integrations/utils'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { @@ -600,6 +604,28 @@ function DashboardsList(): JSX.Element { ), key: '1', }, + { + label: ( + + +
+ View templates +
+ +
+
+ ), + key: '2', + }, ]; if (createNewDashboard) { diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index 9bf1db2051..62767e6799 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -4,7 +4,15 @@ import { red } from '@ant-design/colors'; 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 { + Button, + Flex, + Modal, + Space, + Typography, + Upload, + UploadProps, +} from 'antd'; import logEvent from 'api/common/logEvent'; import createDashboard from 'api/dashboard/create'; import ROUTES from 'constants/routes'; @@ -13,7 +21,9 @@ import { MESSAGE } from 'hooks/useFeatureFlag'; import { useNotifications } from 'hooks/useNotifications'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; import history from 'lib/history'; -import { MonitorDot, MoveRight, X } from 'lucide-react'; +import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react'; +// #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/ +// See more: https://github.com/lucide-icons/lucide/issues/94 import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath } from 'react-router-dom'; @@ -174,27 +184,43 @@ function ImportJSON({ )}
- false} - action="none" - data={jsonData} - > - + + - {' '} - {t('upload_json_file')} - - + + +
+ ); +} + +export default MessagingQueuesConfigOptions; diff --git a/frontend/src/pages/MessagingQueues/MQGraph/MQGraph.tsx b/frontend/src/pages/MessagingQueues/MQGraph/MQGraph.tsx new file mode 100644 index 0000000000..cd7cdd74c4 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQGraph/MQGraph.tsx @@ -0,0 +1,88 @@ +import { QueryParams } from 'constants/query'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { ViewMenuAction } from 'container/GridCardLayout/config'; +import GridCard from 'container/GridCardLayout/GridCard'; +import { Card } from 'container/GridCardLayout/styles'; +import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { useHistory, useLocation } from 'react-router-dom'; +import { UpdateTimeInterval } from 'store/actions'; + +import { + getFiltersFromConfigOptions, + getWidgetQuery, + setSelectedTimelineQuery, +} from '../MessagingQueuesUtils'; + +function MessagingQueuesGraph(): JSX.Element { + const isDarkMode = useIsDarkMode(); + + const urlQuery = useUrlQuery(); + const consumerGrp = urlQuery.get(QueryParams.consumerGrp) || ''; + const topic = urlQuery.get(QueryParams.topic) || ''; + const partition = urlQuery.get(QueryParams.partition) || ''; + + const filterItems = useMemo( + () => getFiltersFromConfigOptions(consumerGrp, topic, partition), + [consumerGrp, topic, partition], + ); + + const widgetData = useMemo( + () => getWidgetQueryBuilder(getWidgetQuery({ filterItems })), + [filterItems], + ); + const history = useHistory(); + const location = useLocation(); + + const messagingQueueCustomTooltipText = (): HTMLDivElement => { + const customText = document.createElement('div'); + customText.textContent = 'Click on co-ordinate to view details'; + customText.style.paddingTop = '8px'; + customText.style.paddingBottom = '2px'; + customText.style.color = '#fff'; + return customText; + }; + + const { pathname } = useLocation(); + const dispatch = useDispatch(); + + const onDragSelect = useCallback( + (start: number, end: number) => { + const startTimestamp = Math.trunc(start); + const endTimestamp = Math.trunc(end); + + urlQuery.set(QueryParams.startTime, startTimestamp.toString()); + urlQuery.set(QueryParams.endTime, endTimestamp.toString()); + const generatedUrl = `${pathname}?${urlQuery.toString()}`; + history.push(generatedUrl); + + if (startTimestamp !== endTimestamp) { + dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); + } + }, + [dispatch, history, pathname, urlQuery], + ); + + return ( + + { + setSelectedTimelineQuery(urlQuery, xValue, location, history, data); + }} + onDragSelect={onDragSelect} + customTooltipElement={messagingQueueCustomTooltipText()} + /> + + ); +} + +export default MessagingQueuesGraph; diff --git a/frontend/src/pages/MessagingQueues/MQGraph/useGetAllConfigOptions.ts b/frontend/src/pages/MessagingQueues/MQGraph/useGetAllConfigOptions.ts new file mode 100644 index 0000000000..c11c0f85f5 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQGraph/useGetAllConfigOptions.ts @@ -0,0 +1,49 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { DefaultOptionType } from 'antd/es/select'; +import { getAttributesValues } from 'api/queryBuilder/getAttributesValues'; +import { useQuery } from 'react-query'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { DataSource } from 'types/common/queryBuilder'; + +export interface ConfigOptions { + attributeKey: string; + searchText?: string; +} + +export interface GetAllConfigOptionsResponse { + options: DefaultOptionType[]; + isFetching: boolean; +} + +export function useGetAllConfigOptions( + props: ConfigOptions, +): GetAllConfigOptionsResponse { + const { attributeKey, searchText } = props; + + const { data, isLoading } = useQuery( + ['attributesValues', attributeKey, searchText], + async () => { + const { payload } = await getAttributesValues({ + aggregateOperator: 'avg', + dataSource: DataSource.METRICS, + aggregateAttribute: 'kafka_consumer_group_lag', + attributeKey, + searchText: searchText ?? '', + filterAttributeKeyDataType: DataTypes.String, + tagType: 'tag', + }); + + if (payload) { + const values = Object.values(payload).find((el) => !!el) || []; + const options: DefaultOptionType[] = values.map((val: string) => ({ + label: val, + value: val, + })); + return options; + } + return []; + }, + ); + + return { options: data ?? [], isFetching: isLoading }; +} diff --git a/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss b/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss new file mode 100644 index 0000000000..cdf3e0023a --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss @@ -0,0 +1,424 @@ +.messaging-queue-container { + .messaging-breadcrumb { + display: flex; + padding: 0px 16px; + align-items: center; + gap: 8px; + padding-top: 10px; + padding-bottom: 8px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.07px; + + border-bottom: 1px solid var(--bg-slate-400); + + .message-queue-text { + cursor: pointer; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.07px; + } + } + + .messaging-header { + display: flex; + min-height: 48px; + padding: 10px 16px; + justify-content: space-between; + align-items: center; + + color: var(--bg-vanilla-400); + font-family: 'Geist Mono'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.25px; + + border-bottom: 1px solid var(--bg-slate-500); + + .header-config { + display: flex; + gap: 10px; + align-items: center; + + .messaging-queue-options { + .ant-select-selector { + display: flex; + height: 32px; + padding: 6px 6px 6px 8px; + align-items: center; + gap: 4px; + + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + } + } + } + } + + .messaging-queue-main-graph { + display: flex; + padding: 24px 16px; + flex-direction: column; + gap: 16px; + + .config-options { + display: flex; + align-items: center; + gap: 8px; + + .config-select-option { + .ant-select-selector { + display: flex; + min-height: 32px; + align-items: center; + gap: 16px; + min-width: 164px; + + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + } + } + } + + .mq-graph { + height: 420px; + padding: 24px 24px 0 24px; + } + + border-bottom: 1px solid var(--bg-slate-500); + } + + .messaging-queue-details { + display: flex; + padding: 16px; + + .mq-details-options { + letter-spacing: -0.06px; + .ant-radio-button-wrapper { + border-color: var(--bg-slate-400); + color: var(--bg-vanilla-400); + } + .ant-radio-button-wrapper-checked { + background: var(--bg-slate-400); + color: var(--bg-vanilla-100); + } + .ant-radio-button-wrapper-disabled { + background: var(--bg-ink-400); + color: var(--bg-slate-200); + } + .ant-radio-button-wrapper::before { + width: 0px; + } + + .disabled-option { + .coming-soon { + margin-left: 8px; + } + } + } + } +} + +.messaging-queue-options-popup { + width: 260px !important; +} + +.messaging-overview { + padding: 24px 16px 10px 16px; + + .overview-text { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.08px; + margin: 0; + } + + .overview-subtext { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.07px; + margin: 0; + margin-top: 4px; + } + + .overview-doc-area { + margin: 16px 0 28px 0; + display: flex; + + .middle-card { + border-left: none !important; + border-right: none !important; + } + + .overview-info-card { + display: flex; + width: 376px; + min-height: 176px; + padding: 18px 20px 20px 20px; + flex-direction: column; + justify-content: space-between; + border: 1px solid var(--bg-slate-500); + border-radius: 2px; + + .card-title { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 600; + line-height: 22px; + letter-spacing: 0.52px; + text-transform: uppercase; + margin: 0; + } + + .card-info-text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.07px; + margin: 0; + margin-top: 4px; + } + + .button-grp { + display: flex; + gap: 8px; + + .ant-btn { + min-width: 80px; + } + + .ant-btn-default { + background-color: var(--bg-slate-400); + border: none; + box-shadow: none; + } + } + } + } + + .summary-section { + display: flex; + + .summary-card { + display: flex; + padding: 12px; + flex-direction: column; + align-items: flex-start; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + width: 337px; + height: 283px; + border-radius: 2px; + + .summary-title { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: 24px; + + > p { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 500; /* 169.231% */ + letter-spacing: 0.52px; + text-transform: uppercase; + margin: 0; + } + + .time-value { + display: flex; + gap: 4px; + align-items: center; + + > p { + color: var(--bg-slate-200); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 22px; + letter-spacing: 0.48px; + text-transform: uppercase; + } + } + } + + .view-detail-btn { + height: 100%; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + } + } + } + + .coming-soon-card { + background: var(--bg-ink-500) !important; + border-left: none !important; + } +} + +.overview-confirm-modal { + background-color: var(--bg-ink-500); + padding: 0; + border-radius: 4px; + + .ant-modal-content { + background-color: var(--bg-ink-300); + .ant-modal-confirm-content { + color: var(--bg-vanilla-100); + } + + .ant-modal-confirm-body-wrapper { + display: flex; + flex-direction: column; + height: 150px; + justify-content: space-between; + } + } +} + +.lightMode { + .messaging-queue-container { + .messaging-breadcrumb { + color: var(--bg-ink-400); + border-bottom: 1px solid var(--bg-vanilla-300); + } + .messaging-header { + color: var(--bg-ink-400); + border-bottom: 1px solid var(--bg-vanilla-300); + + .header-config { + .messaging-queue-options { + .ant-select-selector { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + } + } + } + + .messaging-queue-main-graph { + .config-options { + .config-select-option { + .ant-select-selector { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + } + } + + border-bottom: 1px solid var(--bg-vanilla-300); + } + + .messaging-queue-details { + .mq-details-options { + .ant-radio-button-wrapper { + border-color: var(--bg-vanilla-300); + color: var(--bg-slate-200); + } + .ant-radio-button-wrapper-checked { + color: var(--bg-slate-200); + background: var(--bg-vanilla-300); + } + .ant-radio-button-wrapper-disabled { + background: var(--bg-vanilla-100); + color: var(--bg-vanilla-400); + } + } + } + } + + .messaging-overview { + .overview-text { + color: var(--bg-slate-200); + } + + .overview-subtext { + color: var(--bg-slate-300); + } + + .overview-doc-area { + .overview-info-card { + border: 1px solid var(--bg-vanilla-300); + + .card-title { + color: var(--bg-slate-200); + } + + .card-info-text { + color: var(--bg-slate-300); + } + + .button-grp { + .ant-btn-default { + background-color: var(--bg-vanilla-100); + } + } + } + } + + .summary-section { + .summary-card { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .summary-title { + > p { + color: var(--bg-slate-200); + } + + .time-value { + > p { + color: var(--bg-slate-200); + } + } + } + } + } + + .coming-soon-card { + background: var(--bg-vanilla-200) !important; + } + } + + .overview-confirm-modal { + background-color: var(--bg-vanilla-100); + + .ant-modal-content { + background-color: var(--bg-vanilla-100); + .ant-modal-confirm-content { + color: var(--bg-slate-200); + } + } + } +} diff --git a/frontend/src/pages/MessagingQueues/MessagingQueues.tsx b/frontend/src/pages/MessagingQueues/MessagingQueues.tsx new file mode 100644 index 0000000000..0a7d6eebdd --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MessagingQueues.tsx @@ -0,0 +1,171 @@ +import './MessagingQueues.styles.scss'; + +import { ExclamationCircleFilled } from '@ant-design/icons'; +import { Color } from '@signozhq/design-tokens'; +import { Button, Modal } from 'antd'; +import ROUTES from 'constants/routes'; +import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; +import { Calendar, ListMinus } from 'lucide-react'; +import { useHistory } from 'react-router-dom'; +import { isCloudUser } from 'utils/app'; + +import { KAFKA_SETUP_DOC_LINK } from './MessagingQueuesUtils'; +import { ComingSoon } from './MQCommon/MQCommon'; + +function MessagingQueues(): JSX.Element { + const history = useHistory(); + + const { confirm } = Modal; + + const showConfirm = (): void => { + confirm({ + icon: , + content: + 'Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.', + className: 'overview-confirm-modal', + onOk() { + history.push(ROUTES.MESSAGING_QUEUES_DETAIL); + }, + okText: 'Proceed', + }); + }; + + const isCloudUserVal = isCloudUser(); + + const getStartedRedirect = (link: string): void => { + if (isCloudUserVal) { + history.push(link); + } else { + window.open(KAFKA_SETUP_DOC_LINK, '_blank'); + } + }; + + return ( +
+
+ + Messaging Queues +
+
+
Kafka / Overview
+ +
+
+

+ Start sending data in as little as 20 minutes +

+

Connect and Monitor Your Data Streams

+
+
+
+

Configure Consumer

+

+ Connect your consumer and producer data sources to start monitoring. +

+
+
+ +
+
+
+
+

Configure Producer

+

+ Connect your consumer and producer data sources to start monitoring. +

+
+
+ +
+
+
+
+

Monitor kafka

+

+ Set up your Kafka monitoring to track consumer and producer activities. +

+
+
+ +
+
+
+
+
+
+

Consumer Lag

+
+ +

1D

+
+
+
+ +
+
+
+
+

Avg. Partition latency

+
+ +

1D

+
+
+
+ +
+
+
+
+

Avg. Partition latency

+
+ +

1D

+
+
+
+ +
+
+
+
+

Avg. Partition latency

+
+ +

1D

+
+
+
+ +
+
+
+
+
+ ); +} + +export default MessagingQueues; diff --git a/frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts b/frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts new file mode 100644 index 0000000000..91501e9c5c --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts @@ -0,0 +1,206 @@ +import { QueryParams } from 'constants/query'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types'; +import { History, Location } from 'history'; +import { isEmpty } from 'lodash-es'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { EQueryType } from 'types/common/dashboard'; +import { DataSource } from 'types/common/queryBuilder'; +import { v4 as uuid } from 'uuid'; + +export const KAFKA_SETUP_DOC_LINK = + 'https://github.com/shivanshuraj1333/kafka-opentelemetry-instrumentation/tree/master'; + +export function convertToTitleCase(text: string): string { + return text + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); +} + +export type RowData = { + key: string | number; + [key: string]: string | number; +}; + +export enum ConsumerLagDetailType { + ConsumerDetails = 'consumer-details', + ProducerDetails = 'producer-details', + NetworkLatency = 'network-latency', + PartitionHostMetrics = 'partition-host-metric', +} + +export const ConsumerLagDetailTitle: Record = { + 'consumer-details': 'Consumer Groups Details', + 'producer-details': 'Producer Details', + 'network-latency': 'Network Latency', + 'partition-host-metric': 'Partition Host Metrics', +}; + +export function createWidgetFilterItem( + key: string, + value: string, +): TagFilterItem { + const id = `${key}--string--tag--false`; + + return { + id: uuid(), + key: { + key, + dataType: DataTypes.String, + type: 'tag', + isColumn: false, + isJSON: false, + id, + }, + op: '=', + value, + }; +} + +export function getFiltersFromConfigOptions( + consumerGrp?: string, + topic?: string, + partition?: string, +): TagFilterItem[] { + const configOptions = [ + { key: 'group', values: consumerGrp?.split(',') }, + { key: 'topic', values: topic?.split(',') }, + { key: 'partition', values: partition?.split(',') }, + ]; + return configOptions.reduce( + (accumulator, { key, values }) => { + if (values && !isEmpty(values.filter((item) => item !== ''))) { + accumulator.push( + ...values.map((value) => createWidgetFilterItem(key, value)), + ); + } + return accumulator; + }, + [], + ); +} + +export function getWidgetQuery({ + filterItems, +}: { + filterItems: TagFilterItem[]; +}): GetWidgetQueryBuilderProps { + return { + title: 'Consumer Lag', + panelTypes: PANEL_TYPES.TIME_SERIES, + fillSpans: false, + yAxisUnit: 'none', + query: { + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: { + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'kafka_consumer_group_lag--float64--Gauge--true', + isColumn: true, + isJSON: false, + key: 'kafka_consumer_group_lag', + type: 'Gauge', + }, + aggregateOperator: 'max', + dataSource: DataSource.METRICS, + disabled: false, + expression: 'A', + filters: { + items: filterItems || [], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'group--string--tag--false', + isColumn: false, + isJSON: false, + key: 'group', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'topic--string--tag--false', + isColumn: false, + isJSON: false, + key: 'topic', + type: 'tag', + }, + { + dataType: DataTypes.String, + id: 'partition--string--tag--false', + isColumn: false, + isJSON: false, + key: 'partition', + type: 'tag', + }, + ], + having: [], + legend: '{{group}}-{{topic}}-{{partition}}', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'avg', + stepInterval: 60, + timeAggregation: 'max', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [], + id: uuid(), + }, + }; +} + +export const convertToNanoseconds = (timestamp: number): bigint => + BigInt((timestamp * 1e9).toFixed(0)); + +export const getStartAndEndTimesInMilliseconds = ( + timestamp: number, +): { start: number; end: number } => { + const FIVE_MINUTES_IN_MILLISECONDS = 5 * 60 * 1000; // 5 minutes in milliseconds - check with Shivanshu once + + const start = Math.floor(timestamp); + const end = Math.floor(start + FIVE_MINUTES_IN_MILLISECONDS); + + return { start, end }; +}; + +export interface SelectedTimelineQuery { + group?: string; + partition?: string; + topic?: string; + start?: number; + end?: number; +} + +export function setSelectedTimelineQuery( + urlQuery: URLSearchParams, + timestamp: number, + location: Location, + history: History, + data?: { + [key: string]: string; + }, +): void { + const selectedTimelineQuery: SelectedTimelineQuery = { + group: data?.group, + partition: data?.partition, + topic: data?.topic, + ...getStartAndEndTimesInMilliseconds(timestamp), + }; + urlQuery.set( + QueryParams.selectedTimelineQuery, + encodeURIComponent(JSON.stringify(selectedTimelineQuery)), + ); + const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; + history.replace(generatedUrl); +} diff --git a/frontend/src/pages/MessagingQueues/index.tsx b/frontend/src/pages/MessagingQueues/index.tsx new file mode 100644 index 0000000000..cc59152b17 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/index.tsx @@ -0,0 +1,3 @@ +import MessagingQueues from './MessagingQueues'; + +export default MessagingQueues; diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 83f8370944..1845e77941 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -52,6 +52,8 @@ export const routePermission: Record = { ALL_CHANNELS: ['ADMIN', 'EDITOR', 'VIEWER'], INGESTION_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'], ALL_DASHBOARD: ['ADMIN', 'EDITOR', 'VIEWER'], + MESSAGING_QUEUES: ['ADMIN', 'EDITOR', 'VIEWER'], + MESSAGING_QUEUES_DETAIL: ['ADMIN', 'EDITOR', 'VIEWER'], ALL_ERROR: ['ADMIN', 'EDITOR', 'VIEWER'], APPLICATION: ['ADMIN', 'EDITOR', 'VIEWER'], CHANNELS_EDIT: ['ADMIN'], From 15b0569b56bf3c904fa5313d72c50f2b0f0a3da4 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Thu, 29 Aug 2024 16:47:27 +0530 Subject: [PATCH 52/60] fix: show add credit card modal only for cloud users (#5797) --- frontend/src/container/AppLayout/index.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 3ee58efd7c..e821e67104 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -47,6 +47,7 @@ import { UPDATE_LATEST_VERSION_ERROR, } from 'types/actions/app'; import AppReducer from 'types/reducer/app'; +import { isCloudUser } from 'utils/app'; import { getFormattedDate, getRemainingDays } from 'utils/timeUtils'; import { ChildrenContainer, Layout, LayoutContent } from './styles'; @@ -71,7 +72,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const isPremiumChatSupportEnabled = useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false; + const isChatSupportEnabled = + useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false; + + const isCloudUserVal = isCloudUser(); + const showAddCreditCardModal = + isChatSupportEnabled && + isCloudUserVal && !isPremiumChatSupportEnabled && !licenseData?.payload?.trialConvertedToSubscription; From fb92ddc82266ac56f6926f7fb3a70be58bce80b3 Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:48:08 +0530 Subject: [PATCH 53/60] chore: added trace detail tests (#5523) * feat: added trace filter test cases * feat: added trace filter test cases - initial render * feat: added test cases - query sync, filter section behaviour etc * feat: deleted mock-data files * feat: added test cases of undefined filters and items * feat: deleted tsconfig * feat: added clear and rest btn test cases for traces filters * feat: added collapse and uncollapse test for traces filters * chore: added trace detail tests * chore: added trace detail tests - span selection, focus and reset * chore: fixed eslint --- frontend/src/container/TraceDetail/index.tsx | 13 +- .../mocks-server/__mockdata__/tracedetail.ts | 2089 +++++++++++++++++ frontend/src/mocks-server/handlers.ts | 7 + .../TraceDetail/__test__/TraceDetail.test.tsx | 214 ++ 4 files changed, 2321 insertions(+), 2 deletions(-) create mode 100644 frontend/src/mocks-server/__mockdata__/tracedetail.ts create mode 100644 frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx diff --git a/frontend/src/container/TraceDetail/index.tsx b/frontend/src/container/TraceDetail/index.tsx index 38f6db7c08..096b648acf 100644 --- a/frontend/src/container/TraceDetail/index.tsx +++ b/frontend/src/container/TraceDetail/index.tsx @@ -219,10 +219,18 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { - - @@ -262,6 +270,7 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { collapsedWidth={40} defaultCollapsed onCollapse={(value): void => setCollapsed(value)} + data-testid="span-details-sider" > {!collapsed && ( diff --git a/frontend/src/mocks-server/__mockdata__/tracedetail.ts b/frontend/src/mocks-server/__mockdata__/tracedetail.ts new file mode 100644 index 0000000000..c0e439f65f --- /dev/null +++ b/frontend/src/mocks-server/__mockdata__/tracedetail.ts @@ -0,0 +1,2089 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +export const traceDetailResponse = [ + { + startTimestampMillis: 1721304358677, + endTimestampMillis: 1721304360928, + columns: [ + '__time', + 'SpanId', + 'TraceId', + 'ServiceName', + 'Name', + 'Kind', + 'DurationNano', + 'TagsKeys', + 'TagsValues', + 'References', + 'Events', + 'HasError', + 'StatusMessage', + 'StatusCodeString', + 'SpanKind', + ], + events: [ + [ + 1721304359454, + '74fff7b42cbc923b', + '000000000000000071dc9b0a338729b4', + 'customer', + 'HTTP GET /customer', + '2', + '348950000', + [ + 'signoz.collector.id', + 'client-uuid', + 'component', + 'host.name', + 'http.method', + 'http.url', + 'ip', + 'http.status_code', + 'opencensus.exporterversion', + 'service.name', + ], + [ + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '7cd5f22c2034bc4e', + 'net/http', + '4f6ec470feea', + 'GET', + '/customer?customer=731', + '172.25.0.2', + '200', + 'Jaeger-Go-2.30.0', + 'customer', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=1518282c4475a6d7, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304359454282000,"attributeMap":{"level":"info","method":"GET","url":"/customer?customer=731"}}', + '{"name":"Loading customer","timeUnixNano":1721304359454308000,"attributeMap":{"customer_id":"731","level":"info"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304359454, + '1518282c4475a6d7', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '349380000', + [ + 'net/http.was_idle', + 'client-uuid', + 'host.name', + 'http.status_code', + 'net/http.reused', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'component', + 'http.method', + 'http.url', + 'ip', + ], + [ + 'true', + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '200', + 'true', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + 'net/http', + 'GET', + '0.0.0.0:8081', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=51a3062c3456cd26, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304359454120000}', + '{"name":"GotConn","timeUnixNano":1721304359454129000}', + '{"name":"WroteHeaders","timeUnixNano":1721304359454156000}', + '{"name":"WroteRequest","timeUnixNano":1721304359454158000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304359803413000}', + '{"name":"PutIdleConn","timeUnixNano":1721304359803452000}', + '{"name":"ClosedBody","timeUnixNano":1721304359803472000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359454, + '51a3062c3456cd26', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /customer', + '0', + '349435000', + [ + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + ], + [ + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304359454, + '67e901d53c8f11d4', + '000000000000000071dc9b0a338729b4', + 'mysql', + 'SQL SELECT', + '3', + '348855000', + [ + 'host.name', + 'peer.service', + 'request', + 'signoz.collector.id', + 'sql.query', + 'client-uuid', + 'ip', + 'opencensus.exporterversion', + 'service.name', + ], + [ + '4f6ec470feea', + 'mysql', + '', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + 'SELECT * FROM customer WHERE customer_id=731', + '3441c794741337d0', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'mysql', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=74fff7b42cbc923b, RefType=CHILD_OF}', + ], + [ + '{"name":"Waiting for lock behind 1 transactions","timeUnixNano":1721304359454335000,"attributeMap":{"blockers":"[]"}}', + '{"name":"Acquired lock with 0 transactions waiting behind","timeUnixNano":1721304359529107000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359803, + '541274ec900e06ae', + '000000000000000071dc9b0a338729b4', + 'redis', + 'FindDriverIDs', + '3', + '15030000', + [ + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.location', + ], + [ + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + '728,326', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [ + '{"name":"Found drivers","timeUnixNano":1721304359818819000,"attributeMap":{"level":"info"}}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359818, + '14a6cad2442f62ae', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '7524000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + ], + [ + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T746480C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359826, + '49771a34b0287913', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '10207000', + [ + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'T787177C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359836, + '53e1096a98361b25', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '32032000', + [ + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'T718678C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [ + '{"name":"redis timeout","timeUnixNano":1721304359868650000,"attributeMap":{"driver_id":"T718678C","error":"redis timeout","level":"error"}}', + ], + true, + '', + 'Error', + 'Client', + ], + [ + 1721304359868, + '62f28274cd882905', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '9969000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + ], + [ + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T718678C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359878, + '273c24835b4733f7', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '6974000', + [ + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + ], + [ + 'Jaeger-Go-2.30.0', + 'T778818C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359885, + '1fee566b7d89250d', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '11594000', + [ + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + ], + [ + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T730205C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359897, + '6b7bfcbede8a2e10', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '9198000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + ], + [ + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T708669C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359906, + '707b2e18e9f4b36d', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '28706000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + ], + [ + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T740153C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [ + '{"name":"redis timeout","timeUnixNano":1721304359935174000,"attributeMap":{"driver_id":"T740153C","error":"redis timeout","level":"error"}}', + ], + true, + '', + 'Error', + 'Client', + ], + [ + 1721304359935, + '759867234892fa23', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '10115000', + [ + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + ], + [ + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T740153C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359945, + '40e021cf08a5849d', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '13283000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + ], + [ + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'T739323C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359958, + '53a1db7583207833', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '7573000', + [ + 'opencensus.exporterversion', + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + ], + [ + 'Jaeger-Go-2.30.0', + 'T731135C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359966, + '46aaaa6f2a2fc84f', + '000000000000000071dc9b0a338729b4', + 'redis', + 'GetDriver', + '3', + '9238000', + [ + 'param.driverID', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'T760263C', + 'redis', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '1b45eecbdc9fca62', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=22eadd8a0f27a552, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359803, + '22eadd8a0f27a552', + '000000000000000071dc9b0a338729b4', + 'driver', + '/driver.DriverService/FindNearest', + '2', + '172564000', + [ + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'host.name', + 'ip', + ], + [ + 'Jaeger-Go-2.30.0', + 'driver', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '46ade24719771fcb', + 'gRPC', + '4f6ec470feea', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=0b67082081caa21d, RefType=CHILD_OF}', + ], + [ + '{"name":"Searching for nearby drivers","timeUnixNano":1721304359803853000,"attributeMap":{"level":"info","location":"728,326"}}', + '{"name":"Retrying GetDriver after error","timeUnixNano":1721304359868728000,"attributeMap":{"error":"redis timeout","level":"error","retry_no":"1"}}', + '{"name":"Retrying GetDriver after error","timeUnixNano":1721304359935265000,"attributeMap":{"error":"redis timeout","level":"error","retry_no":"1"}}', + '{"name":"Search successful","timeUnixNano":1721304359975617000,"attributeMap":{"level":"info","num_drivers":"10"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304359803, + '0b67082081caa21d', + '000000000000000071dc9b0a338729b4', + 'frontend', + '/driver.DriverService/FindNearest', + '3', + '173105000', + [ + 'component', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + ], + [ + 'gRPC', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359978, + '3ed7b128c9d765bf', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '54072000', + [ + 'component', + 'http.method', + 'http.status_code', + 'http.url', + 'net/http.reused', + 'net/http.was_idle', + 'service.name', + 'client-uuid', + 'signoz.collector.id', + 'ip', + 'opencensus.exporterversion', + 'host.name', + ], + [ + 'net/http', + 'GET', + '200', + '0.0.0.0:8083', + 'true', + 'true', + 'frontend', + '6fb81b8ca91b2b4d', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + '4f6ec470feea', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=0ac9396a0836beef, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304359978461000}', + '{"name":"GotConn","timeUnixNano":1721304359978470000}', + '{"name":"WroteHeaders","timeUnixNano":1721304359978490000}', + '{"name":"WroteRequest","timeUnixNano":1721304359978492000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360032438000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360032487000}', + '{"name":"ClosedBody","timeUnixNano":1721304360032511000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359978, + '0ac9396a0836beef', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '54081000', + [ + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304359978, + '2b837048c74fc169', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '64945000', + [ + 'ip', + 'service.name', + 'client-uuid', + 'component', + 'http.method', + 'http.status_code', + 'opencensus.exporterversion', + 'signoz.collector.id', + 'host.name', + 'http.url', + 'net/http.reused', + 'net/http.was_idle', + ], + [ + '172.25.0.2', + 'frontend', + '6fb81b8ca91b2b4d', + 'net/http', + 'GET', + '200', + 'Jaeger-Go-2.30.0', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '4f6ec470feea', + '0.0.0.0:8083', + 'false', + 'false', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=1e320f1c93026e9d, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304359979001000}', + '{"name":"ConnectStart","timeUnixNano":1721304359979065000,"attributeMap":{"addr":"0.0.0.0:8083","network":"tcp"}}', + '{"name":"ConnectDone","timeUnixNano":1721304359979262000,"attributeMap":{"addr":"0.0.0.0:8083","network":"tcp"}}', + '{"name":"GotConn","timeUnixNano":1721304359979287000}', + '{"name":"WroteHeaders","timeUnixNano":1721304359979341000}', + '{"name":"WroteRequest","timeUnixNano":1721304359979342000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360043858000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360043897000}', + '{"name":"ClosedBody","timeUnixNano":1721304360043919000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359978, + '1e320f1c93026e9d', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '64954000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304359977, + '3d559df27da11f0f', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '76137000', + [ + 'net/http.reused', + 'net/http.was_idle', + 'component', + 'host.name', + 'http.url', + 'ip', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'http.method', + 'http.status_code', + 'opencensus.exporterversion', + ], + [ + 'true', + 'true', + 'net/http', + '4f6ec470feea', + '0.0.0.0:8083', + '172.25.0.2', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + 'GET', + '200', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=6acf3a3fd51f294a, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304359977905000}', + '{"name":"GotConn","timeUnixNano":1721304359977914000}', + '{"name":"WroteHeaders","timeUnixNano":1721304359977939000}', + '{"name":"WroteRequest","timeUnixNano":1721304359977941000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360053941000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360053982000}', + '{"name":"ClosedBody","timeUnixNano":1721304360054009000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304359977, + '6acf3a3fd51f294a', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '76161000', + [ + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + ], + [ + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360033, + '4f50f3f099502699', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '69683000', + [ + 'client-uuid', + 'component', + 'http.status_code', + 'http.url', + 'net/http.reused', + 'net/http.was_idle', + 'signoz.collector.id', + 'host.name', + 'http.method', + 'ip', + 'opencensus.exporterversion', + 'service.name', + ], + [ + '6fb81b8ca91b2b4d', + 'net/http', + '200', + '0.0.0.0:8083', + 'true', + 'true', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '4f6ec470feea', + 'GET', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=0f064e2af29e5e3d, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360033336000}', + '{"name":"GotConn","timeUnixNano":1721304360033345000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360033369000}', + '{"name":"WroteRequest","timeUnixNano":1721304360033372000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360102925000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360102965000}', + '{"name":"ClosedBody","timeUnixNano":1721304360102988000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360033, + '0f064e2af29e5e3d', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '69697000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360054, + '1968d4e502b91ac4', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '54187000', + [ + 'client-uuid', + 'component', + 'http.status_code', + 'net/http.was_idle', + 'opencensus.exporterversion', + 'signoz.collector.id', + 'host.name', + 'http.method', + 'http.url', + 'ip', + 'net/http.reused', + 'service.name', + ], + [ + '6fb81b8ca91b2b4d', + 'net/http', + '200', + 'true', + 'Jaeger-Go-2.30.0', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '4f6ec470feea', + 'GET', + '0.0.0.0:8083', + '172.25.0.2', + 'true', + 'frontend', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=4984e1953fd99760, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360054823000}', + '{"name":"GotConn","timeUnixNano":1721304360054831000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360054855000}', + '{"name":"WroteRequest","timeUnixNano":1721304360054857000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360108921000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360108961000}', + '{"name":"ClosedBody","timeUnixNano":1721304360108984000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360054, + '4984e1953fd99760', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '54200000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360044, + '032263185bbbf38c', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '69252000', + [ + 'host.name', + 'ip', + 'net/http.was_idle', + 'service.name', + 'client-uuid', + 'component', + 'http.method', + 'http.status_code', + 'http.url', + 'net/http.reused', + 'opencensus.exporterversion', + 'signoz.collector.id', + ], + [ + '4f6ec470feea', + '172.25.0.2', + 'true', + 'frontend', + '6fb81b8ca91b2b4d', + 'net/http', + 'GET', + '200', + '0.0.0.0:8083', + 'true', + 'Jaeger-Go-2.30.0', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=54965682749b13bf, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360044759000}', + '{"name":"GotConn","timeUnixNano":1721304360044768000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360044793000}', + '{"name":"WroteRequest","timeUnixNano":1721304360044796000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360113908000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360113948000}', + '{"name":"ClosedBody","timeUnixNano":1721304360113971000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360044, + '54965682749b13bf', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '69266000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360109, + '255866d4d4b6ec08', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '41317000', + [ + 'component', + 'host.name', + 'http.method', + 'http.status_code', + 'net/http.reused', + 'net/http.was_idle', + 'opencensus.exporterversion', + 'client-uuid', + 'http.url', + 'ip', + 'service.name', + 'signoz.collector.id', + ], + [ + 'net/http', + '4f6ec470feea', + 'GET', + '200', + 'true', + 'true', + 'Jaeger-Go-2.30.0', + '6fb81b8ca91b2b4d', + '0.0.0.0:8083', + '172.25.0.2', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=690f0b793f302403, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360109790000}', + '{"name":"GotConn","timeUnixNano":1721304360109798000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360109823000}', + '{"name":"WroteRequest","timeUnixNano":1721304360109826000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360151018000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360151059000}', + '{"name":"ClosedBody","timeUnixNano":1721304360151081000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360109, + '690f0b793f302403', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '41332000', + [ + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360103, + '26fbd8f91831c199', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '48308000', + [ + 'http.method', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'http.url', + 'net/http.reused', + 'net/http.was_idle', + 'host.name', + 'http.status_code', + ], + [ + 'GET', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + 'net/http', + '0.0.0.0:8083', + 'true', + 'true', + '4f6ec470feea', + '200', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=55e1875d184a70fe, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360103810000}', + '{"name":"GotConn","timeUnixNano":1721304360103818000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360103842000}', + '{"name":"WroteRequest","timeUnixNano":1721304360103844000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360152042000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360152070000}', + '{"name":"ClosedBody","timeUnixNano":1721304360152091000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360103, + '55e1875d184a70fe', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '48325000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360114, + '2903e6c8cbe5f8c6', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '65640000', + [ + 'http.status_code', + 'ip', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'http.method', + 'net/http.was_idle', + 'opencensus.exporterversion', + 'service.name', + 'host.name', + 'http.url', + 'net/http.reused', + ], + [ + '200', + '172.25.0.2', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + 'net/http', + 'GET', + 'true', + 'Jaeger-Go-2.30.0', + 'frontend', + '4f6ec470feea', + '0.0.0.0:8083', + 'true', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=5bcb6dfccf311afe, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360114775000}', + '{"name":"GotConn","timeUnixNano":1721304360114783000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360114809000}', + '{"name":"WroteRequest","timeUnixNano":1721304360114812000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360180281000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360180362000}', + '{"name":"ClosedBody","timeUnixNano":1721304360180389000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360114, + '5bcb6dfccf311afe', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '65664000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304360151, + '3ed0b66560a03e14', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET', + '3', + '78546000', + [ + 'http.status_code', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'host.name', + 'http.method', + 'http.url', + 'ip', + 'net/http.reused', + 'net/http.was_idle', + 'opencensus.exporterversion', + ], + [ + '200', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '6fb81b8ca91b2b4d', + 'net/http', + '4f6ec470feea', + 'GET', + '0.0.0.0:8083', + '172.25.0.2', + 'true', + 'true', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=352ac0b61c8c826f, RefType=CHILD_OF}', + ], + [ + '{"name":"GetConn","timeUnixNano":1721304360151933000}', + '{"name":"GotConn","timeUnixNano":1721304360151942000}', + '{"name":"WroteHeaders","timeUnixNano":1721304360151967000}', + '{"name":"WroteRequest","timeUnixNano":1721304360151969000}', + '{"name":"GotFirstResponseByte","timeUnixNano":1721304360230321000}', + '{"name":"PutIdleConn","timeUnixNano":1721304360230365000}', + '{"name":"ClosedBody","timeUnixNano":1721304360230449000}', + ], + false, + '', + 'Unset', + 'Client', + ], + [ + 1721304360151, + '352ac0b61c8c826f', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET: /route', + '0', + '78561000', + [ + 'client-uuid', + 'host.name', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + ], + [ + '6fb81b8ca91b2b4d', + '4f6ec470feea', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'frontend', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=71dc9b0a338729b4, RefType=CHILD_OF}', + ], + [], + false, + '', + 'Unset', + 'Unspecified', + ], + [ + 1721304359453, + '71dc9b0a338729b4', + '000000000000000071dc9b0a338729b4', + 'frontend', + 'HTTP GET /dispatch', + '2', + '776756000', + [ + 'http.status_code', + 'http.url', + 'opencensus.exporterversion', + 'sampler.param', + 'client-uuid', + 'component', + 'host.name', + 'http.method', + 'signoz.collector.id', + 'ip', + 'sampler.type', + 'service.name', + ], + [ + '200', + '/dispatch?customer=731\u0026nonse=0.8022286220408668', + 'Jaeger-Go-2.30.0', + 'true', + '6fb81b8ca91b2b4d', + 'net/http', + '4f6ec470feea', + 'GET', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '172.25.0.2', + 'const', + 'frontend', + ], + ['{TraceId=000000000000000071dc9b0a338729b4, SpanId=, RefType=CHILD_OF}'], + [ + '{"name":"HTTP request received","timeUnixNano":1721304359453904000,"attributeMap":{"level":"info","method":"GET","url":"/dispatch?customer=731\\u0026nonse=0.8022286220408668"}}', + '{"name":"Getting customer","timeUnixNano":1721304359453990000,"attributeMap":{"customer_id":"731","level":"info"}}', + '{"name":"Found customer","timeUnixNano":1721304359803486000,"attributeMap":{"level":"info"}}', + '{"name":"baggage","timeUnixNano":1721304359803546000,"attributeMap":{"key":"customer","value":"Japanese Desserts"}}', + '{"name":"Finding nearest drivers","timeUnixNano":1721304359803551000,"attributeMap":{"level":"info","location":"728,326"}}', + '{"name":"Found drivers","timeUnixNano":1721304359976695000,"attributeMap":{"level":"info"}}', + '{"name":"Finding route","timeUnixNano":1721304359977409000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"165,543"}}', + '{"name":"Finding route","timeUnixNano":1721304359977989000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"720,72"}}', + '{"name":"Finding route","timeUnixNano":1721304359978530000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"530,911"}}', + '{"name":"Finding route","timeUnixNano":1721304360032532000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"686,946"}}', + '{"name":"Finding route","timeUnixNano":1721304360043941000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"222,675"}}', + '{"name":"Finding route","timeUnixNano":1721304360054028000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"660,997"}}', + '{"name":"Finding route","timeUnixNano":1721304360103005000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"609,233"}}', + '{"name":"Finding route","timeUnixNano":1721304360109005000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"516,970"}}', + '{"name":"Finding route","timeUnixNano":1721304360113986000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"524,763"}}', + '{"name":"Finding route","timeUnixNano":1721304360151099000,"attributeMap":{"dropoff":"728,326","level":"info","pickup":"267,822"}}', + '{"name":"Found routes","timeUnixNano":1721304360230473000,"attributeMap":{"level":"info"}}', + '{"name":"Dispatch successful","timeUnixNano":1721304360230582000,"attributeMap":{"driver":"T718678C","eta":"2m0s","level":"info"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304359980, + '472ffaa577ce6b0e', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '52268000', + [ + 'component', + 'host.name', + 'http.url', + 'opencensus.exporterversion', + 'service.name', + 'client-uuid', + 'http.method', + 'http.status_code', + 'ip', + 'signoz.collector.id', + ], + [ + 'net/http', + '4f6ec470feea', + '/route?dropoff=728%2C326\u0026pickup=720%2C72', + 'Jaeger-Go-2.30.0', + 'route', + '64a18ffd5f8adbfb', + 'GET', + '200', + '172.25.0.2', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=3ed7b128c9d765bf, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304359980063000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=720%2C72"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304359979, + '2a835737138b4671', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '64209000', + [ + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'host.name', + 'http.url', + 'opencensus.exporterversion', + 'http.method', + 'http.status_code', + 'ip', + ], + [ + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + 'net/http', + '4f6ec470feea', + '/route?dropoff=728%2C326\u0026pickup=530%2C911', + 'Jaeger-Go-2.30.0', + 'GET', + '200', + '172.25.0.2', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=2b837048c74fc169, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304359979511000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=530%2C911"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304359980, + '28a8a67365d0bd8b', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '73145000', + [ + 'http.method', + 'http.status_code', + 'opencensus.exporterversion', + 'signoz.collector.id', + 'component', + 'host.name', + 'ip', + 'service.name', + 'client-uuid', + 'http.url', + ], + [ + 'GET', + '200', + 'Jaeger-Go-2.30.0', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + 'net/http', + '4f6ec470feea', + '172.25.0.2', + 'route', + '64a18ffd5f8adbfb', + '/route?dropoff=728%2C326\u0026pickup=165%2C543', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=3d559df27da11f0f, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304359980599000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=165%2C543"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360033, + '38de18f04671e5db', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '69230000', + [ + 'http.status_code', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'http.method', + 'host.name', + 'http.url', + ], + [ + '200', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + 'net/http', + 'GET', + '4f6ec470feea', + '/route?dropoff=728%2C326\u0026pickup=686%2C946', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=4f50f3f099502699, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360033555000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=686%2C946"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360054, + '3420998a476237ce', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '53811000', + [ + 'client-uuid', + 'host.name', + 'http.status_code', + 'http.url', + 'opencensus.exporterversion', + 'service.name', + 'component', + 'http.method', + 'ip', + 'signoz.collector.id', + ], + [ + '64a18ffd5f8adbfb', + '4f6ec470feea', + '200', + '/route?dropoff=728%2C326\u0026pickup=660%2C997', + 'Jaeger-Go-2.30.0', + 'route', + 'net/http', + 'GET', + '172.25.0.2', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=1968d4e502b91ac4, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360054989000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=660%2C997"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360044, + '0c5a7edd2e365047', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '68850000', + [ + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'ip', + 'http.method', + 'http.status_code', + 'http.url', + 'component', + 'host.name', + ], + [ + 'Jaeger-Go-2.30.0', + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + '172.25.0.2', + 'GET', + '200', + '/route?dropoff=728%2C326\u0026pickup=222%2C675', + 'net/http', + '4f6ec470feea', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=032263185bbbf38c, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360044934000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=222%2C675"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360103, + '538b56106b402474', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '46813000', + [ + 'http.url', + 'opencensus.exporterversion', + 'client-uuid', + 'component', + 'host.name', + 'http.method', + 'http.status_code', + 'ip', + 'service.name', + 'signoz.collector.id', + ], + [ + '/route?dropoff=728%2C326\u0026pickup=609%2C233', + 'Jaeger-Go-2.30.0', + '64a18ffd5f8adbfb', + 'net/http', + '4f6ec470feea', + 'GET', + '200', + '172.25.0.2', + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=26fbd8f91831c199, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360103976000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=609%2C233"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360109, + '668e5fc06e78796b', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '41012000', + [ + 'component', + 'http.method', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + 'host.name', + 'http.status_code', + 'http.url', + 'ip', + 'opencensus.exporterversion', + ], + [ + 'net/http', + 'GET', + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + '4f6ec470feea', + '200', + '/route?dropoff=728%2C326\u0026pickup=516%2C970', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=255866d4d4b6ec08, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360109961000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=516%2C970"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360114, + '7eda7467eedf3b92', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '65157000', + [ + 'host.name', + 'ip', + 'service.name', + 'http.status_code', + 'http.url', + 'opencensus.exporterversion', + 'signoz.collector.id', + 'client-uuid', + 'component', + 'http.method', + ], + [ + '4f6ec470feea', + '172.25.0.2', + 'route', + '200', + '/route?dropoff=728%2C326\u0026pickup=524%2C763', + 'Jaeger-Go-2.30.0', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + 'net/http', + 'GET', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=2903e6c8cbe5f8c6, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360114939000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=524%2C763"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + [ + 1721304360152, + '231d5099ef1d826f', + '000000000000000071dc9b0a338729b4', + 'route', + 'HTTP GET /route', + '2', + '77985000', + [ + 'host.name', + 'http.method', + 'http.status_code', + 'component', + 'http.url', + 'ip', + 'opencensus.exporterversion', + 'service.name', + 'signoz.collector.id', + 'client-uuid', + ], + [ + '4f6ec470feea', + 'GET', + '200', + 'net/http', + '/route?dropoff=728%2C326\u0026pickup=267%2C822', + '172.25.0.2', + 'Jaeger-Go-2.30.0', + 'route', + '70d440fb-4875-4371-9a13-1bf5117c99e7', + '64a18ffd5f8adbfb', + ], + [ + '{TraceId=000000000000000071dc9b0a338729b4, SpanId=3ed0b66560a03e14, RefType=CHILD_OF}', + ], + [ + '{"name":"HTTP request received","timeUnixNano":1721304360152182000,"attributeMap":{"level":"info","method":"GET","url":"/route?dropoff=728%2C326\\u0026pickup=267%2C822"}}', + ], + false, + '', + 'Unset', + 'Server', + ], + ], + isSubTree: false, + }, +]; diff --git a/frontend/src/mocks-server/handlers.ts b/frontend/src/mocks-server/handlers.ts index 22978717a5..55b5842612 100644 --- a/frontend/src/mocks-server/handlers.ts +++ b/frontend/src/mocks-server/handlers.ts @@ -13,6 +13,7 @@ import { membersResponse } from './__mockdata__/members'; import { queryRangeSuccessResponse } from './__mockdata__/query_range'; import { serviceSuccessResponse } from './__mockdata__/services'; import { topLevelOperationSuccessResponse } from './__mockdata__/top_level_operations'; +import { traceDetailResponse } from './__mockdata__/tracedetail'; export const handlers = [ rest.post('http://localhost/api/v3/query_range', (req, res, ctx) => @@ -230,6 +231,12 @@ export const handlers = [ }), ), ), + + rest.get( + 'http://localhost/api/v1/traces/000000000000000071dc9b0a338729b4', + (req, res, ctx) => res(ctx.status(200), ctx.json(traceDetailResponse)), + ), + rest.post('http://localhost/api/v1//channels', (_, res, ctx) => res(ctx.status(200), ctx.json(allAlertChannels)), ), diff --git a/frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx b/frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx new file mode 100644 index 0000000000..b16bb6c735 --- /dev/null +++ b/frontend/src/pages/TraceDetail/__test__/TraceDetail.test.tsx @@ -0,0 +1,214 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import ROUTES from 'constants/routes'; +import { MemoryRouter, Route } from 'react-router-dom'; +import { fireEvent, render, screen } from 'tests/test-utils'; + +import TraceDetail from '..'; + +window.HTMLElement.prototype.scrollIntoView = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: (): { pathname: string; search: string } => ({ + pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.TRACE_DETAIL}`, + search: '?spanId=28a8a67365d0bd8b&levelUp=0&levelDown=0', + }), + + useParams: jest.fn().mockReturnValue({ + id: '000000000000000071dc9b0a338729b4', + }), +})); + +jest.mock('container/TraceFlameGraph/index.tsx', () => ({ + __esModule: true, + default: (): JSX.Element =>
TraceFlameGraph
, +})); + +describe('TraceDetail', () => { + it('should render tracedetail', async () => { + const { findByText, getByText, getAllByText, getByPlaceholderText } = render( + + + + + , + , + ); + expect(await findByText('Trace Details')).toBeInTheDocument(); + + // as we have an active spanId, it should scroll to the selected span + expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalled(); + + // assertions + expect(getByText('TraceFlameGraph')).toBeInTheDocument(); + expect(getByText('Focus on selected span')).toBeInTheDocument(); + + // span action buttons + expect(getByText('Reset Focus')).toBeInTheDocument(); + expect(getByText('50 Spans')).toBeInTheDocument(); + + // trace span detail - parent -> child + expect(getAllByText('frontend')[0]).toBeInTheDocument(); + expect(getByText('776.76 ms')).toBeInTheDocument(); + [ + { trace: 'HTTP GET /dispatch', duration: '776.76 ms', count: '50' }, + { trace: 'HTTP GET: /customer', duration: '349.44 ms', count: '4' }, + { + trace: '/driver.DriverService/FindNearest', + duration: '173.10 ms', + count: '15', + }, + // and so on ... + ].forEach((traceDetail) => { + expect(getByText(traceDetail.trace)).toBeInTheDocument(); + expect(getByText(traceDetail.duration)).toBeInTheDocument(); + expect(getByText(traceDetail.count)).toBeInTheDocument(); + }); + + // Details for selected Span + expect(getByText('Details for selected Span')).toBeInTheDocument(); + ['Service', 'Operation', 'SpanKind', 'StatusCodeString'].forEach((detail) => { + expect(getByText(detail)).toBeInTheDocument(); + }); + + // go to related logs button + const goToRelatedLogsButton = getByText('Go to Related logs'); + expect(goToRelatedLogsButton).toBeInTheDocument(); + + // Tag and Event tabs + expect(getByText('Tags')).toBeInTheDocument(); + expect(getByText('Events')).toBeInTheDocument(); + expect(getByPlaceholderText('traceDetails:search_tags')).toBeInTheDocument(); + + // Tag details + [ + { title: 'client-uuid', value: '64a18ffd5f8adbfb' }, + { title: 'component', value: 'net/http' }, + { title: 'host.name', value: '4f6ec470feea' }, + { title: 'http.method', value: 'GET' }, + { title: 'http.url', value: '/route?dropoff=728%2C326&pickup=165%2C543' }, + { title: 'http.status_code', value: '200' }, + { title: 'ip', value: '172.25.0.2' }, + { title: 'opencensus.exporterversion', value: 'Jaeger-Go-2.30.0' }, + ].forEach((tag) => { + expect(getByText(tag.title)).toBeInTheDocument(); + expect(getByText(tag.value)).toBeInTheDocument(); + }); + + // see full value + expect(getAllByText('View full value')[0]).toBeInTheDocument(); + }); + + it('should render tracedetail events tab', async () => { + const { findByText, getByText } = render( + + + + + , + , + ); + expect(await findByText('Trace Details')).toBeInTheDocument(); + + fireEvent.click(getByText('Events')); + + expect(await screen.findByText('HTTP request received')).toBeInTheDocument(); + + // event details + [ + { title: 'Event Start Time', value: '527.60 ms' }, + { title: 'level', value: 'info' }, + ].forEach((tag) => { + expect(getByText(tag.title)).toBeInTheDocument(); + expect(getByText(tag.value)).toBeInTheDocument(); + }); + + expect(getByText('View full log event message')).toBeInTheDocument(); + }); + + it('should toggle slider - selected span details', async () => { + const { findByTestId, queryByText } = render( + + + + + , + , + ); + const slider = await findByTestId('span-details-sider'); + expect(slider).toBeInTheDocument(); + + fireEvent.click( + slider.querySelector('.ant-layout-sider-trigger') as HTMLElement, + ); + + expect(queryByText('Details for selected Span')).not.toBeInTheDocument(); + }); + + it('should be able to selected another span and see its detail', async () => { + const { getByText } = render( + + + + + , + , + ); + + expect(await screen.findByText('Trace Details')).toBeInTheDocument(); + + const spanTitle = getByText('/driver.DriverService/FindNearest'); + expect(spanTitle).toBeInTheDocument(); + fireEvent.click(spanTitle); + + // Tag details + [ + { title: 'client-uuid', value: '6fb81b8ca91b2b4d' }, + { title: 'component', value: 'gRPC' }, + { title: 'host.name', value: '4f6ec470feea' }, + ].forEach((tag) => { + expect(getByText(tag.title)).toBeInTheDocument(); + expect(getByText(tag.value)).toBeInTheDocument(); + }); + }); + + it('focus on selected span and reset focus action', async () => { + const { getByText, getAllByText } = render( + + + + + , + , + ); + + expect(await screen.findByText('Trace Details')).toBeInTheDocument(); + + const spanTitle = getByText('/driver.DriverService/FindNearest'); + expect(spanTitle).toBeInTheDocument(); + fireEvent.click(spanTitle); + + expect(await screen.findByText('6fb81b8ca91b2b4d')).toBeInTheDocument(); + + // focus on selected span + const focusButton = getByText('Focus on selected span'); + expect(focusButton).toBeInTheDocument(); + fireEvent.click(focusButton); + + // assert selected span + expect(getByText('15 Spans')).toBeInTheDocument(); + expect(getAllByText('/driver.DriverService/FindNearest')).toHaveLength(3); + expect(getByText('173.10 ms')).toBeInTheDocument(); + + // reset focus + expect(screen.queryByText('HTTP GET /dispatch')).not.toBeInTheDocument(); + + const resetFocusButton = getByText('Reset Focus'); + expect(resetFocusButton).toBeInTheDocument(); + fireEvent.click(resetFocusButton); + + expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalled(); + expect(screen.queryByText('HTTP GET /dispatch')).toBeInTheDocument(); + }); +}); From 2f0d98ae5142bb335c47af0853e0182c4c9dd44d Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:21:39 +0530 Subject: [PATCH 54/60] chore: added trace views test (#5519) * feat: added trace filter test cases * feat: added trace filter test cases - initial render * feat: added test cases - query sync, filter section behaviour etc * feat: deleted mock-data files * feat: added test cases of undefined filters and items * feat: deleted tsconfig * feat: added clear and rest btn test cases for traces filters * feat: added collapse and uncollapse test for traces filters * fix: added test cases for trace - saved view * chore: code refactor' * chore: added trace for search and navigation * chore: used ROUTES enum * chore: fixed test cases after merge conflict --- .../__mockdata__/explorer_views.ts | 20 +- .../pages/SaveView/__test__/SaveView.test.tsx | 172 ++++++++++++++++++ frontend/src/pages/SaveView/index.tsx | 11 +- .../__test__/TracesExplorer.test.tsx | 4 +- 4 files changed, 193 insertions(+), 14 deletions(-) create mode 100644 frontend/src/pages/SaveView/__test__/SaveView.test.tsx diff --git a/frontend/src/mocks-server/__mockdata__/explorer_views.ts b/frontend/src/mocks-server/__mockdata__/explorer_views.ts index b51eb2ee9c..4719e77697 100644 --- a/frontend/src/mocks-server/__mockdata__/explorer_views.ts +++ b/frontend/src/mocks-server/__mockdata__/explorer_views.ts @@ -78,13 +78,13 @@ export const explorerView = { extraData: '{"color":"#00ffd0"}', }, { - uuid: '58b010b6-8be9-40d1-8d25-f73b5f7314ad', - name: 'success traces list view', + uuid: '8c4bf492-d54d-4ab2-a8d6-9c1563f46e1f', + name: 'R-test panel', category: '', - createdAt: '2023-08-30T13:00:40.958011925Z', - createdBy: 'test-email', - updatedAt: '2024-04-29T13:09:06.175537361Z', - updatedBy: 'test-email', + createdAt: '2024-07-01T13:45:57.924686766Z', + createdBy: 'test-user-test', + updatedAt: '2024-07-01T13:48:31.032106578Z', + updatedBy: 'test-user-test', sourcePage: 'traces', tags: [''], compositeQuery: { @@ -106,13 +106,13 @@ export const explorerView = { items: [ { key: { - key: 'responseStatusCode', + key: 'httpMethod', dataType: 'string', type: 'tag', isColumn: true, isJSON: false, }, - value: '200', + value: 'GET', op: '=', }, ], @@ -128,7 +128,7 @@ export const explorerView = { order: 'desc', }, ], - reduceTo: 'sum', + reduceTo: 'avg', timeAggregation: 'rate', spaceAggregation: 'sum', ShiftBy: 0, @@ -137,7 +137,7 @@ export const explorerView = { panelType: 'list', queryType: 'builder', }, - extraData: '{"color":"#bdff9d"}', + extraData: '{"color":"#AD7F58"}', }, ], }; diff --git a/frontend/src/pages/SaveView/__test__/SaveView.test.tsx b/frontend/src/pages/SaveView/__test__/SaveView.test.tsx new file mode 100644 index 0000000000..fee0d3d6f0 --- /dev/null +++ b/frontend/src/pages/SaveView/__test__/SaveView.test.tsx @@ -0,0 +1,172 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable sonarjs/no-duplicate-string */ +import ROUTES from 'constants/routes'; +import { explorerView } from 'mocks-server/__mockdata__/explorer_views'; +import { server } from 'mocks-server/server'; +import { rest } from 'msw'; +import { MemoryRouter, Route } from 'react-router-dom'; +import { fireEvent, render, screen, waitFor, within } from 'tests/test-utils'; + +import SaveView from '..'; + +const handleExplorerTabChangeTest = jest.fn(); +jest.mock('hooks/useHandleExplorerTabChange', () => ({ + useHandleExplorerTabChange: () => ({ + handleExplorerTabChange: handleExplorerTabChangeTest, + }), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn().mockReturnValue({ + pathname: `${ROUTES.TRACES_SAVE_VIEWS}`, + }), +})); + +describe('SaveView', () => { + it('should render the SaveView component', async () => { + render(); + expect(await screen.findByText('Table View')).toBeInTheDocument(); + + const savedViews = screen.getAllByRole('row'); + expect(savedViews).toHaveLength(2); + + // assert row 1 + expect( + within(document.querySelector('.view-tag') as HTMLElement).getByText('T'), + ).toBeInTheDocument(); + expect(screen.getByText('test-user-1')).toBeInTheDocument(); + + // assert row 2 + expect(screen.getByText('R-test panel')).toBeInTheDocument(); + expect(screen.getByText('test-user-test')).toBeInTheDocument(); + }); + + it('explorer icon should take the user to the related explorer page', async () => { + render( + + + + + , + ); + + expect(await screen.findByText('Table View')).toBeInTheDocument(); + + const explorerIcon = await screen.findAllByTestId('go-to-explorer'); + expect(explorerIcon[0]).toBeInTheDocument(); + + // Simulate click on explorer icon + fireEvent.click(explorerIcon[0]); + + await waitFor(() => + expect(handleExplorerTabChangeTest).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + '/traces-explorer', // Asserts the third argument is '/traces-explorer' + ), + ); + }); + + it('should render the SaveView component with a search input', async () => { + render(); + const searchInput = screen.getByPlaceholderText('Search for views...'); + expect(await screen.findByText('Table View')).toBeInTheDocument(); + + expect(searchInput).toBeInTheDocument(); + + // search for 'R-test panel' + searchInput.focus(); + (searchInput as HTMLInputElement).setSelectionRange( + 0, + (searchInput as HTMLInputElement).value.length, + ); + + fireEvent.change(searchInput, { target: { value: 'R-test panel' } }); + expect(searchInput).toHaveValue('R-test panel'); + searchInput.blur(); + + expect(await screen.findByText('R-test panel')).toBeInTheDocument(); + + // Table View should not be present now + const savedViews = screen.getAllByRole('row'); + expect(savedViews).toHaveLength(1); + }); + + it('should be able to edit name of view', async () => { + server.use( + rest.put( + 'http://localhost/api/v1/explorer/views/test-uuid-1', + // eslint-disable-next-line no-return-assign + (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + ...explorerView, + data: [ + ...explorerView.data, + (explorerView.data[0].name = 'New Table View'), + ], + }), + ), + ), + ); + render(); + + const editButton = await screen.findAllByTestId('edit-view'); + fireEvent.click(editButton[0]); + + const viewName = await screen.findByTestId('view-name'); + expect(viewName).toBeInTheDocument(); + expect(viewName).toHaveValue('Table View'); + + const newViewName = 'New Table View'; + fireEvent.change(viewName, { target: { value: newViewName } }); + expect(viewName).toHaveValue(newViewName); + + const saveButton = await screen.findByTestId('save-view'); + fireEvent.click(saveButton); + + await waitFor(() => + expect(screen.getByText(newViewName)).toBeInTheDocument(), + ); + }); + + it('should be able to delete a view', async () => { + server.use( + rest.delete( + 'http://localhost/api/v1/explorer/views/test-uuid-1', + (_req, res, ctx) => res(ctx.status(200), ctx.json({ status: 'success' })), + ), + ); + + render(); + + const deleteButton = await screen.findAllByTestId('delete-view'); + fireEvent.click(deleteButton[0]); + + expect(await screen.findByText('delete_confirm_message')).toBeInTheDocument(); + + const confirmButton = await screen.findByTestId('confirm-delete'); + fireEvent.click(confirmButton); + + await waitFor(() => expect(screen.queryByText('Table View')).toBeNull()); + }); + + it('should render empty state', async () => { + server.use( + rest.get('http://localhost/api/v1/explorer/views', (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + status: 'success', + data: [], + }), + ), + ), + ); + render(); + + expect(screen.getByText('No data')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/pages/SaveView/index.tsx b/frontend/src/pages/SaveView/index.tsx index 7088c4a78c..03c76151b7 100644 --- a/frontend/src/pages/SaveView/index.tsx +++ b/frontend/src/pages/SaveView/index.tsx @@ -263,13 +263,19 @@ function SaveView(): JSX.Element { handleEditModelOpen(view, bgColor)} /> - handleRedirectQuery(view)} /> + handleRedirectQuery(view)} + data-testid="go-to-explorer" + /> handleDeleteModelOpen(view.uuid, view.name)} />
@@ -347,6 +353,7 @@ function SaveView(): JSX.Element { onClick={onDeleteHandler} className="delete-btn" disabled={isDeleteLoading} + data-testid="confirm-delete" > Delete view , @@ -371,6 +378,7 @@ function SaveView(): JSX.Element { icon={} onClick={onUpdateQueryHandler} disabled={isViewUpdating} + data-testid="save-view" > Save changes , @@ -385,6 +393,7 @@ function SaveView(): JSX.Element { setNewViewName(e.target.value)} />
diff --git a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx index 0622a365c2..a28776f0d0 100644 --- a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx +++ b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx @@ -617,9 +617,7 @@ describe('TracesExplorer - ', () => { const viewListOptions = await screen.findByRole('listbox'); expect(viewListOptions).toBeInTheDocument(); - expect( - within(viewListOptions).getByText('success traces list view'), - ).toBeInTheDocument(); + expect(within(viewListOptions).getByText('R-test panel')).toBeInTheDocument(); expect(within(viewListOptions).getByText('Table View')).toBeInTheDocument(); From 0a146910d68870efc436c59c0850587a2cfb7c48 Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Thu, 29 Aug 2024 19:25:34 +0530 Subject: [PATCH 55/60] =?UTF-8?q?chore(signoz):=20=F0=9F=93=8C=20pin=20ver?= =?UTF-8?q?sions:=20SigNoz=200.53.0,=20SigNoz=20OtelCollector=200.102.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Prashant Shahi --- deploy/docker-swarm/clickhouse-setup/docker-compose.yaml | 8 ++++---- deploy/docker/clickhouse-setup/docker-compose-core.yaml | 4 ++-- .../docker/clickhouse-setup/docker-compose.testing.yaml | 8 ++++---- deploy/docker/clickhouse-setup/docker-compose.yaml | 8 ++++---- go.mod | 2 +- go.sum | 4 ++-- pkg/query-service/tests/test-deploy/docker-compose.yaml | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index e9cdbb31c0..163e40932e 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -146,7 +146,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.52.0 + image: signoz/query-service:0.53.0 command: [ "-config=/root/config/prometheus.yml", @@ -186,7 +186,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:0.52.0 + image: signoz/frontend:0.53.0 deploy: restart_policy: condition: on-failure @@ -199,7 +199,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.102.4 + image: signoz/signoz-otel-collector:0.102.6 command: [ "--config=/etc/otel-collector-config.yaml", @@ -238,7 +238,7 @@ services: - query-service otel-collector-migrator: - image: signoz/signoz-schema-migrator:0.102.4 + image: signoz/signoz-schema-migrator:0.102.6 deploy: restart_policy: condition: on-failure diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index 6b53443e79..f014915192 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -66,7 +66,7 @@ services: - --storage.path=/data otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.4} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.6} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -81,7 +81,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` otel-collector: container_name: signoz-otel-collector - image: signoz/signoz-otel-collector:0.102.4 + image: signoz/signoz-otel-collector:0.102.6 command: [ "--config=/etc/otel-collector-config.yaml", diff --git a/deploy/docker/clickhouse-setup/docker-compose.testing.yaml b/deploy/docker/clickhouse-setup/docker-compose.testing.yaml index 8fe7c6b4ee..065e9ebbee 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.testing.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.testing.yaml @@ -164,7 +164,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` query-service: - image: signoz/query-service:${DOCKER_TAG:-0.52.0} + image: signoz/query-service:${DOCKER_TAG:-0.53.0} container_name: signoz-query-service command: [ @@ -204,7 +204,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.52.0} + image: signoz/frontend:${DOCKER_TAG:-0.53.0} container_name: signoz-frontend restart: on-failure depends_on: @@ -216,7 +216,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.4} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.6} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -230,7 +230,7 @@ services: otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.4} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.6} container_name: signoz-otel-collector command: [ diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 3d70f0d0f9..a7b71983ca 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -164,7 +164,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` query-service: - image: signoz/query-service:${DOCKER_TAG:-0.52.0} + image: signoz/query-service:${DOCKER_TAG:-0.53.0} container_name: signoz-query-service command: [ @@ -203,7 +203,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.52.0} + image: signoz/frontend:${DOCKER_TAG:-0.53.0} container_name: signoz-frontend restart: on-failure depends_on: @@ -215,7 +215,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.4} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.6} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -229,7 +229,7 @@ services: otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.4} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.6} container_name: signoz-otel-collector command: [ diff --git a/go.mod b/go.mod index 97cc7f36f4..24d5b089cd 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/ClickHouse/clickhouse-go/v2 v2.23.2 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd - github.com/SigNoz/signoz-otel-collector v0.102.4 + github.com/SigNoz/signoz-otel-collector v0.102.6 github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974 github.com/antonmedv/expr v1.15.3 diff --git a/go.sum b/go.sum index 3caed8d06d..00d40497dd 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkb github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc= github.com/SigNoz/prometheus v1.11.1 h1:roM8ugYf4UxaeKKujEeBvoX7ybq3IrS+TB26KiRtIJg= github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I= -github.com/SigNoz/signoz-otel-collector v0.102.4 h1:098regGkGcrv0eSGElD4uEyuChxLM9VUoeWMW6KrUTI= -github.com/SigNoz/signoz-otel-collector v0.102.4/go.mod h1:3s9cSL8yexkBBMfK9mC3WWrAPm8oMtlZhvBxvt+Ziag= +github.com/SigNoz/signoz-otel-collector v0.102.6 h1:1g1pVqkPuum8U4MJ5QVKP0c//d357Rp7tT90ZgJxTa4= +github.com/SigNoz/signoz-otel-collector v0.102.6/go.mod h1:3s9cSL8yexkBBMfK9mC3WWrAPm8oMtlZhvBxvt+Ziag= github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc= github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo= github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY= diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index 4ca72424f3..11d05cf4a3 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml @@ -192,7 +192,7 @@ services: <<: *db-depend otel-collector-migrator: - image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.4} + image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.6} container_name: otel-migrator command: - "--dsn=tcp://clickhouse:9000" @@ -205,7 +205,7 @@ services: # condition: service_healthy otel-collector: - image: signoz/signoz-otel-collector:0.102.4 + image: signoz/signoz-otel-collector:0.102.6 container_name: signoz-otel-collector command: [ From 4295a2756a5dacaad9dad4ff44f8e5f667a6e68e Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 29 Aug 2024 21:44:12 +0530 Subject: [PATCH 56/60] chore: remove old data migrations (#5802) --- .../migrate/0_45_alerts_to_v4/run.go | 153 ------------------ .../migrate/0_47_alerts_custom_step/run.go | 70 -------- pkg/query-service/migrate/migate.go | 25 --- 3 files changed, 248 deletions(-) delete mode 100644 pkg/query-service/migrate/0_45_alerts_to_v4/run.go delete mode 100644 pkg/query-service/migrate/0_47_alerts_custom_step/run.go diff --git a/pkg/query-service/migrate/0_45_alerts_to_v4/run.go b/pkg/query-service/migrate/0_45_alerts_to_v4/run.go deleted file mode 100644 index f68f4ca43b..0000000000 --- a/pkg/query-service/migrate/0_45_alerts_to_v4/run.go +++ /dev/null @@ -1,153 +0,0 @@ -package alertstov4 - -import ( - "context" - "encoding/json" - - "github.com/jmoiron/sqlx" - v3 "go.signoz.io/signoz/pkg/query-service/model/v3" - "go.signoz.io/signoz/pkg/query-service/rules" - "go.uber.org/multierr" - "go.uber.org/zap" -) - -var Version = "0.45-alerts-to-v4" - -var mapTimeAggregation = map[v3.AggregateOperator]v3.TimeAggregation{ - v3.AggregateOperatorSum: v3.TimeAggregationSum, - v3.AggregateOperatorMin: v3.TimeAggregationMin, - v3.AggregateOperatorMax: v3.TimeAggregationMax, - v3.AggregateOperatorSumRate: v3.TimeAggregationRate, - v3.AggregateOperatorAvgRate: v3.TimeAggregationRate, - v3.AggregateOperatorMinRate: v3.TimeAggregationRate, - v3.AggregateOperatorMaxRate: v3.TimeAggregationRate, - v3.AggregateOperatorHistQuant50: v3.TimeAggregationUnspecified, - v3.AggregateOperatorHistQuant75: v3.TimeAggregationUnspecified, - v3.AggregateOperatorHistQuant90: v3.TimeAggregationUnspecified, - v3.AggregateOperatorHistQuant95: v3.TimeAggregationUnspecified, - v3.AggregateOperatorHistQuant99: v3.TimeAggregationUnspecified, -} - -var mapSpaceAggregation = map[v3.AggregateOperator]v3.SpaceAggregation{ - v3.AggregateOperatorSum: v3.SpaceAggregationSum, - v3.AggregateOperatorMin: v3.SpaceAggregationMin, - v3.AggregateOperatorMax: v3.SpaceAggregationMax, - v3.AggregateOperatorSumRate: v3.SpaceAggregationSum, - v3.AggregateOperatorAvgRate: v3.SpaceAggregationAvg, - v3.AggregateOperatorMinRate: v3.SpaceAggregationMin, - v3.AggregateOperatorMaxRate: v3.SpaceAggregationMax, - v3.AggregateOperatorHistQuant50: v3.SpaceAggregationPercentile50, - v3.AggregateOperatorHistQuant75: v3.SpaceAggregationPercentile75, - v3.AggregateOperatorHistQuant90: v3.SpaceAggregationPercentile90, - v3.AggregateOperatorHistQuant95: v3.SpaceAggregationPercentile95, - v3.AggregateOperatorHistQuant99: v3.SpaceAggregationPercentile99, -} - -func canMigrateOperator(operator v3.AggregateOperator) bool { - switch operator { - case v3.AggregateOperatorSum, - v3.AggregateOperatorMin, - v3.AggregateOperatorMax, - v3.AggregateOperatorSumRate, - v3.AggregateOperatorAvgRate, - v3.AggregateOperatorMinRate, - v3.AggregateOperatorMaxRate, - v3.AggregateOperatorHistQuant50, - v3.AggregateOperatorHistQuant75, - v3.AggregateOperatorHistQuant90, - v3.AggregateOperatorHistQuant95, - v3.AggregateOperatorHistQuant99: - return true - } - return false -} - -func Migrate(conn *sqlx.DB) error { - ruleDB := rules.NewRuleDB(conn) - storedRules, err := ruleDB.GetStoredRules(context.Background()) - if err != nil { - return err - } - - for _, storedRule := range storedRules { - parsedRule, errs := rules.ParsePostableRule([]byte(storedRule.Data)) - if len(errs) > 0 { - // this should not happen but if it does, we should not stop the migration - zap.L().Error("Error parsing rule", zap.Error(multierr.Combine(errs...)), zap.Int("rule", storedRule.Id)) - continue - } - zap.L().Info("Rule parsed", zap.Int("rule", storedRule.Id)) - updated := false - if parsedRule.RuleCondition != nil && parsedRule.Version == "" { - if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder { - // check if all the queries can be converted to v4 - canMigrate := true - for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries { - if query.DataSource == v3.DataSourceMetrics && query.Expression == query.QueryName { - if !canMigrateOperator(query.AggregateOperator) { - canMigrate = false - break - } - } - } - - if canMigrate { - parsedRule.Version = "v4" - for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries { - if query.DataSource == v3.DataSourceMetrics && query.Expression == query.QueryName { - // update aggregate attribute - if query.AggregateOperator == v3.AggregateOperatorSum || - query.AggregateOperator == v3.AggregateOperatorMin || - query.AggregateOperator == v3.AggregateOperatorMax { - query.AggregateAttribute.Type = "Gauge" - } - if query.AggregateOperator == v3.AggregateOperatorSumRate || - query.AggregateOperator == v3.AggregateOperatorAvgRate || - query.AggregateOperator == v3.AggregateOperatorMinRate || - query.AggregateOperator == v3.AggregateOperatorMaxRate { - query.AggregateAttribute.Type = "Sum" - } - - if query.AggregateOperator == v3.AggregateOperatorHistQuant50 || - query.AggregateOperator == v3.AggregateOperatorHistQuant75 || - query.AggregateOperator == v3.AggregateOperatorHistQuant90 || - query.AggregateOperator == v3.AggregateOperatorHistQuant95 || - query.AggregateOperator == v3.AggregateOperatorHistQuant99 { - query.AggregateAttribute.Type = "Histogram" - } - query.AggregateAttribute.DataType = v3.AttributeKeyDataTypeFloat64 - query.AggregateAttribute.IsColumn = true - query.TimeAggregation = mapTimeAggregation[query.AggregateOperator] - query.SpaceAggregation = mapSpaceAggregation[query.AggregateOperator] - query.AggregateOperator = v3.AggregateOperator(query.TimeAggregation) - updated = true - } - } - } - } - } - - if !updated { - zap.L().Info("Rule not updated", zap.Int("rule", storedRule.Id)) - continue - } - - ruleJSON, jsonErr := json.Marshal(parsedRule) - if jsonErr != nil { - zap.L().Error("Error marshalling rule; skipping rule migration", zap.Error(jsonErr), zap.Int("rule", storedRule.Id)) - continue - } - - stmt, prepareError := conn.PrepareContext(context.Background(), `UPDATE rules SET data=$3 WHERE id=$4;`) - if prepareError != nil { - zap.L().Error("Error in preparing statement for UPDATE to rules", zap.Error(prepareError)) - continue - } - defer stmt.Close() - - if _, err := stmt.Exec(ruleJSON, storedRule.Id); err != nil { - zap.L().Error("Error in Executing prepared statement for UPDATE to rules", zap.Error(err)) - } - } - return nil -} diff --git a/pkg/query-service/migrate/0_47_alerts_custom_step/run.go b/pkg/query-service/migrate/0_47_alerts_custom_step/run.go deleted file mode 100644 index b25705aaf2..0000000000 --- a/pkg/query-service/migrate/0_47_alerts_custom_step/run.go +++ /dev/null @@ -1,70 +0,0 @@ -package alertscustomstep - -import ( - "context" - "encoding/json" - "time" - - "github.com/jmoiron/sqlx" - v3 "go.signoz.io/signoz/pkg/query-service/model/v3" - "go.signoz.io/signoz/pkg/query-service/rules" - "go.uber.org/multierr" - "go.uber.org/zap" -) - -var Version = "0.47-alerts-custom-step" - -func Migrate(conn *sqlx.DB) error { - ruleDB := rules.NewRuleDB(conn) - storedRules, err := ruleDB.GetStoredRules(context.Background()) - if err != nil { - return err - } - - for _, storedRule := range storedRules { - parsedRule, errs := rules.ParsePostableRule([]byte(storedRule.Data)) - if len(errs) > 0 { - // this should not happen but if it does, we should not stop the migration - zap.L().Error("Error parsing rule", zap.Error(multierr.Combine(errs...)), zap.Int("rule", storedRule.Id)) - continue - } - zap.L().Info("Rule parsed", zap.Int("rule", storedRule.Id)) - updated := false - if parsedRule.RuleCondition != nil { - if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder { - if parsedRule.EvalWindow <= rules.Duration(6*time.Hour) { - for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries { - if query.StepInterval > 60 { - updated = true - zap.L().Info("Updating step interval", zap.Int("rule", storedRule.Id), zap.Int64("old", query.StepInterval), zap.Int64("new", 60)) - query.StepInterval = 60 - } - } - } - } - } - - if !updated { - zap.L().Info("Rule not updated", zap.Int("rule", storedRule.Id)) - continue - } - - ruleJSON, jsonErr := json.Marshal(parsedRule) - if jsonErr != nil { - zap.L().Error("Error marshalling rule; skipping rule migration", zap.Error(jsonErr), zap.Int("rule", storedRule.Id)) - continue - } - - stmt, prepareError := conn.PrepareContext(context.Background(), `UPDATE rules SET data=$3 WHERE id=$4;`) - if prepareError != nil { - zap.L().Error("Error in preparing statement for UPDATE to rules", zap.Error(prepareError)) - continue - } - defer stmt.Close() - - if _, err := stmt.Exec(ruleJSON, storedRule.Id); err != nil { - zap.L().Error("Error in Executing prepared statement for UPDATE to rules", zap.Error(err)) - } - } - return nil -} diff --git a/pkg/query-service/migrate/migate.go b/pkg/query-service/migrate/migate.go index bf5f3a7738..60e65d6d72 100644 --- a/pkg/query-service/migrate/migate.go +++ b/pkg/query-service/migrate/migate.go @@ -7,9 +7,6 @@ import ( "github.com/ClickHouse/clickhouse-go/v2/lib/driver" "github.com/jmoiron/sqlx" - alertstov4 "go.signoz.io/signoz/pkg/query-service/migrate/0_45_alerts_to_v4" - alertscustomstep "go.signoz.io/signoz/pkg/query-service/migrate/0_47_alerts_custom_step" - "go.uber.org/zap" ) type DataMigration struct { @@ -56,28 +53,6 @@ func Migrate(dsn string) error { return err } - if m, err := getMigrationVersion(conn, "0.45_alerts_to_v4"); err == nil && m == nil { - if err := alertstov4.Migrate(conn); err != nil { - zap.L().Error("failed to migrate 0.45_alerts_to_v4", zap.Error(err)) - } else { - _, err := conn.Exec("INSERT INTO data_migrations (version, succeeded) VALUES ('0.45_alerts_to_v4', true)") - if err != nil { - return err - } - } - } - - if m, err := getMigrationVersion(conn, "0.47_alerts_custom_step"); err == nil && m == nil { - if err := alertscustomstep.Migrate(conn); err != nil { - zap.L().Error("failed to migrate 0.47_alerts_custom_step", zap.Error(err)) - } else { - _, err := conn.Exec("INSERT INTO data_migrations (version, succeeded) VALUES ('0.47_alerts_custom_step', true)") - if err != nil { - return err - } - } - } - return nil } From 44598e304da35086ff91d76d3802fae5c650b22e Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 29 Aug 2024 21:53:28 +0530 Subject: [PATCH 57/60] chore: remove feature usage code from manager (#5803) --- pkg/query-service/rules/manager.go | 108 +---------------------------- 1 file changed, 1 insertion(+), 107 deletions(-) diff --git a/pkg/query-service/rules/manager.go b/pkg/query-service/rules/manager.go index 40764f9fb0..95f97753e0 100644 --- a/pkg/query-service/rules/manager.go +++ b/pkg/query-service/rules/manager.go @@ -20,11 +20,9 @@ import ( "github.com/jmoiron/sqlx" - // opentracing "github.com/opentracing/opentracing-go" am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager" "go.signoz.io/signoz/pkg/query-service/interfaces" "go.signoz.io/signoz/pkg/query-service/model" - v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/telemetry" "go.signoz.io/signoz/pkg/query-service/utils/labels" ) @@ -240,20 +238,6 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error parsedRule, errs := ParsePostableRule([]byte(ruleStr)) - currentRule, err := m.GetRule(ctx, id) - if err != nil { - zap.L().Error("failed to get the rule from rule db", zap.String("id", id), zap.Error(err)) - return err - } - - if !checkIfTraceOrLogQB(¤tRule.PostableRule) { - // check if the new rule uses any feature that is not enabled - err = m.checkFeatureUsage(parsedRule) - if err != nil { - return err - } - } - if len(errs) > 0 { zap.L().Error("failed to parse rules", zap.Errors("errors", errs)) // just one rule is being parsed so expect just one error @@ -272,20 +256,6 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id string) error } } - // update feature usage if the current rule is not a trace or log query builder - if !checkIfTraceOrLogQB(¤tRule.PostableRule) { - err = m.updateFeatureUsage(parsedRule, 1) - if err != nil { - zap.L().Error("error updating feature usage", zap.Error(err)) - } - // update feature usage if the new rule is not a trace or log query builder and the current rule is - } else if !checkIfTraceOrLogQB(parsedRule) { - err = m.updateFeatureUsage(¤tRule.PostableRule, -1) - if err != nil { - zap.L().Error("error updating feature usage", zap.Error(err)) - } - } - return nil } @@ -335,13 +305,6 @@ func (m *Manager) DeleteRule(ctx context.Context, id string) error { return fmt.Errorf("delete rule received an rule id in invalid format, must be a number") } - // update feature usage - rule, err := m.GetRule(ctx, id) - if err != nil { - zap.L().Error("failed to get the rule from rule db", zap.String("id", id), zap.Error(err)) - return err - } - taskName := prepareTaskName(int64(idInt)) if !m.opts.DisableRules { m.deleteTask(taskName) @@ -352,11 +315,6 @@ func (m *Manager) DeleteRule(ctx context.Context, id string) error { return err } - err = m.updateFeatureUsage(&rule.PostableRule, -1) - if err != nil { - zap.L().Error("error updating feature usage", zap.Error(err)) - } - return nil } @@ -381,12 +339,6 @@ func (m *Manager) deleteTask(taskName string) { func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule, error) { parsedRule, errs := ParsePostableRule([]byte(ruleStr)) - // check if the rule uses any feature that is not enabled - err := m.checkFeatureUsage(parsedRule) - if err != nil { - return nil, err - } - if len(errs) > 0 { zap.L().Error("failed to parse rules", zap.Errors("errors", errs)) // just one rule is being parsed so expect just one error @@ -409,11 +361,6 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule return nil, err } - // update feature usage - err = m.updateFeatureUsage(parsedRule, 1) - if err != nil { - zap.L().Error("error updating feature usage", zap.Error(err)) - } gettableRule := &GettableRule{ Id: fmt.Sprintf("%d", lastInsertId), PostableRule: *parsedRule, @@ -421,59 +368,6 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*GettableRule return gettableRule, nil } -func (m *Manager) updateFeatureUsage(parsedRule *PostableRule, usage int64) error { - isTraceOrLogQB := checkIfTraceOrLogQB(parsedRule) - if isTraceOrLogQB { - feature, err := m.featureFlags.GetFeatureFlag(model.QueryBuilderAlerts) - if err != nil { - return err - } - feature.Usage += usage - if feature.Usage == feature.UsageLimit && feature.UsageLimit != -1 { - feature.Active = false - } - if feature.Usage < feature.UsageLimit || feature.UsageLimit == -1 { - feature.Active = true - } - err = m.featureFlags.UpdateFeatureFlag(feature) - if err != nil { - return err - } - } - return nil -} - -func (m *Manager) checkFeatureUsage(parsedRule *PostableRule) error { - isTraceOrLogQB := checkIfTraceOrLogQB(parsedRule) - if isTraceOrLogQB { - err := m.featureFlags.CheckFeature(model.QueryBuilderAlerts) - if err != nil { - switch err.(type) { - case model.ErrFeatureUnavailable: - zap.L().Error("feature unavailable", zap.String("featureKey", model.QueryBuilderAlerts), zap.Error(err)) - return model.BadRequest(err) - default: - zap.L().Error("feature check failed", zap.String("featureKey", model.QueryBuilderAlerts), zap.Error(err)) - return model.BadRequest(err) - } - } - } - return nil -} - -func checkIfTraceOrLogQB(parsedRule *PostableRule) bool { - if parsedRule != nil { - if parsedRule.RuleCondition.QueryType() == v3.QueryTypeBuilder { - for _, query := range parsedRule.RuleCondition.CompositeQuery.BuilderQueries { - if query.DataSource == v3.DataSourceTraces || query.DataSource == v3.DataSourceLogs { - return true - } - } - } - } - return false -} - func (m *Manager) addTask(rule *PostableRule, taskName string) error { m.mtx.Lock() defer m.mtx.Unlock() @@ -569,7 +463,7 @@ func (m *Manager) prepareTask(acquireLock bool, r *PostableRule, taskName string m.rules[ruleId] = pr } else { - return nil, fmt.Errorf(fmt.Sprintf("unsupported rule type. Supported types: %s, %s", RuleTypeProm, RuleTypeThreshold)) + return nil, fmt.Errorf("unsupported rule type. Supported types: %s, %s", RuleTypeProm, RuleTypeThreshold) } return task, nil From dde4485839bc7a3c1f8e9b13d7fb99ae0b097bb0 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 30 Aug 2024 10:34:11 +0530 Subject: [PATCH 58/60] chore: add types for alert type, state, and rule data kind (#5804) --- pkg/query-service/app/http_handler.go | 10 +++--- pkg/query-service/rules/alerting.go | 29 +++++++++++++++++ pkg/query-service/rules/api_params.go | 32 ++++++++++++++----- pkg/query-service/rules/db.go | 6 ++-- pkg/query-service/rules/manager.go | 12 +++---- pkg/query-service/rules/threshold_rule.go | 7 ++-- .../rules/threshold_rule_test.go | 2 +- 7 files changed, 71 insertions(+), 27 deletions(-) diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 5d3d9affd5..1210cd4f67 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -805,7 +805,7 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request continue } filterItems := []v3.FilterItem{} - if rule.AlertType == "LOGS_BASED_ALERT" || rule.AlertType == "TRACES_BASED_ALERT" { + if rule.AlertType == rules.AlertTypeLogs || rule.AlertType == rules.AlertTypeTraces { if rule.RuleCondition.CompositeQuery != nil { if rule.RuleCondition.QueryType() == v3.QueryTypeBuilder { for _, query := range rule.RuleCondition.CompositeQuery.BuilderQueries { @@ -818,9 +818,9 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request } newFilters := common.PrepareFilters(lbls, filterItems) ts := time.Unix(res.Items[idx].UnixMilli/1000, 0) - if rule.AlertType == "LOGS_BASED_ALERT" { + if rule.AlertType == rules.AlertTypeLogs { res.Items[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, newFilters) - } else if rule.AlertType == "TRACES_BASED_ALERT" { + } else if rule.AlertType == rules.AlertTypeTraces { res.Items[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, newFilters) } } @@ -854,9 +854,9 @@ func (aH *APIHandler) getRuleStateHistoryTopContributors(w http.ResponseWriter, } ts := time.Unix(params.End/1000, 0) filters := common.PrepareFilters(lbls, nil) - if rule.AlertType == "LOGS_BASED_ALERT" { + if rule.AlertType == rules.AlertTypeLogs { res[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, filters) - } else if rule.AlertType == "TRACES_BASED_ALERT" { + } else if rule.AlertType == rules.AlertTypeTraces { res[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, filters) } } diff --git a/pkg/query-service/rules/alerting.go b/pkg/query-service/rules/alerting.go index 36a0ea1b69..7c7fb40ed6 100644 --- a/pkg/query-service/rules/alerting.go +++ b/pkg/query-service/rules/alerting.go @@ -61,6 +61,35 @@ func (s AlertState) String() string { panic(errors.Errorf("unknown alert state: %d", s)) } +func (s AlertState) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +func (s *AlertState) UnmarshalJSON(b []byte) error { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return err + } + switch value := v.(type) { + case string: + switch value { + case "inactive": + *s = StateInactive + case "pending": + *s = StatePending + case "firing": + *s = StateFiring + case "disabled": + *s = StateDisabled + default: + return errors.New("invalid alert state") + } + return nil + default: + return errors.New("invalid alert state") + } +} + type Alert struct { State AlertState diff --git a/pkg/query-service/rules/api_params.go b/pkg/query-service/rules/api_params.go index 74ca041aae..890d464671 100644 --- a/pkg/query-service/rules/api_params.go +++ b/pkg/query-service/rules/api_params.go @@ -16,6 +16,22 @@ import ( yaml "gopkg.in/yaml.v2" ) +type AlertType string + +const ( + AlertTypeMetric AlertType = "METRIC_BASED_ALERT" + AlertTypeTraces AlertType = "TRACES_BASED_ALERT" + AlertTypeLogs AlertType = "LOGS_BASED_ALERT" + AlertTypeExceptions AlertType = "EXCEPTIONS_BASED_ALERT" +) + +type RuleDataKind string + +const ( + RuleDataKindJson RuleDataKind = "json" + RuleDataKindYaml RuleDataKind = "yaml" +) + // this file contains api request and responses to be // served over http @@ -31,12 +47,12 @@ func newApiErrorBadData(err error) *model.ApiError { // PostableRule is used to create alerting rule from HTTP api type PostableRule struct { - AlertName string `yaml:"alert,omitempty" json:"alert,omitempty"` - AlertType string `yaml:"alertType,omitempty" json:"alertType,omitempty"` - Description string `yaml:"description,omitempty" json:"description,omitempty"` - RuleType RuleType `yaml:"ruleType,omitempty" json:"ruleType,omitempty"` - EvalWindow Duration `yaml:"evalWindow,omitempty" json:"evalWindow,omitempty"` - Frequency Duration `yaml:"frequency,omitempty" json:"frequency,omitempty"` + AlertName string `yaml:"alert,omitempty" json:"alert,omitempty"` + AlertType AlertType `yaml:"alertType,omitempty" json:"alertType,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + RuleType RuleType `yaml:"ruleType,omitempty" json:"ruleType,omitempty"` + EvalWindow Duration `yaml:"evalWindow,omitempty" json:"evalWindow,omitempty"` + Frequency Duration `yaml:"frequency,omitempty" json:"frequency,omitempty"` RuleCondition *RuleCondition `yaml:"condition,omitempty" json:"condition,omitempty"` Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` @@ -234,8 +250,8 @@ type GettableRules struct { // GettableRule has info for an alerting rules. type GettableRule struct { - Id string `json:"id"` - State string `json:"state"` + Id string `json:"id"` + State AlertState `json:"state"` PostableRule CreatedAt *time.Time `json:"createAt"` CreatedBy *string `json:"createBy"` diff --git a/pkg/query-service/rules/db.go b/pkg/query-service/rules/db.go index 37e45f2711..d9a9be195c 100644 --- a/pkg/query-service/rules/db.go +++ b/pkg/query-service/rules/db.go @@ -325,9 +325,9 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) { continue } alertNames = append(alertNames, rule.AlertName) - if rule.AlertType == "LOGS_BASED_ALERT" { + if rule.AlertType == AlertTypeLogs { alertsInfo.LogsBasedAlerts = alertsInfo.LogsBasedAlerts + 1 - } else if rule.AlertType == "METRIC_BASED_ALERT" { + } else if rule.AlertType == AlertTypeMetric { alertsInfo.MetricBasedAlerts = alertsInfo.MetricBasedAlerts + 1 if rule.RuleCondition != nil && rule.RuleCondition.CompositeQuery != nil { if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeBuilder { @@ -343,7 +343,7 @@ func (r *ruleDB) GetAlertsInfo(ctx context.Context) (*model.AlertsInfo, error) { } } } - } else if rule.AlertType == "TRACES_BASED_ALERT" { + } else if rule.AlertType == AlertTypeTraces { alertsInfo.TracesBasedAlerts = alertsInfo.TracesBasedAlerts + 1 } alertsInfo.TotalAlerts = alertsInfo.TotalAlerts + 1 diff --git a/pkg/query-service/rules/manager.go b/pkg/query-service/rules/manager.go index 95f97753e0..f738fefcc7 100644 --- a/pkg/query-service/rules/manager.go +++ b/pkg/query-service/rules/manager.go @@ -604,10 +604,10 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*GettableRules, error) { // fetch state of rule from memory if rm, ok := m.rules[ruleResponse.Id]; !ok { - ruleResponse.State = StateDisabled.String() + ruleResponse.State = StateDisabled ruleResponse.Disabled = true } else { - ruleResponse.State = rm.State().String() + ruleResponse.State = rm.State() } ruleResponse.CreatedAt = s.CreatedAt ruleResponse.CreatedBy = s.CreatedBy @@ -631,10 +631,10 @@ func (m *Manager) GetRule(ctx context.Context, id string) (*GettableRule, error) r.Id = fmt.Sprintf("%d", s.Id) // fetch state of rule from memory if rm, ok := m.rules[r.Id]; !ok { - r.State = StateDisabled.String() + r.State = StateDisabled r.Disabled = true } else { - r.State = rm.State().String() + r.State = rm.State() } r.CreatedAt = s.CreatedAt r.CreatedBy = s.CreatedBy @@ -740,10 +740,10 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, ruleId string) // fetch state of rule from memory if rm, ok := m.rules[ruleId]; !ok { - response.State = StateDisabled.String() + response.State = StateDisabled response.Disabled = true } else { - response.State = rm.State().String() + response.State = rm.State() } return &response, nil diff --git a/pkg/query-service/rules/threshold_rule.go b/pkg/query-service/rules/threshold_rule.go index 5093e407dc..9bdecbc63d 100644 --- a/pkg/query-service/rules/threshold_rule.go +++ b/pkg/query-service/rules/threshold_rule.go @@ -91,8 +91,7 @@ type ThresholdRule struct { lastTimestampWithDatapoints time.Time // Type of the rule - // One of ["LOGS_BASED_ALERT", "TRACES_BASED_ALERT", "METRIC_BASED_ALERT", "EXCEPTIONS_BASED_ALERT"] - typ string + typ AlertType // querier is used for alerts created before the introduction of new metrics query builder querier interfaces.Querier @@ -975,12 +974,12 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie // Links with timestamps should go in annotations since labels // is used alert grouping, and we want to group alerts with the same // label set, but different timestamps, together. - if r.typ == "TRACES_BASED_ALERT" { + if r.typ == AlertTypeTraces { link := r.prepareLinksToTraces(ts, smpl.MetricOrig) if link != "" && r.hostFromSource() != "" { annotations = append(annotations, labels.Label{Name: "related_traces", Value: fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)}) } - } else if r.typ == "LOGS_BASED_ALERT" { + } else if r.typ == AlertTypeLogs { link := r.prepareLinksToLogs(ts, smpl.MetricOrig) if link != "" && r.hostFromSource() != "" { annotations = append(annotations, labels.Label{Name: "related_logs", Value: fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)}) diff --git a/pkg/query-service/rules/threshold_rule_test.go b/pkg/query-service/rules/threshold_rule_test.go index 55a831efcd..05bd613900 100644 --- a/pkg/query-service/rules/threshold_rule_test.go +++ b/pkg/query-service/rules/threshold_rule_test.go @@ -674,7 +674,7 @@ func TestNormalizeLabelName(t *testing.T) { func TestPrepareLinksToLogs(t *testing.T) { postableRule := PostableRule{ AlertName: "Tricky Condition Tests", - AlertType: "LOGS_BASED_ALERT", + AlertType: AlertTypeLogs, RuleType: RuleTypeThreshold, EvalWindow: Duration(5 * time.Minute), Frequency: Duration(1 * time.Minute), From 363fb7bc34bce1cfb10a27451c329312142b86a7 Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:00:52 +0530 Subject: [PATCH 59/60] feat: Kafka UI feedbacks (#5801) * fix: solved kafka feature feedbacks * fix: changed coming soon text to - join slack community --- .../SideNav/NavItem/NavItem.styles.scss | 4 ++ .../src/container/SideNav/NavItem/NavItem.tsx | 2 +- frontend/src/container/SideNav/menuItems.tsx | 1 + .../MessagingQueues/MQCommon/MQCommon.tsx | 14 ++++++- .../MQDetailPage/MQDetailPage.tsx | 38 ++++++++++++------- .../MessagingQueues.styles.scss | 2 +- .../pages/MessagingQueues/MessagingQueues.tsx | 13 ++++--- .../MessagingQueues/MessagingQueuesUtils.ts | 19 ++++++++++ 8 files changed, 72 insertions(+), 21 deletions(-) diff --git a/frontend/src/container/SideNav/NavItem/NavItem.styles.scss b/frontend/src/container/SideNav/NavItem/NavItem.styles.scss index aea7b3a0ee..ede4376033 100644 --- a/frontend/src/container/SideNav/NavItem/NavItem.styles.scss +++ b/frontend/src/container/SideNav/NavItem/NavItem.styles.scss @@ -75,6 +75,10 @@ text-overflow: ellipsis; } } + + .beta-tag { + padding-right: 0; + } } .lightMode { diff --git a/frontend/src/container/SideNav/NavItem/NavItem.tsx b/frontend/src/container/SideNav/NavItem/NavItem.tsx index b3d57193f1..97a8cdb0ed 100644 --- a/frontend/src/container/SideNav/NavItem/NavItem.tsx +++ b/frontend/src/container/SideNav/NavItem/NavItem.tsx @@ -24,7 +24,7 @@ export default function NavItem({ onClick={(event): void => onClick(event)} >
-
+
{icon}
{label}
diff --git a/frontend/src/container/SideNav/menuItems.tsx b/frontend/src/container/SideNav/menuItems.tsx index 1566141735..be694227a1 100644 --- a/frontend/src/container/SideNav/menuItems.tsx +++ b/frontend/src/container/SideNav/menuItems.tsx @@ -91,6 +91,7 @@ const menuItems: SidebarItem[] = [ key: ROUTES.MESSAGING_QUEUES, label: 'Messaging Queues', icon: , + isBeta: true, }, { key: ROUTES.LIST_ALL_ALERT, diff --git a/frontend/src/pages/MessagingQueues/MQCommon/MQCommon.tsx b/frontend/src/pages/MessagingQueues/MQCommon/MQCommon.tsx index 1d5394a14a..ce6fd1e96f 100644 --- a/frontend/src/pages/MessagingQueues/MQCommon/MQCommon.tsx +++ b/frontend/src/pages/MessagingQueues/MQCommon/MQCommon.tsx @@ -9,7 +9,19 @@ import { Info } from 'lucide-react'; export function ComingSoon(): JSX.Element { return ( + Join our Slack community for more details:{' '} + e.stopPropagation()} + > + SigNoz Community + +
+ } placement="top" overlayClassName="tooltip-overlay" > diff --git a/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx b/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx index 17baff2790..f82a2f605d 100644 --- a/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx @@ -6,17 +6,12 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; import { ListMinus } from 'lucide-react'; import { useHistory } from 'react-router-dom'; +import { MessagingQueuesViewType } from '../MessagingQueuesUtils'; import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon'; import MessagingQueuesDetails from '../MQDetails/MQDetails'; import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions'; import MessagingQueuesGraph from '../MQGraph/MQGraph'; -enum MessagingQueueViewType { - consumerLag = 'consumerLag', - avgPartitionLatency = 'avgPartitionLatency', - avgProducerLatency = 'avgProducerLatency', -} - function MQDetailPage(): JSX.Element { const history = useHistory(); @@ -36,21 +31,38 @@ function MQDetailPage(): JSX.Element { Kafka / views /