Skip to content

Custom Traces- Sorting/Toast #2397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,17 @@

it('Renders data grid, flyout and filters', () => {
cy.get('.panel-title-count').contains('(11)').should('exist');
cy.get('.euiButton__text[title="Span list"]').click({ force: true });

Check warning on line 100 in .cypress/integration/trace_analytics_test/trace_analytics_traces.spec.js

View workflow job for this annotation

GitHub Actions / Lint

Do not use force on click and type calls
cy.contains('2 columns hidden').should('exist');

cy.get('.euiLink').contains(SPAN_ID).trigger('mouseover', { force: true });

Check warning on line 103 in .cypress/integration/trace_analytics_test/trace_analytics_traces.spec.js

View workflow job for this annotation

GitHub Actions / Lint

Do not use force on click and type calls
cy.get('button[data-datagrid-interactable="true"]').eq(0).click({ force: true });

Check warning on line 104 in .cypress/integration/trace_analytics_test/trace_analytics_traces.spec.js

View workflow job for this annotation

GitHub Actions / Lint

Do not use force on click and type calls
cy.get('button[data-datagrid-interactable="true"]').eq(0).click({ force: true }); // first click doesn't go through eui data grid

Check warning on line 105 in .cypress/integration/trace_analytics_test/trace_analytics_traces.spec.js

View workflow job for this annotation

GitHub Actions / Lint

Do not use force on click and type calls

cy.contains('Span detail').should('exist');
cy.contains('Span attributes').should('exist');
cy.get('.euiTextColor').contains('Span ID').trigger('mouseover');
cy.get('.euiButtonIcon[aria-label="span-flyout-filter-icon"').click({ force: true });

Check warning on line 110 in .cypress/integration/trace_analytics_test/trace_analytics_traces.spec.js

View workflow job for this annotation

GitHub Actions / Lint

Do not use force on click and type calls

cy.get('.euiBadge__text').contains('spanId: ').should('exist');
cy.contains('Spans (1)').should('exist');
Expand Down Expand Up @@ -325,7 +325,7 @@

cy.get('a.euiLink.euiLink--primary').first().click();
cy.get('[data-test-subj="globalLoadingIndicator"]').should('not.exist');
cy.get('.overview-content').should('contain.text', '4fa04f117be100f476b175e41096e736');
cy.get('.overview-content').should('contain.text', 'd5bc99166e521eec173bcb7f9b0d3c43');
});

it('Renders all spans column attributes as hidden, shows column when added', () => {
Expand Down Expand Up @@ -366,6 +366,6 @@

cy.get('a.euiLink.euiLink--primary').first().click();
cy.get('[data-test-subj="globalLoadingIndicator"]').should('not.exist');
cy.get('.overview-content').should('contain.text', '02feb3a4f611abd81f2a53244d1278ae');
cy.get('.overview-content').should('contain.text', 'be0a3dceda2ecf601fd2e476fef3ee07');
});
});
128 changes: 89 additions & 39 deletions public/components/trace_analytics/components/traces/traces_content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);

const getDefaultSort = () => {
return tracesTableMode === 'traces'
? { field: 'last_updated', direction: 'desc' as const }
: { field: 'endTime', direction: 'desc' as const };
};

const onSort = (sortColumns: Array<{ id: string; direction: 'desc' | 'asc' }>) => {
if (!sortColumns || sortColumns.length === 0) {
setSortingColumns([]);
Expand All @@ -95,43 +101,40 @@

setSortingColumns(sortColumns);

const localOnlyFields = ['trace_group', 'percentile_in_trace_group', 'trace_id'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add some comments to explain what local only means, for future reference

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment
// The columns that can not be used in a query, only rendered on page


if (tracesTableMode === 'traces') {
const sortedItems = [...tableItems].sort((a, b) => {
let valueA = a[sortField];
let valueB = b[sortField];

if (sortField === 'last_updated') {
const dateA = new Date(valueA);
const dateB = new Date(valueB);

const isValidA = !isNaN(dateA.getTime());
const isValidB = !isNaN(dateB.getTime());

// Treat invalid dates as the lowest value
valueA = isValidA ? dateA.getTime() : -Infinity;
valueB = isValidB ? dateB.getTime() : -Infinity;
} else if (sortField === 'trace_group') {
valueA = typeof valueA === 'string' ? valueA.toLowerCase() : '';
valueB = typeof valueB === 'string' ? valueB.toLowerCase() : '';
}

if (typeof valueA === 'number' && typeof valueB === 'number') {
return sortDirection === 'asc' ? valueA - valueB : valueB - valueA;
}

if (typeof valueA === 'string' && typeof valueB === 'string') {
return sortDirection === 'asc'
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA);
}

return 0;
});
// Client-side sort if field is local-only
if (localOnlyFields.includes(sortField)) {
const sorted = [...tableItems].sort((a, b) => {
let valueA = a[sortField];
let valueB = b[sortField];

if (typeof valueA === 'string' && typeof valueB === 'string') {
valueA = valueA.toLowerCase();
valueB = valueB.toLowerCase();
return sortDirection === 'asc'
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA);
}

if (typeof valueA === 'number' && typeof valueB === 'number') {
return sortDirection === 'asc' ? valueA - valueB : valueB - valueA;
}

return 0;
});

setTableItems(sorted);
return;
}

setTableItems(sortedItems);
// Server-side sort for supported fields
const sort = { field: sortField, direction: sortDirection };
refreshTracesTableData(sort, 0, pageSize);
} else {
const { DSL, isUnderOneHour } = generateDSLs();
refreshTableDataOnly(pageIndex, pageSize, DSL, isUnderOneHour, {
refreshSpanTableData(pageIndex, pageSize, DSL, isUnderOneHour, {
field: sortField,
direction: sortDirection,
});
Expand Down Expand Up @@ -168,7 +171,7 @@
const currentSort = sortingColumns[0]
? { field: sortingColumns[0].id, direction: sortingColumns[0].direction }
: undefined;
refreshTableDataOnly(newPage, pageSize, DSL, isUnderOneHour, currentSort);
refreshSpanTableData(newPage, pageSize, DSL, isUnderOneHour, currentSort);
}
},
onChangeItemsPerPage: (newSize) => {
Expand All @@ -179,7 +182,7 @@
const currentSort = sortingColumns[0]
? { field: sortingColumns[0].id, direction: sortingColumns[0].direction }
: undefined;
refreshTableDataOnly(0, newSize, DSL, isUnderOneHour, currentSort);
refreshSpanTableData(0, newSize, DSL, isUnderOneHour, currentSort);
}
},
};
Expand Down Expand Up @@ -252,16 +255,17 @@
setFilters(newFilters);
};

const refreshTableDataOnly = async (
const refreshSpanTableData = async (
newPageIndex: number,
newPageSize: number,
DSL: any,

Check warning on line 261 in public/components/trace_analytics/components/traces/traces_content.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
isUnderOneHour: boolean,
sortParams?: { field: string; direction: 'desc' | 'asc' }
) => {
setPageIndex(newPageIndex);
setPageSize(newPageSize);
setIsTraceTableLoading(true);
const sort = sortParams ?? getDefaultSort();

handleCustomIndicesTracesRequest(
http,
Expand All @@ -273,12 +277,57 @@
newPageSize,
setTotalHits,
props.dataSourceMDSId[0]?.id,
sortParams,
sort,
tracesTableMode,
isUnderOneHour
).finally(() => setIsTraceTableLoading(false));
};

const refreshTracesTableData = async (
sortParams?: { field: string; direction: 'desc' | 'asc' },
newPageIndex: number = pageIndex,
newPageSize: number = pageSize
) => {
setPageIndex(newPageIndex);
setPageSize(newPageSize);
setIsTraceTableLoading(true);
const sort = sortParams ?? getDefaultSort();

const DSL = filtersToDsl(
mode,
filters,
query,
processTimeStamp(startTime, mode),
processTimeStamp(endTime, mode),
page,
appConfigs
);

const timeFilterDSL = filtersToDsl(
mode,
[],
'',
processTimeStamp(startTime, mode),
processTimeStamp(endTime, mode),
page
);

const isUnderOneHour = datemath.parse(endTime)?.diff(datemath.parse(startTime), 'hours')! < 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

util functions like this can be extracted, i see the same logic is also used in other places

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added helper function isUnderOneHourRange

export const isUnderOneHourRange = (startTime: string, endTime: string): boolean => {
  const start = dateMath.parse(startTime);
  const end = dateMath.parse(endTime);

  if (!start || !end) return false;

  return end.diff(start, 'hours')! < 1;
};


await handleTracesRequest(
http,
DSL,
timeFilterDSL,
tableItems,
setTableItems,
mode,
maxTraces,
props.dataSourceMDSId[0].id,
sort,
isUnderOneHour
).finally(() => setIsTraceTableLoading(false));
};

const refresh = async (
sort?: PropertySort,
overrideQuery?: string,
Expand All @@ -304,6 +353,7 @@
page
);
const isUnderOneHour = datemath.parse(endTime)?.diff(datemath.parse(startTime), 'hours')! < 1;
const newSort = sort ?? getDefaultSort();

setIsTraceTableLoading(true);

Expand All @@ -311,7 +361,7 @@
// Remove serviceName filter from service map query
const serviceMapDSL = cloneDeep(DSL);
serviceMapDSL.query.bool.must = serviceMapDSL.query.bool.must.filter(
(must: any) => !must?.term?.serviceName

Check warning on line 364 in public/components/trace_analytics/components/traces/traces_content.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
);

const tracesRequest =
Expand All @@ -326,7 +376,7 @@
newPageSize,
setTotalHits,
props.dataSourceMDSId[0]?.id,
sort,
newSort,
tracesTableMode,
isUnderOneHour
)
Expand All @@ -339,7 +389,7 @@
mode,
maxTraces,
props.dataSourceMDSId[0].id,
sort,
newSort,
isUnderOneHour
);
tracesRequest.finally(() => setIsTraceTableLoading(false));
Expand Down
122 changes: 56 additions & 66 deletions public/components/trace_analytics/requests/traces_request_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@

export const handleCustomIndicesTracesRequest = async (
http: HttpSetup,
DSL: any,

Check warning on line 38 in public/components/trace_analytics/requests/traces_request_handler.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
items: any,

Check warning on line 39 in public/components/trace_analytics/requests/traces_request_handler.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
setItems: (items: any) => void,

Check warning on line 40 in public/components/trace_analytics/requests/traces_request_handler.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
mode: TraceAnalyticsMode,
pageIndex: number = 0,
pageSize: number = 10,
Expand All @@ -47,65 +47,60 @@
queryMode?: TraceQueryMode,
isUnderOneHour?: boolean
) => {
const responsePromise = handleDslRequest(
http,
DSL,
getCustomIndicesTracesQuery(
try {
const response = await handleDslRequest(
http,
DSL,
getCustomIndicesTracesQuery(
mode,
undefined,
pageIndex,
pageSize,
sort,
queryMode,
isUnderOneHour
),
mode,
undefined,
pageIndex,
pageSize,
sort,
queryMode,
isUnderOneHour
),
mode,
dataSourceMDSId
);

return Promise.allSettled([responsePromise])
.then(([responseResult]) => {
if (responseResult.status === 'rejected') return Promise.reject(responseResult.reason);
dataSourceMDSId
);

const responseData = responseResult.value;
const hits = response?.hits?.hits || [];
const totalHits = response?.hits?.total?.value ?? 0;

const totalHits = responseData.hits?.total?.value ?? 0;
setTotalHits(totalHits);
setTotalHits(totalHits);

if (mode === 'data_prepper' || mode === 'custom_data_prepper') {
const keys = new Set();
const response = responseResult.value.hits.hits.map((val) => {
const source = omitBy(val._source, isArray || isObject);
Object.keys(source).forEach((key) => keys.add(key));
return { ...source };
});
if (!hits.length) {
setItems([]);
return;
}

return [keys, response];
} else {
return [
[undefined],
responseResult.value.aggregations.traces.buckets.map((bucket: any) => {
return {
trace_id: bucket.key,
latency: bucket.latency.value,
last_updated: moment(bucket.last_updated.value).format(TRACE_ANALYTICS_DATE_FORMAT),
error_count: bucket.error_count.doc_count,
actions: '#',
};
}),
];
}
})
.then((newItems) => {
setItems(newItems[1]);
})
.catch((error) => {
console.error('Error in handleCustomIndicesTracesRequest:', error);
coreRefs.core?.notifications.toasts.addError(error, {
title: 'Failed to retrieve custom indices traces',
toastLifeTimeMs: 10000,
if (mode === 'data_prepper' || mode === 'custom_data_prepper') {
const keys = new Set();
const results = hits.map((val) => {
const source = omitBy(val._source, isArray || isObject);
Object.keys(source).forEach((key) => keys.add(key));
return { ...source };
});

setItems(results);
} else {
const buckets = response?.aggregations?.traces?.buckets || [];
const results = buckets.map((bucket: any) => ({
trace_id: bucket.key,
latency: bucket.latency.value,
last_updated: moment(bucket.last_updated.value).format(TRACE_ANALYTICS_DATE_FORMAT),
error_count: bucket.error_count.doc_count,
actions: '#',
}));
setItems(results);
}
} catch (error) {
console.error('Error in handleCustomIndicesTracesRequest:', error);
coreRefs.core?.notifications.toasts.addError(error, {
title: 'Failed to retrieve custom indices traces',
toastLifeTimeMs: 10000,
});
}
};

export const handleTracesRequest = async (
Expand Down Expand Up @@ -159,31 +154,28 @@
});
return map;
})
: Promise.reject('Only data_prepper mode supports percentile');
: Promise.resolve({});

return Promise.allSettled([responsePromise, percentileRangesPromise])
.then(([responseResult, percentileRangesResult]) => {
if (responseResult.status === 'rejected') return Promise.reject(responseResult.reason);
if (responseResult.status === 'rejected') {
setItems([]);
return;
}

const percentileRanges =
percentileRangesResult.status === 'fulfilled' ? percentileRangesResult.value : {};
const response = responseResult.value;

if ((response.statusCode && response.statusCode >= 400) || response.error) {
return Promise.reject(response);
}

if (
!response ||
!response.aggregations ||
!response.aggregations.traces ||
!response.aggregations.traces.buckets ||
!response?.aggregations?.traces?.buckets ||
response.aggregations.traces.buckets.length === 0
) {
setItems([]);
return [];
return;
}

return response.aggregations.traces.buckets.map((bucket: any) => {
const newItems = response.aggregations.traces.buckets.map((bucket: any) => {
if (mode === 'data_prepper' || mode === 'custom_data_prepper') {
return {
trace_id: bucket.key,
Expand All @@ -206,8 +198,6 @@
actions: '#',
};
});
})
.then((newItems) => {
setItems(newItems);
})
.catch((error) => {
Expand Down
Loading