mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-27 19:40:41 +00:00
Compare commits
6 Commits
main
...
feat/trace
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
674f878008 | ||
|
|
7d258c17ac | ||
|
|
8cd159fc55 | ||
|
|
50b323bcb2 | ||
|
|
56806ede49 | ||
|
|
3ed283a473 |
@@ -3,7 +3,6 @@ import {
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
|
||||
@@ -591,7 +590,6 @@ export const listViewInitialTraceQuery: Query = {
|
||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
||||
offset: 0,
|
||||
pageSize: 10,
|
||||
selectColumns: defaultTraceSelectedColumns,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryBuilderFormTraceOperatorValues,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryRangeRequestV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import ListView from './index';
|
||||
|
||||
@@ -24,7 +30,7 @@ jest.mock('react-redux', () => {
|
||||
});
|
||||
|
||||
// List columns come from the options menu (server-synced preferences). Pin them
|
||||
// so the query fires and the expected columns render, independent of that API.
|
||||
// so the expected columns render, independent of that API.
|
||||
jest.mock('container/OptionsMenu/useOptionsMenu', () => ({
|
||||
__esModule: true,
|
||||
default: (): unknown => ({
|
||||
@@ -35,6 +41,8 @@ jest.mock('container/OptionsMenu/useOptionsMenu', () => ({
|
||||
{ name: 'duration_nano', fieldContext: 'span' },
|
||||
{ name: 'http_method', fieldContext: 'span' },
|
||||
{ name: 'response_status_code', fieldContext: 'span' },
|
||||
{ name: 'http.route', fieldContext: 'attribute' },
|
||||
{ name: 'cache.hit', fieldContext: 'attribute' },
|
||||
],
|
||||
},
|
||||
config: { addColumn: { onRemove: jest.fn() } },
|
||||
@@ -48,19 +56,19 @@ const listRows = [
|
||||
{
|
||||
timestamp: '2024-07-19T08:39:58.735245Z',
|
||||
data: {
|
||||
'service.name': 'frontend',
|
||||
name: 'HTTP GET',
|
||||
duration_nano: 55306000,
|
||||
http_method: 'GET',
|
||||
response_status_code: '200',
|
||||
span_id: '772c4d29dd9076ac',
|
||||
trace_id: '0000000000000000344ded1387b08a7e',
|
||||
attributes: { 'http.route': '/api/cart', 'cache.hit': false },
|
||||
resource: { 'service.name': 'frontend' },
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamp: '2024-07-19T08:39:59.949129915Z',
|
||||
data: {
|
||||
'service.name': 'demo-app',
|
||||
name: 'authenticate_check_db',
|
||||
duration_nano: 790949390,
|
||||
// empty status fields to assert the "-" cell
|
||||
@@ -68,6 +76,8 @@ const listRows = [
|
||||
response_status_code: '',
|
||||
span_id: '5704353737b6778e',
|
||||
trace_id: 'a364a8e15af3e9a8c866e0528db8b637',
|
||||
attributes: {},
|
||||
resource: { 'service.name': 'demo-app' },
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -84,7 +94,9 @@ const mockSuccess = (rows: unknown[] = listRows): void => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderListView = (): ReturnType<typeof render> =>
|
||||
const renderListView = (
|
||||
stagedQuery: Query = initialQueriesMap.traces,
|
||||
): ReturnType<typeof render> =>
|
||||
render(
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 500, itemHeight: 54 }}>
|
||||
<ListView
|
||||
@@ -98,8 +110,8 @@ const renderListView = (): ReturnType<typeof render> =>
|
||||
initialRoute: '/traces-explorer',
|
||||
queryBuilderOverrides: {
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
stagedQuery: initialQueriesMap.traces,
|
||||
currentQuery: initialQueriesMap.traces,
|
||||
stagedQuery,
|
||||
currentQuery: stagedQuery,
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
} as any,
|
||||
},
|
||||
@@ -118,6 +130,10 @@ describe('Traces ListView - Data Loaded', () => {
|
||||
await expect(screen.findByText('frontend')).resolves.toBeInTheDocument();
|
||||
expect(screen.getByText('authenticate_check_db')).toBeInTheDocument();
|
||||
|
||||
// attribute column reads from the attributes map
|
||||
expect(screen.getByText('/api/cart')).toBeInTheDocument();
|
||||
expect(screen.getByText('false')).toBeInTheDocument();
|
||||
|
||||
// duration_nano renders in milliseconds
|
||||
expect(screen.getAllByTestId('duration_nano')[0]).toHaveTextContent(/ms$/);
|
||||
|
||||
@@ -130,4 +146,37 @@ describe('Traces ListView - Data Loaded', () => {
|
||||
// empty status fields render "-"
|
||||
expect(screen.getAllByText('-').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('requests every span field instead of the selected columns', async () => {
|
||||
let requestBody: QueryRangeRequestV5 | undefined;
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
requestBody = await req.json<QueryRangeRequestV5>();
|
||||
return res(ctx.status(200), ctx.json(listResponse(listRows)));
|
||||
}),
|
||||
);
|
||||
const selectColumns = [{ name: 'http.route', fieldContext: 'attribute' }];
|
||||
const { builder } = initialQueriesMap.traces;
|
||||
renderListView({
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...builder,
|
||||
queryData: [{ ...builder.queryData[0], selectColumns }],
|
||||
queryTraceOperator: [
|
||||
{
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
expression: 'A',
|
||||
selectColumns,
|
||||
},
|
||||
],
|
||||
},
|
||||
} as Query);
|
||||
|
||||
await expect(screen.findByText('frontend')).resolves.toBeInTheDocument();
|
||||
const queries = requestBody?.compositeQuery.queries ?? [];
|
||||
expect(queries).toHaveLength(2);
|
||||
queries.forEach(({ spec }) => {
|
||||
expect(spec).not.toHaveProperty('selectFields');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,23 +99,20 @@ function ListView({
|
||||
const paginationConfig =
|
||||
paginationQueryData ?? getDefaultPaginationConfig(PER_PAGE_OPTIONS);
|
||||
|
||||
const requestQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces, orderBy),
|
||||
[stagedQuery, orderBy],
|
||||
);
|
||||
|
||||
// Stable sorted-name signature for the queryKey.
|
||||
// - Drag updates selectColumns; raw queryKey would churn on reorder.
|
||||
// - Trace API fetches only listed columns → add/remove must refetch.
|
||||
// - Sorted-name signature: stable on reorder, changes on add/remove.
|
||||
const selectColumnsSignature = useMemo(
|
||||
() =>
|
||||
(options?.selectColumns ?? [])
|
||||
.map((c) => c.name)
|
||||
.sort()
|
||||
.join(','),
|
||||
[options?.selectColumns],
|
||||
);
|
||||
// Empty selectFields makes the API return every span field.
|
||||
const requestQuery = useMemo(() => {
|
||||
const query = getListViewQuery(
|
||||
stagedQuery || initialQueriesMap.traces,
|
||||
orderBy,
|
||||
);
|
||||
query.builder.queryData.forEach((queryData) => {
|
||||
queryData.selectColumns = [];
|
||||
});
|
||||
query.builder.queryTraceOperator?.forEach((queryTraceOperator) => {
|
||||
queryTraceOperator.selectColumns = [];
|
||||
});
|
||||
return query;
|
||||
}, [stagedQuery, orderBy]);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
@@ -126,14 +123,12 @@ function ListView({
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
],
|
||||
[
|
||||
stagedQuery,
|
||||
panelType,
|
||||
globalSelectedTime,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
maxTime,
|
||||
minTime,
|
||||
],
|
||||
@@ -154,7 +149,6 @@ function ListView({
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationConfig,
|
||||
selectColumns: options?.selectColumns,
|
||||
},
|
||||
},
|
||||
// ENTITY_VERSION_V4,
|
||||
@@ -163,10 +157,7 @@ function ListView({
|
||||
queryKey,
|
||||
enabled:
|
||||
// don't make api call while the time range state in redux is loading
|
||||
!timeRangeUpdateLoading &&
|
||||
!!stagedQuery &&
|
||||
panelType === PANEL_TYPES.LIST &&
|
||||
!!options?.selectColumns?.length,
|
||||
!timeRangeUpdateLoading && !!stagedQuery && panelType === PANEL_TYPES.LIST,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { getSpanAttribute } from 'pages/TraceDetailsV3/utils';
|
||||
|
||||
import { TIMESTAMP_FIELD_NAMES } from './constants';
|
||||
import FieldCell from './FieldCell';
|
||||
@@ -16,7 +17,10 @@ export function getFieldColumn(
|
||||
return {
|
||||
id: buildCompositeKey(name, fieldContext, fieldDataType),
|
||||
header: name,
|
||||
accessorFn: (row): unknown => row[name],
|
||||
// Span rows nest attribute/resource fields in maps; trace rows are flat.
|
||||
accessorFn: (row): unknown =>
|
||||
row[name] ??
|
||||
getSpanAttribute(row as Parameters<typeof getSpanAttribute>[0], name),
|
||||
enableMove: !isTimestamp,
|
||||
enableRemove: !isTimestamp,
|
||||
canBeHidden: !isTimestamp,
|
||||
|
||||
@@ -190,10 +190,12 @@ function SpanDetailsContent({
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
clusterName: getSpanAttribute(selectedSpan, 'k8s.cluster.name') || '',
|
||||
podName: getSpanAttribute(selectedSpan, 'k8s.pod.name') || '',
|
||||
nodeName: getSpanAttribute(selectedSpan, 'k8s.node.name') || '',
|
||||
hostName: getSpanAttribute(selectedSpan, 'host.name') || '',
|
||||
clusterName: String(
|
||||
getSpanAttribute(selectedSpan, 'k8s.cluster.name') ?? '',
|
||||
),
|
||||
podName: String(getSpanAttribute(selectedSpan, 'k8s.pod.name') ?? ''),
|
||||
nodeName: String(getSpanAttribute(selectedSpan, 'k8s.node.name') ?? ''),
|
||||
hostName: String(getSpanAttribute(selectedSpan, 'host.name') ?? ''),
|
||||
spanTimestamp: dayjs(selectedSpan.timestamp).format(),
|
||||
};
|
||||
}, [selectedSpan]);
|
||||
|
||||
@@ -88,5 +88,17 @@ describe('Presentation / Styling Utils', () => {
|
||||
);
|
||||
expect(value).toBe('host-1');
|
||||
});
|
||||
|
||||
it('groups 0 and false under their own value, not "unknown"', () => {
|
||||
expect(
|
||||
getFlamegraphSpanGroupValue({ attributes: { 'host.name': 0 } }, HOST_FIELD),
|
||||
).toBe('0');
|
||||
expect(
|
||||
getFlamegraphSpanGroupValue(
|
||||
{ attributes: { 'host.name': false } },
|
||||
HOST_FIELD,
|
||||
),
|
||||
).toBe('false');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ export function getFlamegraphRowMetrics(
|
||||
export function getFlamegraphServiceName(
|
||||
span: Partial<Pick<FlamegraphSpan, 'resource' | 'attributes'>>,
|
||||
): string {
|
||||
return getSpanAttribute(span, 'service.name') || '';
|
||||
return String(getSpanAttribute(span, 'service.name') ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +92,7 @@ export function getFlamegraphSpanGroupValue(
|
||||
span: Partial<Pick<FlamegraphSpan, 'resource' | 'attributes'>>,
|
||||
field: TelemetryFieldKey,
|
||||
): string {
|
||||
return getSpanAttribute(span, field.name) || 'unknown';
|
||||
return String(getSpanAttribute(span, field.name) ?? 'unknown');
|
||||
}
|
||||
|
||||
interface GetSpanColorArgs {
|
||||
|
||||
@@ -27,6 +27,14 @@ describe('getAvailableColorByFieldNames', () => {
|
||||
expect(getAvailableColorByFieldNames(spans)).toStrictEqual(['host.name']);
|
||||
});
|
||||
|
||||
it('offers a field whose only values are 0 or false', () => {
|
||||
const spans = [
|
||||
span({ attributes: { 'host.name': 0 } }),
|
||||
span({ attributes: { 'host.name': false } }),
|
||||
];
|
||||
expect(getAvailableColorByFieldNames(spans)).toStrictEqual(['host.name']);
|
||||
});
|
||||
|
||||
it('does not offer fields no span carries', () => {
|
||||
const spans = [span({ resource: { 'service.name': 'api' } })];
|
||||
expect(getAvailableColorByFieldNames(spans)).toStrictEqual(['service.name']);
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
RESERVED_ERROR,
|
||||
} from './utils/generateColorPair';
|
||||
|
||||
/** Like `||`, but keeps `0`. */
|
||||
/** Like `||`, but keeps `0` and `false`. */
|
||||
const valueOrUndefined = <T>(value: T): T | undefined =>
|
||||
value || value === 0 ? value : undefined;
|
||||
value || value === 0 || value === false ? value : undefined;
|
||||
|
||||
/**
|
||||
* Look up an attribute from both `resource` and `attributes` on a span.
|
||||
* Resources are checked first (service.name, k8s.* etc. live there).
|
||||
* Falsy values count as missing, except `0`.
|
||||
* Falsy values count as missing, except `0` and `false`.
|
||||
*
|
||||
* Accepts both `SpanV3` (waterfall) and `FlamegraphSpan` (flamegraph) by typing
|
||||
* structurally — the only fields touched are `resource` and `attributes`.
|
||||
@@ -30,7 +30,7 @@ export function getSpanAttribute(
|
||||
tagMap?: Record<string, any>;
|
||||
},
|
||||
key: string,
|
||||
): string | undefined {
|
||||
): string | number | boolean | undefined {
|
||||
return (
|
||||
valueOrUndefined(span.resource?.[key]) ??
|
||||
valueOrUndefined(span.attributes?.[key]) ??
|
||||
@@ -117,7 +117,7 @@ export function getSpanGroupValue(
|
||||
span: SpanV3,
|
||||
colorByFieldName: string,
|
||||
): string {
|
||||
return getSpanAttribute(span, colorByFieldName) || 'unknown';
|
||||
return String(getSpanAttribute(span, colorByFieldName) ?? 'unknown');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,6 +143,6 @@ export function resolveSpanColor(
|
||||
*/
|
||||
export function getAvailableColorByFieldNames(spans: SpanV3[]): string[] {
|
||||
return COLOR_BY_OPTIONS.filter((opt) =>
|
||||
spans.some((s) => getSpanAttribute(s, opt.field.name)),
|
||||
spans.some((s) => getSpanAttribute(s, opt.field.name) !== undefined),
|
||||
).map((opt) => opt.field.name);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ const timestampAt = (index: number, start: number, end: number): string => {
|
||||
return new Date(end - index * step).toISOString();
|
||||
};
|
||||
|
||||
export const traceSpanRows = ({
|
||||
const flatSpanRows = ({
|
||||
count,
|
||||
offset,
|
||||
limit,
|
||||
@@ -167,9 +167,22 @@ export const traceSpanRows = ({
|
||||
};
|
||||
});
|
||||
|
||||
/** The List view returns every span field, with resource and attribute fields nested in maps. */
|
||||
export const traceSpanRows = (options: SpanRowsOptions): RawRow[] =>
|
||||
flatSpanRows(options).map(
|
||||
({ timestamp, data: { 'service.name': serviceName, ...data } }) => ({
|
||||
timestamp,
|
||||
data: {
|
||||
...data,
|
||||
attributes: {},
|
||||
resource: { 'service.name': serviceName },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/** The Trace view lists root spans, with the span count of the whole trace. */
|
||||
export const traceRootSpanRows = (options: SpanRowsOptions): RawRow[] =>
|
||||
traceSpanRows(options).map((row, index) => ({
|
||||
flatSpanRows(options).map((row, index) => ({
|
||||
...row,
|
||||
data: {
|
||||
...row.data,
|
||||
|
||||
Reference in New Issue
Block a user