Compare commits

...

2 Commits

Author SHA1 Message Date
Tushar Vats
509a1cfb85 fix: added constants to improve readability 2025-11-18 00:03:24 +05:30
Tushar Vats
fd118d386a fix: handle conflicting attributes 2025-11-17 20:54:50 +05:30
18 changed files with 448 additions and 183 deletions

View File

@@ -10,6 +10,10 @@ import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import {
LOG_FIELD_BODY_KEY,
LOG_FIELD_TIMESTAMP_KEY,
} from 'lib/logs/flatLogData';
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso } from 'react-virtuoso';
@@ -85,11 +89,15 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
dataType: 'string',
type: '',
name: 'body',
displayName: 'Body',
key: LOG_FIELD_BODY_KEY,
},
{
dataType: 'string',
type: '',
name: 'timestamp',
displayName: 'Timestamp',
key: LOG_FIELD_TIMESTAMP_KEY,
},
]}
/>

View File

@@ -13,6 +13,10 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
import {
LOG_FIELD_BODY_KEY,
LOG_FIELD_TIMESTAMP_KEY,
} from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useMemo, useState } from 'react';
// interfaces
@@ -42,7 +46,9 @@ interface LogFieldProps {
}
type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> &
Pick<AddToQueryHOCProps, 'onAddToQuery'>;
Pick<AddToQueryHOCProps, 'onAddToQuery'> & {
fieldKeyDisplay: string;
};
function LogGeneralField({
fieldKey,
@@ -74,6 +80,7 @@ function LogGeneralField({
function LogSelectedField({
fieldKey = '',
fieldValue = '',
fieldKeyDisplay = '',
onAddToQuery,
fontSize,
}: LogSelectedFieldProps): JSX.Element {
@@ -90,7 +97,7 @@ function LogSelectedField({
style={{ color: blue[4] }}
className={cx('selected-log-field-key', fontSize)}
>
{fieldKey}
{fieldKeyDisplay}
</span>
</Typography.Text>
</AddToQueryHOC>
@@ -162,7 +169,7 @@ function ListLogView({
);
const updatedSelecedFields = useMemo(
() => selectedFields.filter((e) => e.name !== 'id'),
() => selectedFields.filter((e) => e.key !== 'id'),
[selectedFields],
);
@@ -170,16 +177,16 @@ function ListLogView({
const timestampValue = useMemo(
() =>
typeof flattenLogData.timestamp === 'string'
typeof flattenLogData[LOG_FIELD_TIMESTAMP_KEY] === 'string'
? formatTimezoneAdjustedTimestamp(
flattenLogData.timestamp,
flattenLogData[LOG_FIELD_TIMESTAMP_KEY],
DATE_TIME_FORMATS.ISO_DATETIME_MS,
)
: formatTimezoneAdjustedTimestamp(
flattenLogData.timestamp / 1e6,
flattenLogData[LOG_FIELD_TIMESTAMP_KEY] / 1e6,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
),
[flattenLogData.timestamp, formatTimezoneAdjustedTimestamp],
[flattenLogData, formatTimezoneAdjustedTimestamp],
);
const logType = getLogIndicatorType(logData);
@@ -215,10 +222,12 @@ function ListLogView({
/>
<div>
<LogContainer fontSize={fontSize}>
{updatedSelecedFields.some((field) => field.name === 'body') && (
{updatedSelecedFields.some(
(field) => field.key === LOG_FIELD_BODY_KEY,
) && (
<LogGeneralField
fieldKey="Log"
fieldValue={flattenLogData.body}
fieldValue={flattenLogData[LOG_FIELD_BODY_KEY]}
linesPerRow={linesPerRow}
fontSize={fontSize}
/>
@@ -230,7 +239,9 @@ function ListLogView({
fontSize={fontSize}
/>
)}
{updatedSelecedFields.some((field) => field.name === 'timestamp') && (
{updatedSelecedFields.some(
(field) => field.key === LOG_FIELD_TIMESTAMP_KEY,
) && (
<LogGeneralField
fieldKey="Timestamp"
fieldValue={timestampValue}
@@ -239,13 +250,17 @@ function ListLogView({
)}
{updatedSelecedFields
.filter((field) => !['timestamp', 'body'].includes(field.name))
.filter(
(field) =>
![LOG_FIELD_TIMESTAMP_KEY, LOG_FIELD_BODY_KEY].includes(field.key),
)
.map((field) =>
isValidLogField(flattenLogData[field.name] as never) ? (
isValidLogField(flattenLogData[field.key] as never) ? (
<LogSelectedField
key={field.name}
fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never}
key={field.key}
fieldKey={field.key}
fieldKeyDisplay={field.displayName}
fieldValue={flattenLogData[field.key] as never}
onAddToQuery={onAddToQuery}
fontSize={fontSize}
/>

View File

@@ -73,16 +73,25 @@ function RawLogView({
);
const attributesValues = updatedSelecedFields
.filter((field) => !['timestamp', 'body'].includes(field.name))
.map((field) => flattenLogData[field.name])
.filter((attribute) => {
.filter(
(field) => !['log.timestamp:string', 'log.body:string'].includes(field.key),
)
.map((field) => {
const value = flattenLogData[field.key];
const label = field.displayName;
// loadash isEmpty doesnot work with numbers
if (isNumber(attribute)) {
return true;
if (isNumber(value)) {
return `${label}: ${value}`;
}
return !isUndefined(attribute) && !isEmpty(attribute);
});
if (!isUndefined(value) && !isEmpty(value)) {
return `${label}: ${value}`;
}
return null;
})
.filter((attribute) => attribute !== null);
let attributesText = attributesValues.join(' | ');

View File

@@ -6,7 +6,11 @@ import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import {
FlatLogData,
LOG_FIELD_BODY_KEY,
LOG_FIELD_TIMESTAMP_KEY,
} from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { useMemo } from 'react';
@@ -51,28 +55,33 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => !['id', 'body', 'timestamp'].includes(e.name))
.map(({ name }) => ({
title: name,
dataIndex: name,
accessorKey: name,
id: name.toLowerCase().replace(/\./g, '_'),
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: isListViewPanel
? defaultListViewPanelStyle
: getDefaultCellStyle(isDarkMode),
},
children: (
<Typography.Paragraph
ellipsis={{ rows: linesPerRow }}
className={cx('paragraph', fontSize)}
>
{field}
</Typography.Paragraph>
),
}),
.filter(
(e) => !['id', LOG_FIELD_BODY_KEY, LOG_FIELD_TIMESTAMP_KEY].includes(e.key),
)
.map((field) => ({
title: field.displayName,
dataIndex: field.key,
accessorKey: field.key,
id: field.key.toLowerCase().replace(/\./g, '_').replace(/:/g, '_'),
key: field.key,
render: (fieldValue, record): ColumnTypeRender<Record<string, unknown>> => {
const value = record[field.key] || fieldValue;
return {
props: {
style: isListViewPanel
? defaultListViewPanelStyle
: getDefaultCellStyle(isDarkMode),
},
children: (
<Typography.Paragraph
ellipsis={{ rows: linesPerRow }}
className={cx('paragraph', fontSize)}
>
{value}
</Typography.Paragraph>
),
};
},
}));
if (isListViewPanel) {
@@ -100,26 +109,29 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
),
}),
},
...(fields.some((field) => field.name === 'timestamp')
...(fields.some((field) => field.key === LOG_FIELD_TIMESTAMP_KEY)
? [
{
title: 'timestamp',
dataIndex: 'timestamp',
dataIndex: LOG_FIELD_TIMESTAMP_KEY,
key: 'timestamp',
accessorKey: 'timestamp',
accessorKey: LOG_FIELD_TIMESTAMP_KEY,
id: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (
field: string | number,
record: Record<string, unknown>,
): ColumnTypeRender<Record<string, unknown>> => {
const timestampValue =
(record[LOG_FIELD_TIMESTAMP_KEY] as string | number) || field;
const date =
typeof field === 'string'
typeof timestampValue === 'string'
? formatTimezoneAdjustedTimestamp(
field,
timestampValue,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
)
: formatTimezoneAdjustedTimestamp(
field / 1e6,
timestampValue / 1e6,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
);
return {
@@ -136,33 +148,37 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
]
: []),
...(appendTo === 'center' ? fieldColumns : []),
...(fields.some((field) => field.name === 'body')
...(fields.some((field) => field.key === LOG_FIELD_BODY_KEY)
? [
{
title: 'body',
dataIndex: 'body',
dataIndex: LOG_FIELD_BODY_KEY,
key: 'body',
accessorKey: 'body',
accessorKey: LOG_FIELD_BODY_KEY,
id: 'body',
render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: bodyColumnStyle,
},
children: (
<TableBodyContent
dangerouslySetInnerHTML={{
__html: getSanitizedLogBody(field as string, {
shouldEscapeHtml: true,
}),
}}
fontSize={fontSize}
linesPerRow={linesPerRow}
isDarkMode={isDarkMode}
/>
),
}),
record: Record<string, unknown>,
): ColumnTypeRender<Record<string, unknown>> => {
const bodyValue = (record[LOG_FIELD_BODY_KEY] as string) || '';
return {
props: {
style: bodyColumnStyle,
},
children: (
<TableBodyContent
dangerouslySetInnerHTML={{
__html: getSanitizedLogBody(bodyValue, {
shouldEscapeHtml: true,
}),
}}
fontSize={fontSize}
linesPerRow={linesPerRow}
isDarkMode={isDarkMode}
/>
),
};
},
},
]
: []),

View File

@@ -416,18 +416,21 @@ function OptionsMenu({
)}
<div className="column-format">
{addColumn?.value?.map(({ name }) => (
<div className="column-name" key={name}>
{addColumn?.value?.map((column) => (
<div className="column-name" key={column.key}>
<div className="name">
<Tooltip placement="left" title={name}>
{name}
<Tooltip
placement="left"
title={column.displayName || column.name}
>
{column.displayName || column.name}
</Tooltip>
</div>
{addColumn?.value?.length > 1 && (
<X
className="delete-btn"
size={14}
onClick={(): void => addColumn.onRemove(name)}
onClick={(): void => addColumn.onRemove(column.key)}
/>
)}
</div>

View File

@@ -11,6 +11,10 @@ import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import {
LOG_FIELD_BODY_KEY,
LOG_FIELD_TIMESTAMP_KEY,
} from 'lib/logs/flatLogData';
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso } from 'react-virtuoso';
@@ -75,11 +79,15 @@ function EntityLogs({
dataType: 'string',
type: '',
name: 'body',
displayName: 'Body',
key: LOG_FIELD_BODY_KEY,
},
{
dataType: 'string',
type: '',
name: 'timestamp',
displayName: 'Timestamp',
key: LOG_FIELD_TIMESTAMP_KEY,
},
]}
/>

View File

@@ -13,7 +13,6 @@ import InfinityTableView from 'container/LogsExplorerList/InfinityTableView';
import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
import { useOptionsMenu } from 'container/OptionsMenu';
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useEventSource } from 'providers/EventSource';
@@ -57,10 +56,7 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
[formattedLogs, activeLogId],
);
const selectedFields = convertKeysToColumnFields([
...defaultLogsSelectedColumns,
...options.selectColumns,
]);
const selectedFields = convertKeysToColumnFields(options.selectColumns);
const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => {

View File

@@ -13,7 +13,9 @@ export const convertKeysToColumnFields = (
.filter((item) => !isEmpty(item.name))
.map((item) => ({
dataType: item.fieldDataType ?? '',
name: item.name,
name: item.name ?? '',
key: item.key ?? '',
displayName: item.displayName ?? '',
type: item.fieldContext ?? '',
}));
/**

View File

@@ -1,6 +1,10 @@
import { ColumnsType } from 'antd/es/table';
import { Typography } from 'antd/lib';
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import {
LOG_FIELD_BODY_KEY,
LOG_FIELD_TIMESTAMP_KEY,
} from 'lib/logs/flatLogData';
// import Typography from 'antd/es/typography/Typography';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ReactNode } from 'react';
@@ -18,15 +22,15 @@ export const getLogPanelColumnsList = (
const columns: ColumnsType<RowData> =
selectedLogFields?.map((field: IField) => {
const { name } = field;
const { name, key, displayName } = field;
return {
title: name,
dataIndex: name,
key: name,
width: name === 'body' ? 350 : 100,
title: displayName,
dataIndex: key,
key,
width: key === LOG_FIELD_BODY_KEY ? 350 : 100,
render: (value: ReactNode): JSX.Element => {
if (name === 'timestamp') {
if (key === LOG_FIELD_TIMESTAMP_KEY) {
return (
<Typography.Text>
{formatTimezoneAdjustedTimestamp(value as string)}
@@ -34,7 +38,7 @@ export const getLogPanelColumnsList = (
);
}
if (name === 'body') {
if (key === LOG_FIELD_BODY_KEY) {
return (
<Typography.Paragraph ellipsis={{ rows: 1 }} data-testid={name}>
{value}

View File

@@ -558,8 +558,10 @@ export const getDefaultWidgetData = (
decimalPrecision: PrecisionOptionsEnum.TWO, // default decimal precision
selectedLogFields: defaultLogsSelectedColumns.map((field) => ({
...field,
key: field.key ?? '',
type: field.fieldContext ?? '',
dataType: field.fieldDataType ?? '',
displayName: field.displayName || field.name,
})),
selectedTracesFields: defaultTraceSelectedColumns,
});

View File

@@ -42,10 +42,12 @@ function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
</SearchIconWrapper>
</Input.Group>
{config.value?.map(({ name }) => (
<AddColumnItem direction="horizontal" key={name}>
<Typography>{name}</Typography>
<DeleteOutlinedIcon onClick={(): void => config.onRemove(name)} />
{config.value?.map((column) => (
<AddColumnItem direction="horizontal" key={column.key}>
<Typography>{column.displayName || column.name}</Typography>
<DeleteOutlinedIcon
onClick={(): void => config.onRemove(column.key || column.name)}
/>
</AddColumnItem>
))}
</AddColumnWrapper>

View File

@@ -1,4 +1,8 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import {
LOG_FIELD_BODY_KEY,
LOG_FIELD_TIMESTAMP_KEY,
} from 'lib/logs/flatLogData';
import { FontSize, OptionsQuery } from './types';
@@ -16,15 +20,19 @@ export const defaultLogsSelectedColumns: TelemetryFieldKey[] = [
name: 'timestamp',
signal: 'logs',
fieldContext: 'log',
fieldDataType: '',
fieldDataType: 'string',
isIndexed: false,
key: LOG_FIELD_TIMESTAMP_KEY,
displayName: 'Timestamp',
},
{
name: 'body',
signal: 'logs',
fieldContext: 'log',
fieldDataType: '',
fieldDataType: 'string',
isIndexed: false,
key: LOG_FIELD_BODY_KEY,
displayName: 'Body',
},
];
@@ -34,29 +42,47 @@ export const defaultTraceSelectedColumns: TelemetryFieldKey[] = [
signal: 'traces',
fieldContext: 'resource',
fieldDataType: 'string',
key: 'resource.service.name:string',
displayName: 'Service Name',
},
{
name: 'name',
signal: 'traces',
fieldContext: 'span',
fieldDataType: 'string',
key: 'span.name:string',
displayName: 'Span Name',
},
{
name: 'duration_nano',
signal: 'traces',
fieldContext: 'span',
fieldDataType: '',
fieldDataType: 'number',
key: 'span.duration_nano:number',
displayName: 'Duration',
},
{
name: 'http_method',
signal: 'traces',
fieldContext: 'span',
fieldDataType: '',
fieldDataType: 'string',
key: 'span.http_method:string',
displayName: 'HTTP Method',
},
{
name: 'response_status_code',
signal: 'traces',
fieldContext: 'span',
fieldDataType: '',
fieldDataType: 'string',
key: 'span.response_status_code:string',
displayName: 'Status Code',
},
{
name: 'timestamp',
signal: 'traces',
fieldContext: 'span',
fieldDataType: 'string',
key: 'span.timestamp:string',
displayName: 'Timestamp',
},
];

View File

@@ -16,11 +16,6 @@ import {
QueryKeyRequestProps,
QueryKeySuggestionsResponseProps,
} from 'types/api/querySuggestions/types';
import {
FieldContext,
FieldDataType,
SignalType,
} from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import {
@@ -35,7 +30,11 @@ import {
OptionsMenuConfig,
OptionsQuery,
} from './types';
import { getOptionsFromKeys } from './utils';
import {
createTelemetryFieldKey,
getOptionsFromKeys,
resolveColumnConflicts,
} from './utils';
interface UseOptionsMenuProps {
storageKey?: string;
@@ -104,28 +103,19 @@ const useOptionsMenu = ({
return [];
}
const attributesData = initialAttributesResult?.reduce(
(acc: TelemetryFieldKey[], attributeResponse): TelemetryFieldKey[] => {
const suggestions =
Object.values(attributeResponse?.data?.data?.data?.keys || {}).flat() ||
[];
// Collect all suggestions from all API responses first
const allSuggestions =
initialAttributesResult?.flatMap((attributeResponse) =>
Object.values(attributeResponse?.data?.data?.data?.keys || {})
.flat()
.map((suggestion) => createTelemetryFieldKey(suggestion)),
) || [];
const mappedSuggestions: TelemetryFieldKey[] = suggestions.map(
(suggestion) => ({
name: suggestion.name,
signal: suggestion.signal as SignalType,
fieldDataType: suggestion.fieldDataType as FieldDataType,
fieldContext: suggestion.fieldContext as FieldContext,
}),
);
return [...acc, ...mappedSuggestions];
},
[],
);
// Resolve conflicts and deduplicate once at the end for better performance
const attributesData = resolveColumnConflicts(allSuggestions);
let initialSelected: TelemetryFieldKey[] = (initialOptions?.selectColumns
?.map((column) => attributesData.find(({ name }) => name === column))
?.map((column) => attributesData.find(({ key }) => key === column))
.filter((e) => !!e) || []) as TelemetryFieldKey[];
if (dataSource === DataSource.TRACES) {
@@ -133,13 +123,15 @@ const useOptionsMenu = ({
?.map((col) => {
if (col && Object.keys(AllTraceFilterKeyValue).includes(col?.name)) {
const metaData = defaultTraceSelectedColumns.find(
(coln) => coln.name === col.name,
(coln) => coln.key === col.key,
);
return {
...metaData,
name: metaData?.name || '',
};
if (metaData) {
return {
...metaData,
name: metaData.name,
};
}
}
return col;
})
@@ -187,40 +179,23 @@ const useOptionsMenu = ({
const searchedAttributesDataList = Object.values(
searchedAttributesDataV5?.data.data.keys || {},
).flat();
if (searchedAttributesDataList.length) {
if (dataSource === DataSource.LOGS) {
const logsSelectedColumns: TelemetryFieldKey[] = defaultLogsSelectedColumns.map(
(e) => ({
...e,
name: e.name,
signal: e.signal as SignalType,
fieldContext: e.fieldContext as FieldContext,
fieldDataType: e.fieldDataType as FieldDataType,
}),
);
return [
...logsSelectedColumns,
...searchedAttributesDataList
.filter((attribute) => attribute.name !== 'body')
// eslint-disable-next-line sonarjs/no-identical-functions
.map((e) => ({
...e,
name: e.name,
signal: e.signal as SignalType,
fieldContext: e.fieldContext as FieldContext,
fieldDataType: e.fieldDataType as FieldDataType,
})),
];
}
// eslint-disable-next-line sonarjs/no-identical-functions
return searchedAttributesDataList.map((e) => ({
...e,
name: e.name,
signal: e.signal as SignalType,
fieldContext: e.fieldContext as FieldContext,
fieldDataType: e.fieldDataType as FieldDataType,
}));
// Map all attributes with proper key and displayName
const mappedAttributes = searchedAttributesDataList.map((e) =>
createTelemetryFieldKey(e),
);
// Combine with default columns and resolve conflicts
const allColumns =
dataSource === DataSource.LOGS
? [...defaultLogsSelectedColumns, ...mappedAttributes]
: mappedAttributes;
// Resolve conflicts with deduplication
return resolveColumnConflicts(allColumns);
}
if (dataSource === DataSource.TRACES) {
return defaultTraceSelectedColumns.map((e) => ({
...e,
@@ -234,19 +209,10 @@ const useOptionsMenu = ({
const initialOptionsQuery: OptionsQuery = useMemo(() => {
let defaultColumns: TelemetryFieldKey[] = defaultOptionsQuery.selectColumns;
if (dataSource === DataSource.TRACES) {
defaultColumns = defaultTraceSelectedColumns.map((e) => ({
...e,
name: e.name,
}));
defaultColumns = defaultTraceSelectedColumns;
} else if (dataSource === DataSource.LOGS) {
// eslint-disable-next-line sonarjs/no-identical-functions
defaultColumns = defaultLogsSelectedColumns.map((e) => ({
...e,
name: e.name,
signal: e.signal as SignalType,
fieldContext: e.fieldContext as FieldContext,
fieldDataType: e.fieldDataType as FieldDataType,
}));
defaultColumns = defaultLogsSelectedColumns;
}
const finalSelectColumns = initialOptions?.selectColumns
@@ -261,7 +227,7 @@ const useOptionsMenu = ({
}, [dataSource, initialOptions, initialSelectedColumns]);
const selectedColumnKeys = useMemo(
() => preferences?.columns?.map(({ name }) => name) || [],
() => preferences?.columns?.map(({ key }) => key) || [],
[preferences?.columns],
);
@@ -290,7 +256,7 @@ const useOptionsMenu = ({
const column = [
...searchedAttributeKeys,
...(preferences?.columns || []),
].find(({ name }) => name === key);
].find((column) => column.key === key);
if (!column) return acc;
return [...acc, column];
@@ -319,7 +285,7 @@ const useOptionsMenu = ({
const handleRemoveSelectedColumn = useCallback(
(columnKey: string) => {
const newSelectedColumns = preferences?.columns?.filter(
({ name }) => name !== columnKey,
(column) => column.key !== columnKey,
);
if (!newSelectedColumns?.length && dataSource !== DataSource.LOGS) {

View File

@@ -1,13 +1,163 @@
import { SelectProps } from 'antd';
import { TelemetryFieldKey } from 'api/v5/v5';
import {
FieldContext,
FieldDataType,
SignalType,
} from 'types/api/v5/queryRange';
/**
* Display name mapping for log fieldContext columns
* Provides user-friendly names for standard log fields
*/
const LOG_FIELD_DISPLAY_NAMES: Record<string, string> = {
body: 'Body',
severity_number: 'Severity Number',
severity_text: 'Severity Text',
span_id: 'Span ID',
trace_flags: 'Trace Flags',
trace_id: 'Trace ID',
scope_name: 'Scope Name',
scope_version: 'Scope Version',
};
/**
* Helper function to create a TelemetryFieldKey with properly formatted key and displayName
* Ensures consistent key format: fieldContext.name:fieldDataType
* Uses display name map for log fieldContext columns
* @param suggestion - The raw suggestion data from the API
* @returns A TelemetryFieldKey with key and displayName fields properly set
*/
export function createTelemetryFieldKey(suggestion: any): TelemetryFieldKey {
let displayName = suggestion.displayName || suggestion.name;
// Use mapped display name for log fieldContext columns
// We need to check the fieldContext to avoid overriding non-log fields coming from attributes
if (
(suggestion.fieldContext === 'log' || suggestion.fieldContext === 'scope') &&
LOG_FIELD_DISPLAY_NAMES[suggestion.name]
) {
displayName = LOG_FIELD_DISPLAY_NAMES[suggestion.name];
}
return {
name: suggestion.name,
displayName,
key: `${suggestion.fieldContext}.${suggestion.name}:${suggestion.fieldDataType}`,
signal: suggestion.signal as SignalType,
fieldDataType: suggestion.fieldDataType as FieldDataType,
fieldContext: suggestion.fieldContext as FieldContext,
};
}
/**
* Generates a suffix for a column display name based on conflicts
* @param column - The column to generate suffix for
* @param hasContextConflict - Whether there's a conflict in fieldContext
* @param hasDataTypeConflict - Whether there's a conflict in fieldDataType
* @returns The suffix string to append to the column name
*/
function generateColumnSuffix(
column: TelemetryFieldKey,
hasContextConflict: boolean,
hasDataTypeConflict: boolean,
): string {
if (hasContextConflict && column.fieldContext) {
return ` (${column.fieldContext})`;
}
if (!hasContextConflict && hasDataTypeConflict && column.fieldDataType) {
return ` (${column.fieldDataType})`;
}
return '';
}
/**
* Updates display names for conflicting columns in the columnsByKey map
* @param columns - Array of columns with the same name
* @param columnsByKey - Map to update with new display names
*/
function updateConflictingDisplayNames(
columns: TelemetryFieldKey[],
columnsByKey: Map<string, TelemetryFieldKey>,
): void {
const contexts = new Set(columns.map((c) => c.fieldContext));
const dataTypes = new Set(columns.map((c) => c.fieldDataType));
const hasContextConflict = contexts.size > 1;
const hasDataTypeConflict = dataTypes.size > 1;
if (!hasContextConflict && !hasDataTypeConflict) {
return;
}
columns.forEach((column) => {
// Skip if already has a custom displayName (not just the name)
if (column.displayName && column.displayName !== column.name) {
return;
}
const suffix = generateColumnSuffix(
column,
hasContextConflict,
hasDataTypeConflict,
);
if (suffix) {
columnsByKey.set(column.key || column.name, {
...column,
displayName: `${column.name}${suffix}`,
});
}
});
}
/**
* Processes a list of TelemetryFieldKeys and updates displayName for conflicting columns
* Adds suffix with fieldContext and/or fieldDataType when columns have the same name
* but different context or data type.
* Note: 'log' & 'scope' fieldContext suffix is hidden as it's the default context for logs.
* Also deduplicates columns with the same key.
* @param suggestions - Array of TelemetryFieldKey objects to process
* @returns Array with updated displayNames for conflicting columns and no duplicates
*/
export function resolveColumnConflicts(
suggestions: TelemetryFieldKey[],
): TelemetryFieldKey[] {
// Use Map for O(1) deduplication by key
const columnsByKey = new Map<string, TelemetryFieldKey>();
// Track columns by name to detect conflicts
const columnsByName = new Map<string, TelemetryFieldKey[]>();
// First pass: deduplicate by key and group by name
suggestions.forEach((suggestion) => {
// Skip duplicates (same key)
if (columnsByKey.has(suggestion.key || suggestion.name)) {
return;
}
columnsByKey.set(suggestion.key || suggestion.name, suggestion);
// Group by name for conflict detection
const existing = columnsByName.get(suggestion.name) || [];
columnsByName.set(suggestion.name, [...existing, suggestion]);
});
// Second pass: resolve conflicts for columns with same name
columnsByName.forEach((columns) => {
if (columns.length > 1) {
updateConflictingDisplayNames(columns, columnsByKey);
}
});
return Array.from(columnsByKey.values());
}
export const getOptionsFromKeys = (
keys: TelemetryFieldKey[],
selectedKeys: (string | undefined)[],
): SelectProps['options'] => {
const options = keys.map(({ name }) => ({
label: name,
value: name,
const options = keys.map(({ key, displayName, name }) => ({
label: displayName || name,
value: key,
}));
return options.filter(

View File

@@ -20,6 +20,10 @@ import { FontSize } from 'container/OptionsMenu/types';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import createQueryParams from 'lib/createQueryParams';
import {
LOG_FIELD_BODY_KEY,
LOG_FIELD_TIMESTAMP_KEY,
} from 'lib/logs/flatLogData';
import { Compass } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { Virtuoso } from 'react-virtuoso';
@@ -182,12 +186,16 @@ function SpanLogs({
{
dataType: 'string',
type: '',
displayName: 'Body',
name: 'body',
key: LOG_FIELD_BODY_KEY,
},
{
dataType: 'string',
type: '',
name: 'timestamp',
key: LOG_FIELD_TIMESTAMP_KEY,
displayName: 'Timestamp',
},
]}
/>

View File

@@ -1,17 +1,61 @@
import { defaultTo } from 'lodash-es';
import { ILog } from 'types/api/logs/log';
// Exported constants for top-level field mappings
export const LOG_FIELD_ID_KEY = 'id';
export const LOG_FIELD_TIMESTAMP_KEY = 'log.timestamp:string';
export const LOG_FIELD_BODY_KEY = 'log.body:string';
export const LOG_FIELD_SPAN_ID_KEY = 'log.span_id:string';
export const LOG_FIELD_TRACE_ID_KEY = 'log.trace_id:string';
export const LOG_FIELD_TRACE_FLAGS_KEY = 'log.trace_flags:number';
export const LOG_FIELD_SEVERITY_TEXT_KEY = 'log.severity_text:string';
export const LOG_FIELD_SEVERITY_NUMBER_KEY = 'log.severity_number:number';
export const LOG_FIELD_SCOPE_NAME_KEY = 'scope.scope_name:string';
export const LOG_FIELD_SCOPE_VERSION_KEY = 'scope.scope_version:string';
export function FlatLogData(log: ILog): Record<string, string> {
const flattenLogObject: Record<string, string> = {};
Object.keys(log).forEach((key: string): void => {
if (typeof log[key as never] !== 'object') {
flattenLogObject[key] = log[key as never];
} else {
Object.keys(defaultTo(log[key as never], {})).forEach((childKey) => {
flattenLogObject[childKey] = log[key as never][childKey];
// Map of field names to their contexts and data types
const fieldMappings: Record<string, { context: string; datatype: string }> = {
resources_string: { context: 'resource', datatype: 'string' },
scope_string: { context: 'scope', datatype: 'string' },
attributes_string: { context: 'attribute', datatype: 'string' },
attributes_number: { context: 'attribute', datatype: 'number' },
attributes_bool: { context: 'attribute', datatype: 'bool' },
};
// Flatten specific fields with context and datatype
Object.entries(fieldMappings).forEach(([fieldKey, { context, datatype }]) => {
const fieldData = log[fieldKey as keyof ILog];
if (fieldData && typeof fieldData === 'object') {
Object.entries(defaultTo(fieldData, {})).forEach(([key, value]) => {
const flatKey = `${context}.${key}:${datatype}`;
flattenLogObject[flatKey] = String(value);
});
}
});
// Add top-level fields
const topLevelFieldsToContextMapping: Record<string, string> = {
id: LOG_FIELD_ID_KEY,
timestamp: LOG_FIELD_TIMESTAMP_KEY,
body: LOG_FIELD_BODY_KEY,
span_id: LOG_FIELD_SPAN_ID_KEY,
trace_id: LOG_FIELD_TRACE_ID_KEY,
trace_flags: LOG_FIELD_TRACE_FLAGS_KEY,
severity_text: LOG_FIELD_SEVERITY_TEXT_KEY,
severity_number: LOG_FIELD_SEVERITY_NUMBER_KEY,
scope_name: LOG_FIELD_SCOPE_NAME_KEY,
scope_version: LOG_FIELD_SCOPE_VERSION_KEY,
};
Object.entries(topLevelFieldsToContextMapping).forEach(([field, key]) => {
const value = log[field as keyof ILog];
if (value !== undefined && value !== null) {
flattenLogObject[key] = String(value);
}
});
return flattenLogObject;
}

View File

@@ -1,5 +1,7 @@
export interface IField {
name: string;
displayName: string;
key: string;
type: string;
dataType: string;
}

View File

@@ -128,7 +128,11 @@ export interface VariableItem {
export interface TelemetryFieldKey {
name: string;
displayName?: string;
// display name can change dynamically depending on if there's a conflicting field with same name and in only meant for UI display
key?: string;
// key is a unique identifier generated for each field, used for comparisons and selections
// key = fieldContext.name:fieldDataType
description?: string;
unit?: string;
signal?: SignalType;