mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 07:28:59 +08:00
feat: show/hide timestamp and body fields in logs explorer (raw, default, column views) (#6903)
* feat: show/hide timestamp and body fields in logs explorer (raw, default, column views) * fix: add width to log indicator column to ensure that a single column doesn't take half the space * fix: handle edge cases and fix issues for show/hide body and timestamp in logs explorer
This commit is contained in:
parent
61a6c21edb
commit
98cdbcd711
@ -220,12 +220,14 @@ function ListLogView({
|
||||
<LogStateIndicator type={logType} fontSize={fontSize} />
|
||||
<div>
|
||||
<LogContainer fontSize={fontSize}>
|
||||
{updatedSelecedFields.some((field) => field.name === 'body') && (
|
||||
<LogGeneralField
|
||||
fieldKey="Log"
|
||||
fieldValue={flattenLogData.body}
|
||||
linesPerRow={linesPerRow}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
)}
|
||||
{flattenLogData.stream && (
|
||||
<LogGeneralField
|
||||
fieldKey="Stream"
|
||||
@ -233,13 +235,17 @@ function ListLogView({
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
)}
|
||||
{updatedSelecedFields.some((field) => field.name === 'timestamp') && (
|
||||
<LogGeneralField
|
||||
fieldKey="Timestamp"
|
||||
fieldValue={timestampValue}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
)}
|
||||
|
||||
{updatedSelecedFields.map((field) =>
|
||||
{updatedSelecedFields
|
||||
.filter((field) => !['timestamp', 'body'].includes(field.name))
|
||||
.map((field) =>
|
||||
isValidLogField(flattenLogData[field.name] as never) ? (
|
||||
<LogSelectedField
|
||||
key={field.name}
|
||||
|
@ -74,6 +74,7 @@ function RawLogView({
|
||||
);
|
||||
|
||||
const attributesValues = updatedSelecedFields
|
||||
.filter((field) => !['timestamp', 'body'].includes(field.name))
|
||||
.map((field) => flattenLogData[field.name])
|
||||
.filter((attribute) => {
|
||||
// loadash isEmpty doesnot work with numbers
|
||||
@ -93,6 +94,13 @@ function RawLogView({
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const text = useMemo(() => {
|
||||
const parts = [];
|
||||
|
||||
// Check if timestamp is selected
|
||||
const showTimestamp = selectedFields.some(
|
||||
(field) => field.name === 'timestamp',
|
||||
);
|
||||
if (showTimestamp) {
|
||||
const date =
|
||||
typeof data.timestamp === 'string'
|
||||
? formatTimezoneAdjustedTimestamp(
|
||||
@ -103,12 +111,23 @@ function RawLogView({
|
||||
data.timestamp / 1e6,
|
||||
DATE_TIME_FORMATS.ISO_DATETIME_MS,
|
||||
);
|
||||
parts.push(date);
|
||||
}
|
||||
|
||||
return `${date} | ${attributesText} ${data.body}`;
|
||||
// Check if body is selected
|
||||
const showBody = selectedFields.some((field) => field.name === 'body');
|
||||
if (showBody) {
|
||||
parts.push(`${attributesText} ${data.body}`);
|
||||
} else {
|
||||
parts.push(attributesText);
|
||||
}
|
||||
|
||||
return parts.join(' | ');
|
||||
}, [
|
||||
selectedFields,
|
||||
attributesText,
|
||||
data.timestamp,
|
||||
data.body,
|
||||
attributesText,
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
]);
|
||||
|
||||
|
@ -49,7 +49,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
|
||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||
.filter((e) => e.name !== 'id')
|
||||
.filter((e) => !['id', 'body', 'timestamp'].includes(e.name))
|
||||
.map(({ name }) => ({
|
||||
title: name,
|
||||
dataIndex: name,
|
||||
@ -92,12 +92,16 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
),
|
||||
}),
|
||||
},
|
||||
...(fields.some((field) => field.name === 'timestamp')
|
||||
? [
|
||||
{
|
||||
title: 'timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
// https://github.com/ant-design/ant-design/discussions/36886
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => {
|
||||
render: (
|
||||
field: string | number,
|
||||
): ColumnTypeRender<Record<string, unknown>> => {
|
||||
const date =
|
||||
typeof field === 'string'
|
||||
? formatTimezoneAdjustedTimestamp(
|
||||
@ -119,12 +123,18 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
};
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(appendTo === 'center' ? fieldColumns : []),
|
||||
...(fields.some((field) => field.name === 'body')
|
||||
? [
|
||||
{
|
||||
title: 'body',
|
||||
dataIndex: 'body',
|
||||
key: 'body',
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
render: (
|
||||
field: string | number,
|
||||
): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
style: defaultTableStyle,
|
||||
},
|
||||
@ -132,7 +142,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
<TableBodyContent
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: convert.toHtml(
|
||||
dompurify.sanitize(unescapeString(field), {
|
||||
dompurify.sanitize(unescapeString(field as string), {
|
||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||
}),
|
||||
),
|
||||
@ -144,6 +154,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
),
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(appendTo === 'end' ? fieldColumns : []),
|
||||
];
|
||||
}, [
|
||||
|
@ -417,11 +417,13 @@ export default function LogsFormatOptionsMenu({
|
||||
{key}
|
||||
</Tooltip>
|
||||
</div>
|
||||
{addColumn?.value?.length > 1 && (
|
||||
<X
|
||||
className="delete-btn"
|
||||
size={14}
|
||||
onClick={(): void => addColumn.onRemove(id as string)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{addColumn && addColumn?.value?.length === 0 && (
|
||||
|
@ -37,7 +37,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, isEqual } from 'lodash-es';
|
||||
import { cloneDeep, isEqual, omit } from 'lodash-es';
|
||||
import {
|
||||
Check,
|
||||
ConciergeBell,
|
||||
@ -256,13 +256,17 @@ function ExplorerOptions({
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
|
||||
const { options, handleOptionsChange } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS,
|
||||
dataSource: DataSource.TRACES,
|
||||
storageKey:
|
||||
sourcepage === DataSource.TRACES
|
||||
? LOCALSTORAGE.TRACES_LIST_OPTIONS
|
||||
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: sourcepage,
|
||||
aggregateOperator: StringOperators.NOOP,
|
||||
});
|
||||
|
||||
type ExtraData = {
|
||||
selectColumns?: BaseAutocompleteData[];
|
||||
version?: number;
|
||||
};
|
||||
|
||||
const updateOrRestoreSelectColumns = (
|
||||
@ -283,14 +287,20 @@ function ExplorerOptions({
|
||||
console.error('Error parsing extraData:', error);
|
||||
}
|
||||
|
||||
let backwardCompatibleOptions = options;
|
||||
|
||||
if (!extraData?.version) {
|
||||
backwardCompatibleOptions = omit(options, 'version');
|
||||
}
|
||||
|
||||
if (extraData.selectColumns?.length) {
|
||||
handleOptionsChange({
|
||||
...options,
|
||||
...backwardCompatibleOptions,
|
||||
selectColumns: extraData.selectColumns,
|
||||
});
|
||||
} else if (!isEqual(defaultTraceSelectedColumns, options.selectColumns)) {
|
||||
handleOptionsChange({
|
||||
...options,
|
||||
...backwardCompatibleOptions,
|
||||
selectColumns: defaultTraceSelectedColumns,
|
||||
});
|
||||
}
|
||||
@ -423,6 +433,7 @@ function ExplorerOptions({
|
||||
extraData: JSON.stringify({
|
||||
color,
|
||||
selectColumns: options.selectColumns,
|
||||
version: 1,
|
||||
}),
|
||||
notifications,
|
||||
panelType: panelType || PANEL_TYPES.LIST,
|
||||
|
@ -121,7 +121,9 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
const tableHeader = useCallback(
|
||||
() => (
|
||||
<tr>
|
||||
{tableColumns.map((column) => {
|
||||
{tableColumns
|
||||
.filter((column) => column.key)
|
||||
.map((column) => {
|
||||
const isDragColumn = column.key !== 'expand';
|
||||
|
||||
return (
|
||||
|
@ -29,7 +29,7 @@ export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
|
||||
props.$isDarkMode ? 'inherit' : themeColors.whiteCream};
|
||||
|
||||
${({ $isLogIndicator }): string =>
|
||||
$isLogIndicator ? 'padding: 0 0 0 8px;' : ''}
|
||||
$isLogIndicator ? 'padding: 0 0 0 8px;width: 15px;' : ''}
|
||||
color: ${(props): string =>
|
||||
props.$isDarkMode ? themeColors.white : themeColors.bckgGrey};
|
||||
`;
|
||||
|
@ -11,6 +11,27 @@ export const defaultOptionsQuery: OptionsQuery = {
|
||||
fontSize: FontSize.SMALL,
|
||||
};
|
||||
|
||||
export const defaultLogsSelectedColumns = [
|
||||
{
|
||||
key: 'timestamp',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'timestamp--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'body',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'body--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultTraceSelectedColumns = [
|
||||
{
|
||||
key: 'serviceName',
|
||||
|
@ -17,6 +17,7 @@ export interface OptionsQuery {
|
||||
maxLines: number;
|
||||
format: LogViewMode;
|
||||
fontSize: FontSize;
|
||||
version?: number;
|
||||
}
|
||||
|
||||
export interface InitialOptions
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
defaultOptionsQuery,
|
||||
defaultTraceSelectedColumns,
|
||||
URL_OPTIONS,
|
||||
@ -169,6 +170,15 @@ const useOptionsMenu = ({
|
||||
|
||||
const searchedAttributeKeys = useMemo(() => {
|
||||
if (searchedAttributesData?.payload?.attributeKeys?.length) {
|
||||
if (dataSource === DataSource.LOGS) {
|
||||
// add timestamp and body to the list of attributes
|
||||
return [
|
||||
...defaultLogsSelectedColumns,
|
||||
...searchedAttributesData.payload.attributeKeys.filter(
|
||||
(attribute) => attribute.key !== 'body',
|
||||
),
|
||||
];
|
||||
}
|
||||
return searchedAttributesData.payload.attributeKeys;
|
||||
}
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
@ -198,12 +208,17 @@ const useOptionsMenu = ({
|
||||
);
|
||||
|
||||
const optionsFromAttributeKeys = useMemo(() => {
|
||||
const filteredAttributeKeys = searchedAttributeKeys.filter(
|
||||
(item) => item.key !== 'body',
|
||||
);
|
||||
const filteredAttributeKeys = searchedAttributeKeys.filter((item) => {
|
||||
// For other data sources, only filter out 'body' if it exists
|
||||
if (dataSource !== DataSource.LOGS) {
|
||||
return item.key !== 'body';
|
||||
}
|
||||
// For LOGS, keep all keys
|
||||
return true;
|
||||
});
|
||||
|
||||
return getOptionsFromKeys(filteredAttributeKeys, selectedColumnKeys);
|
||||
}, [searchedAttributeKeys, selectedColumnKeys]);
|
||||
}, [dataSource, searchedAttributeKeys, selectedColumnKeys]);
|
||||
|
||||
const handleRedirectWithOptionsData = useCallback(
|
||||
(newQueryData: OptionsQuery) => {
|
||||
|
@ -9,11 +9,18 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
||||
import LogsExplorerViews from 'container/LogsExplorerViews';
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
defaultOptionsQuery,
|
||||
URL_OPTIONS,
|
||||
} from 'container/OptionsMenu/constants';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { isNull } from 'lodash-es';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { isEqual, isNull } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@ -73,6 +80,39 @@ function LogsExplorer(): JSX.Element {
|
||||
}
|
||||
}, [currentQuery.builder.queryData, currentQuery.builder.queryData.length]);
|
||||
|
||||
const {
|
||||
queryData: optionsQueryData,
|
||||
redirectWithQuery: redirectWithOptionsData,
|
||||
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||
|
||||
const migrateOptionsQuery = (query: OptionsQuery): OptionsQuery => {
|
||||
// If version is missing AND timestamp/body are not in selectColumns, this is an old URL
|
||||
if (
|
||||
!query.version &&
|
||||
!query.selectColumns.some((col) => col.key === 'timestamp') &&
|
||||
!query.selectColumns.some((col) => col.key === 'body')
|
||||
) {
|
||||
return {
|
||||
...query,
|
||||
version: 1,
|
||||
selectColumns: [
|
||||
// Add default timestamp and body columns
|
||||
...defaultLogsSelectedColumns,
|
||||
...query.selectColumns,
|
||||
],
|
||||
};
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const migratedQuery = migrateOptionsQuery(optionsQueryData);
|
||||
// Only redirect if the query was actually modified
|
||||
if (!isEqual(migratedQuery, optionsQueryData)) {
|
||||
redirectWithOptionsData(migratedQuery);
|
||||
}
|
||||
}, [optionsQueryData, redirectWithOptionsData]);
|
||||
|
||||
const isMultipleQueries = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData?.length > 1 ||
|
||||
|
Loading…
x
Reference in New Issue
Block a user