mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-14 07:50:28 +01:00
Compare commits
20 Commits
feat/filte
...
feat/test-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35a029c65e | ||
|
|
ba7f60d6c6 | ||
|
|
7279c5f770 | ||
|
|
864effa63b | ||
|
|
16cbaeddc2 | ||
|
|
d993c4089e | ||
|
|
cdb59bed07 | ||
|
|
16a2c7a1af | ||
|
|
6ec9a2ec41 | ||
|
|
9c056f809a | ||
|
|
c1d4273416 | ||
|
|
618fe891d5 | ||
|
|
549c7e7034 | ||
|
|
dd65f83c3d | ||
|
|
8463a131fc | ||
|
|
2d42518440 | ||
|
|
43d75a3853 | ||
|
|
c5bb34e385 | ||
|
|
6fd129991d | ||
|
|
9c5cca426a |
@@ -2787,6 +2787,147 @@ components:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
TracedetailtypesEvent:
|
||||
properties:
|
||||
attributeMap:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
isError:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
timeUnixNano:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type: object
|
||||
TracedetailtypesWaterfallRequest:
|
||||
properties:
|
||||
limit:
|
||||
minimum: 0
|
||||
type: integer
|
||||
selectedSpanId:
|
||||
type: string
|
||||
uncollapsedSpans:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
TracedetailtypesWaterfallResponse:
|
||||
properties:
|
||||
endTimestampMillis:
|
||||
minimum: 0
|
||||
type: integer
|
||||
hasMissingSpans:
|
||||
type: boolean
|
||||
hasMore:
|
||||
type: boolean
|
||||
rootServiceEntryPoint:
|
||||
type: string
|
||||
rootServiceName:
|
||||
type: string
|
||||
serviceNameToTotalDurationMap:
|
||||
additionalProperties:
|
||||
minimum: 0
|
||||
type: integer
|
||||
nullable: true
|
||||
type: object
|
||||
spans:
|
||||
items:
|
||||
$ref: '#/components/schemas/TracedetailtypesWaterfallSpan'
|
||||
nullable: true
|
||||
type: array
|
||||
startTimestampMillis:
|
||||
minimum: 0
|
||||
type: integer
|
||||
totalErrorSpansCount:
|
||||
minimum: 0
|
||||
type: integer
|
||||
totalSpansCount:
|
||||
minimum: 0
|
||||
type: integer
|
||||
uncollapsedSpans:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
TracedetailtypesWaterfallSpan:
|
||||
properties:
|
||||
attributes:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
db_name:
|
||||
type: string
|
||||
db_operation:
|
||||
type: string
|
||||
duration_nano:
|
||||
minimum: 0
|
||||
type: integer
|
||||
events:
|
||||
items:
|
||||
$ref: '#/components/schemas/TracedetailtypesEvent'
|
||||
nullable: true
|
||||
type: array
|
||||
external_http_method:
|
||||
type: string
|
||||
external_http_url:
|
||||
type: string
|
||||
flags:
|
||||
minimum: 0
|
||||
type: integer
|
||||
has_children:
|
||||
type: boolean
|
||||
has_error:
|
||||
type: boolean
|
||||
http_host:
|
||||
type: string
|
||||
http_method:
|
||||
type: string
|
||||
http_url:
|
||||
type: string
|
||||
is_remote:
|
||||
type: string
|
||||
kind:
|
||||
format: int32
|
||||
type: integer
|
||||
kind_string:
|
||||
type: string
|
||||
level:
|
||||
minimum: 0
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
parent_span_id:
|
||||
type: string
|
||||
resource:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
response_status_code:
|
||||
type: string
|
||||
span_id:
|
||||
type: string
|
||||
status_code:
|
||||
format: int32
|
||||
type: integer
|
||||
status_code_string:
|
||||
type: string
|
||||
status_message:
|
||||
type: string
|
||||
sub_tree_node_count:
|
||||
minimum: 0
|
||||
type: integer
|
||||
timestamp:
|
||||
minimum: 0
|
||||
type: integer
|
||||
trace_id:
|
||||
type: string
|
||||
trace_state:
|
||||
type: string
|
||||
type: object
|
||||
TypesChangePasswordRequest:
|
||||
properties:
|
||||
newPassword:
|
||||
@@ -3309,7 +3450,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CloudintegrationtypesPostableAccount'
|
||||
responses:
|
||||
"200":
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -3322,7 +3463,7 @@ paths:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
description: Created
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
@@ -3683,6 +3824,11 @@ paths:
|
||||
provider
|
||||
operationId: ListServicesMetadata
|
||||
parameters:
|
||||
- in: query
|
||||
name: cloud_integration_id
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: cloud_provider
|
||||
required: true
|
||||
@@ -3735,6 +3881,11 @@ paths:
|
||||
description: This endpoint gets a service for the specified cloud provider
|
||||
operationId: GetService
|
||||
parameters:
|
||||
- in: query
|
||||
name: cloud_integration_id
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: cloud_provider
|
||||
required: true
|
||||
@@ -9504,6 +9655,76 @@ paths:
|
||||
summary: Put profile in Zeus for a deployment.
|
||||
tags:
|
||||
- zeus
|
||||
/api/v3/traces/{traceID}/waterfall:
|
||||
post:
|
||||
deprecated: false
|
||||
description: Returns the waterfall view of spans for a given trace ID with tree
|
||||
structure, metadata, and windowed pagination
|
||||
operationId: GetWaterfall
|
||||
parameters:
|
||||
- in: path
|
||||
name: traceID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TracedetailtypesWaterfallRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TracedetailtypesWaterfallResponse'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get waterfall view for a trace
|
||||
tags:
|
||||
- tracedetail
|
||||
/api/v5/query_range:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
CloudintegrationtypesPostableAgentCheckInDTO,
|
||||
CloudintegrationtypesUpdatableAccountDTO,
|
||||
CloudintegrationtypesUpdatableServiceDTO,
|
||||
CreateAccount200,
|
||||
CreateAccount201,
|
||||
CreateAccountPathParameters,
|
||||
DisconnectAccountPathParameters,
|
||||
GetAccount200,
|
||||
@@ -36,10 +36,12 @@ import type {
|
||||
GetConnectionCredentials200,
|
||||
GetConnectionCredentialsPathParameters,
|
||||
GetService200,
|
||||
GetServiceParams,
|
||||
GetServicePathParameters,
|
||||
ListAccounts200,
|
||||
ListAccountsPathParameters,
|
||||
ListServicesMetadata200,
|
||||
ListServicesMetadataParams,
|
||||
ListServicesMetadataPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
UpdateAccountPathParameters,
|
||||
@@ -260,7 +262,7 @@ export const createAccount = (
|
||||
cloudintegrationtypesPostableAccountDTO: BodyType<CloudintegrationtypesPostableAccountDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateAccount200>({
|
||||
return GeneratedAPIInstance<CreateAccount201>({
|
||||
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -940,19 +942,25 @@ export const invalidateGetConnectionCredentials = async (
|
||||
*/
|
||||
export const listServicesMetadata = (
|
||||
{ cloudProvider }: ListServicesMetadataPathParameters,
|
||||
params?: ListServicesMetadataParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListServicesMetadata200>({
|
||||
url: `/api/v1/cloud_integrations/${cloudProvider}/services`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListServicesMetadataQueryKey = ({
|
||||
cloudProvider,
|
||||
}: ListServicesMetadataPathParameters) => {
|
||||
return [`/api/v1/cloud_integrations/${cloudProvider}/services`] as const;
|
||||
export const getListServicesMetadataQueryKey = (
|
||||
{ cloudProvider }: ListServicesMetadataPathParameters,
|
||||
params?: ListServicesMetadataParams,
|
||||
) => {
|
||||
return [
|
||||
`/api/v1/cloud_integrations/${cloudProvider}/services`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getListServicesMetadataQueryOptions = <
|
||||
@@ -960,6 +968,7 @@ export const getListServicesMetadataQueryOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ cloudProvider }: ListServicesMetadataPathParameters,
|
||||
params?: ListServicesMetadataParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listServicesMetadata>>,
|
||||
@@ -971,11 +980,12 @@ export const getListServicesMetadataQueryOptions = <
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getListServicesMetadataQueryKey({ cloudProvider });
|
||||
queryOptions?.queryKey ??
|
||||
getListServicesMetadataQueryKey({ cloudProvider }, params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof listServicesMetadata>>
|
||||
> = ({ signal }) => listServicesMetadata({ cloudProvider }, signal);
|
||||
> = ({ signal }) => listServicesMetadata({ cloudProvider }, params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
@@ -1003,6 +1013,7 @@ export function useListServicesMetadata<
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ cloudProvider }: ListServicesMetadataPathParameters,
|
||||
params?: ListServicesMetadataParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listServicesMetadata>>,
|
||||
@@ -1013,6 +1024,7 @@ export function useListServicesMetadata<
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListServicesMetadataQueryOptions(
|
||||
{ cloudProvider },
|
||||
params,
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -1031,10 +1043,11 @@ export function useListServicesMetadata<
|
||||
export const invalidateListServicesMetadata = async (
|
||||
queryClient: QueryClient,
|
||||
{ cloudProvider }: ListServicesMetadataPathParameters,
|
||||
params?: ListServicesMetadataParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }) },
|
||||
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }, params) },
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -1047,21 +1060,24 @@ export const invalidateListServicesMetadata = async (
|
||||
*/
|
||||
export const getService = (
|
||||
{ cloudProvider, serviceId }: GetServicePathParameters,
|
||||
params?: GetServiceParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetService200>({
|
||||
url: `/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetServiceQueryKey = ({
|
||||
cloudProvider,
|
||||
serviceId,
|
||||
}: GetServicePathParameters) => {
|
||||
export const getGetServiceQueryKey = (
|
||||
{ cloudProvider, serviceId }: GetServicePathParameters,
|
||||
params?: GetServiceParams,
|
||||
) => {
|
||||
return [
|
||||
`/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
@@ -1070,6 +1086,7 @@ export const getGetServiceQueryOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ cloudProvider, serviceId }: GetServicePathParameters,
|
||||
params?: GetServiceParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getService>>,
|
||||
@@ -1081,11 +1098,12 @@ export const getGetServiceQueryOptions = <
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetServiceQueryKey({ cloudProvider, serviceId });
|
||||
queryOptions?.queryKey ??
|
||||
getGetServiceQueryKey({ cloudProvider, serviceId }, params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getService>>> = ({
|
||||
signal,
|
||||
}) => getService({ cloudProvider, serviceId }, signal);
|
||||
}) => getService({ cloudProvider, serviceId }, params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
@@ -1111,6 +1129,7 @@ export function useGetService<
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ cloudProvider, serviceId }: GetServicePathParameters,
|
||||
params?: GetServiceParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getService>>,
|
||||
@@ -1121,6 +1140,7 @@ export function useGetService<
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetServiceQueryOptions(
|
||||
{ cloudProvider, serviceId },
|
||||
params,
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -1139,10 +1159,11 @@ export function useGetService<
|
||||
export const invalidateGetService = async (
|
||||
queryClient: QueryClient,
|
||||
{ cloudProvider, serviceId }: GetServicePathParameters,
|
||||
params?: GetServiceParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }) },
|
||||
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }, params) },
|
||||
options,
|
||||
);
|
||||
|
||||
|
||||
@@ -3196,6 +3196,249 @@ export interface TelemetrytypesTelemetryFieldValuesDTO {
|
||||
stringValues?: string[];
|
||||
}
|
||||
|
||||
export type TracedetailtypesEventDTOAttributeMap = { [key: string]: unknown };
|
||||
|
||||
export interface TracedetailtypesEventDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
attributeMap?: TracedetailtypesEventDTOAttributeMap;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
isError?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
timeUnixNano?: number;
|
||||
}
|
||||
|
||||
export interface TracedetailtypesWaterfallRequestDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
selectedSpanId?: string;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
uncollapsedSpans?: string[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TracedetailtypesWaterfallResponseDTOServiceNameToTotalDurationMap = {
|
||||
[key: string]: number;
|
||||
} | null;
|
||||
|
||||
export interface TracedetailtypesWaterfallResponseDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
endTimestampMillis?: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
hasMissingSpans?: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
hasMore?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
rootServiceEntryPoint?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
rootServiceName?: string;
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
serviceNameToTotalDurationMap?: TracedetailtypesWaterfallResponseDTOServiceNameToTotalDurationMap;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
spans?: TracedetailtypesWaterfallSpanDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
startTimestampMillis?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
totalErrorSpansCount?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
totalSpansCount?: number;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
uncollapsedSpans?: string[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TracedetailtypesWaterfallSpanDTOAttributes = {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TracedetailtypesWaterfallSpanDTOResource = {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
|
||||
export interface TracedetailtypesWaterfallSpanDTO {
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
attributes?: TracedetailtypesWaterfallSpanDTOAttributes;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
db_name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
db_operation?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
duration_nano?: number;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
events?: TracedetailtypesEventDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
external_http_method?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
external_http_url?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
flags?: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
has_children?: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
has_error?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_host?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_method?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_url?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
is_remote?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int32
|
||||
*/
|
||||
kind?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
kind_string?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
level?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
parent_span_id?: string;
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
resource?: TracedetailtypesWaterfallSpanDTOResource;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
response_status_code?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
span_id?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int32
|
||||
*/
|
||||
status_code?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status_code_string?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status_message?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
sub_tree_node_count?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
timestamp?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
trace_id?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
trace_state?: string;
|
||||
}
|
||||
|
||||
export interface TypesChangePasswordRequestDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -3589,7 +3832,7 @@ export type ListAccounts200 = {
|
||||
export type CreateAccountPathParameters = {
|
||||
cloudProvider: string;
|
||||
};
|
||||
export type CreateAccount200 = {
|
||||
export type CreateAccount201 = {
|
||||
data: CloudintegrationtypesGettableAccountWithConnectionArtifactDTO;
|
||||
/**
|
||||
* @type string
|
||||
@@ -3647,6 +3890,14 @@ export type GetConnectionCredentials200 = {
|
||||
export type ListServicesMetadataPathParameters = {
|
||||
cloudProvider: string;
|
||||
};
|
||||
export type ListServicesMetadataParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
cloud_integration_id?: string;
|
||||
};
|
||||
|
||||
export type ListServicesMetadata200 = {
|
||||
data: CloudintegrationtypesGettableServicesMetadataDTO;
|
||||
/**
|
||||
@@ -3659,6 +3910,14 @@ export type GetServicePathParameters = {
|
||||
cloudProvider: string;
|
||||
serviceId: string;
|
||||
};
|
||||
export type GetServiceParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
cloud_integration_id?: string;
|
||||
};
|
||||
|
||||
export type GetService200 = {
|
||||
data: CloudintegrationtypesServiceDTO;
|
||||
/**
|
||||
@@ -4857,6 +5116,17 @@ export type GetHosts200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetWaterfallPathParameters = {
|
||||
traceID: string;
|
||||
};
|
||||
export type GetWaterfall200 = {
|
||||
data: TracedetailtypesWaterfallResponseDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type QueryRangeV5200 = {
|
||||
data: Querybuildertypesv5QueryRangeResponseDTO;
|
||||
/**
|
||||
|
||||
121
frontend/src/api/generated/services/tracedetail/index.ts
Normal file
121
frontend/src/api/generated/services/tracedetail/index.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import type {
|
||||
MutationFunction,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
} from 'react-query';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type {
|
||||
GetWaterfall200,
|
||||
GetWaterfallPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
TracedetailtypesWaterfallRequestDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
/**
|
||||
* Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination
|
||||
* @summary Get waterfall view for a trace
|
||||
*/
|
||||
export const getWaterfall = (
|
||||
{ traceID }: GetWaterfallPathParameters,
|
||||
tracedetailtypesWaterfallRequestDTO: BodyType<TracedetailtypesWaterfallRequestDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetWaterfall200>({
|
||||
url: `/api/v3/traces/${traceID}/waterfall`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: tracedetailtypesWaterfallRequestDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetWaterfallMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getWaterfall>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data: BodyType<TracedetailtypesWaterfallRequestDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getWaterfall>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data: BodyType<TracedetailtypesWaterfallRequestDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['getWaterfall'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof getWaterfall>>,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data: BodyType<TracedetailtypesWaterfallRequestDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return getWaterfall(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type GetWaterfallMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getWaterfall>>
|
||||
>;
|
||||
export type GetWaterfallMutationBody = BodyType<TracedetailtypesWaterfallRequestDTO>;
|
||||
export type GetWaterfallMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get waterfall view for a trace
|
||||
*/
|
||||
export const useGetWaterfall = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getWaterfall>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data: BodyType<TracedetailtypesWaterfallRequestDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof getWaterfall>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data: BodyType<TracedetailtypesWaterfallRequestDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getGetWaterfallMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
@@ -1,5 +1,68 @@
|
||||
import { Tabs } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { Compass, Cone, TowerControl } from 'lucide-react';
|
||||
|
||||
import TraceDetailsV3 from '../TraceDetailsV3';
|
||||
|
||||
export default function TraceDetailsPage(): JSX.Element {
|
||||
return <TraceDetailsV3 />;
|
||||
import './TraceDetailV2.styles.scss';
|
||||
|
||||
function NewTraceDetail(props: any): JSX.Element {
|
||||
const { items } = props;
|
||||
return (
|
||||
<div className="traces-module-container">
|
||||
<Tabs
|
||||
items={items}
|
||||
animated
|
||||
className="trace-module"
|
||||
onTabClick={(activeKey): void => {
|
||||
if (activeKey === 'saved-views') {
|
||||
history.push(ROUTES.TRACES_SAVE_VIEWS);
|
||||
}
|
||||
if (activeKey === 'trace-details') {
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
}
|
||||
if (activeKey === 'funnels') {
|
||||
logEvent('Trace Funnels: visited from trace details page', {});
|
||||
history.push(ROUTES.TRACES_FUNNELS);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TraceDetailsPage(): JSX.Element {
|
||||
const items = [
|
||||
{
|
||||
label: (
|
||||
<div className="tab-item">
|
||||
<Compass size={16} /> Explorer
|
||||
</div>
|
||||
),
|
||||
key: 'trace-details',
|
||||
children: <TraceDetailsV3 />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="tab-item">
|
||||
<Cone className="funnel-icon" size={16} /> Funnels
|
||||
</div>
|
||||
),
|
||||
key: 'funnels',
|
||||
children: <div />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="tab-item">
|
||||
<TowerControl size={16} /> Views
|
||||
</div>
|
||||
),
|
||||
key: 'saved-views',
|
||||
children: <div />,
|
||||
},
|
||||
];
|
||||
|
||||
return <NewTraceDetail items={items} />;
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
.trace-details-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
|
||||
&__search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__scope {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
.ant-typography {
|
||||
font-size: 12px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
padding: 2px 4px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__no-results {
|
||||
font-size: 12px;
|
||||
color: var(--l2-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Spin, Tooltip, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
DataSource,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
|
||||
import './TraceDetailsFilter.styles.scss';
|
||||
|
||||
interface TraceDetailsFilterProps {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
traceId: string;
|
||||
onFilteredSpansChange: (spanIds: string[], isFilterActive: boolean) => void;
|
||||
}
|
||||
|
||||
const TRACE_ID_FILTER_ITEM = {
|
||||
id: 'trace-details-trace-id',
|
||||
key: {
|
||||
key: 'trace_id',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
id: 'trace_id--string----true',
|
||||
},
|
||||
op: '=',
|
||||
value: '',
|
||||
};
|
||||
|
||||
function buildInitialQuery(): IBuilderQuery {
|
||||
return {
|
||||
...initialQueriesMap.traces.builder.queryData[0],
|
||||
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||
orderBy: [{ columnName: 'timestamp', order: 'asc' }],
|
||||
dataSource: DataSource.TRACES,
|
||||
filters: { items: [], op: 'AND' },
|
||||
filter: { expression: '' },
|
||||
};
|
||||
}
|
||||
|
||||
function prepareApiQuery(builderQuery: IBuilderQuery, traceId: string): Query {
|
||||
return {
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: [
|
||||
{
|
||||
...builderQuery,
|
||||
// Inject trace_id filter for scoping results to this trace
|
||||
filters: {
|
||||
op: builderQuery.filters?.op || 'AND',
|
||||
items: [
|
||||
...(builderQuery.filters?.items || []),
|
||||
{ ...TRACE_ID_FILTER_ITEM, value: traceId },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function TraceDetailsFilter({
|
||||
startTime,
|
||||
endTime,
|
||||
traceId,
|
||||
onFilteredSpansChange,
|
||||
}: TraceDetailsFilterProps): JSX.Element {
|
||||
const [builderQuery, setBuilderQuery] = useState<IBuilderQuery>(
|
||||
buildInitialQuery,
|
||||
);
|
||||
const [noData, setNoData] = useState(false);
|
||||
const [filteredSpanIds, setFilteredSpanIds] = useState<string[]>([]);
|
||||
const [currentSearchedIndex, setCurrentSearchedIndex] = useState(0);
|
||||
|
||||
const { search } = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
const expression = builderQuery.filter?.expression || '';
|
||||
const hasActiveFilter =
|
||||
(builderQuery.filters?.items || []).length > 0 ||
|
||||
expression.trim().length > 0;
|
||||
|
||||
// Called by QuerySearch when user types in the CodeMirror editor
|
||||
const handleExpressionChange = useCallback(
|
||||
(value: string): void => {
|
||||
setBuilderQuery((prev) => {
|
||||
if (!value.trim() && (prev.filters?.items || []).length === 0) {
|
||||
onFilteredSpansChange([], false);
|
||||
setFilteredSpanIds([]);
|
||||
setCurrentSearchedIndex(0);
|
||||
setNoData(false);
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
filter: { expression: value },
|
||||
};
|
||||
});
|
||||
},
|
||||
[onFilteredSpansChange],
|
||||
);
|
||||
|
||||
// Called by SpanScopeSelector when scope changes (All Spans / Root Spans etc.)
|
||||
// Merges the scope filter items into filter.expression using the shared util
|
||||
const handleScopeChange = useCallback((value: TagFilter): void => {
|
||||
setBuilderQuery((prev) => {
|
||||
const currentExpression = prev.filter?.expression || '';
|
||||
const {
|
||||
filters: mergedFilters,
|
||||
filter: mergedFilter,
|
||||
} = convertFiltersToExpressionWithExistingQuery(value, currentExpression);
|
||||
return {
|
||||
...prev,
|
||||
filters: mergedFilters,
|
||||
filter: mergedFilter,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handlePrevNext = useCallback(
|
||||
(index: number, spanId?: string): void => {
|
||||
const searchParams = new URLSearchParams(search);
|
||||
if (spanId) {
|
||||
searchParams.set('spanId', spanId);
|
||||
} else {
|
||||
searchParams.set('spanId', filteredSpanIds[index]);
|
||||
}
|
||||
history.replace({ search: searchParams.toString() });
|
||||
},
|
||||
[filteredSpanIds, history, search],
|
||||
);
|
||||
|
||||
const query = useMemo(() => prepareApiQuery(builderQuery, traceId), [
|
||||
builderQuery,
|
||||
traceId,
|
||||
]);
|
||||
|
||||
const { isFetching, error } = useGetQueryRange(
|
||||
{
|
||||
query,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
tableParams: {
|
||||
pagination: {
|
||||
offset: 0,
|
||||
limit: 200,
|
||||
},
|
||||
selectColumns: [
|
||||
{
|
||||
key: 'name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
id: 'name--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
DEFAULT_ENTITY_VERSION,
|
||||
{
|
||||
queryKey: [builderQuery.filter, builderQuery.filters],
|
||||
enabled: hasActiveFilter,
|
||||
onSuccess: (data) => {
|
||||
if (data?.payload.data.newResult.data.result[0].list) {
|
||||
const uniqueSpans = uniqBy(
|
||||
data?.payload.data.newResult.data.result[0].list,
|
||||
'data.spanID',
|
||||
);
|
||||
const spanIds = uniqueSpans.map((val) => val.data.spanID);
|
||||
setFilteredSpanIds(spanIds);
|
||||
onFilteredSpansChange(spanIds, true);
|
||||
handlePrevNext(0, spanIds[0]);
|
||||
setNoData(false);
|
||||
} else {
|
||||
setNoData(true);
|
||||
setFilteredSpanIds([]);
|
||||
onFilteredSpansChange([], true);
|
||||
setCurrentSearchedIndex(0);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="trace-details-filter">
|
||||
<div className="trace-details-filter__search">
|
||||
<QuerySearch
|
||||
queryData={builderQuery}
|
||||
onChange={handleExpressionChange}
|
||||
dataSource={DataSource.TRACES}
|
||||
placeholder="Search Filter : select options from suggested values, for IN/NOT IN operators - press 'Enter' after selecting options"
|
||||
/>
|
||||
</div>
|
||||
<div className="trace-details-filter__scope">
|
||||
<SpanScopeSelector
|
||||
query={builderQuery}
|
||||
onChange={handleScopeChange}
|
||||
skipQueryBuilderRedirect
|
||||
/>
|
||||
</div>
|
||||
{filteredSpanIds.length > 0 && (
|
||||
<div className="trace-details-filter__nav">
|
||||
<Typography.Text>
|
||||
{currentSearchedIndex + 1} / {filteredSpanIds.length}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
icon={<ChevronUp size={14} />}
|
||||
disabled={currentSearchedIndex === 0}
|
||||
type="text"
|
||||
onClick={(): void => {
|
||||
handlePrevNext(currentSearchedIndex - 1);
|
||||
setCurrentSearchedIndex((prev) => prev - 1);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={<ChevronDown size={14} />}
|
||||
type="text"
|
||||
disabled={currentSearchedIndex === filteredSpanIds.length - 1}
|
||||
onClick={(): void => {
|
||||
handlePrevNext(currentSearchedIndex + 1);
|
||||
setCurrentSearchedIndex((prev) => prev + 1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isFetching && <Spin indicator={<LoadingOutlined spin />} size="small" />}
|
||||
{error && (
|
||||
<Tooltip title={(error as AxiosError)?.message || 'Something went wrong'}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
)}
|
||||
{noData && (
|
||||
<Typography.Text className="trace-details-filter__no-results">
|
||||
No results found
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TraceDetailsFilter;
|
||||
@@ -1,7 +1,7 @@
|
||||
.trace-details-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
padding: 0px 16px;
|
||||
|
||||
.previous-btn {
|
||||
display: flex;
|
||||
@@ -58,12 +58,6 @@
|
||||
border-left: unset;
|
||||
border-radius: 0px 4px 4px 0px;
|
||||
}
|
||||
|
||||
&__filter {
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
|
||||
@@ -6,25 +6,9 @@ import history from 'lib/history';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { TraceDetailV2URLProps } from 'types/api/trace/getTraceV2';
|
||||
|
||||
import Filters from '../TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters';
|
||||
|
||||
import './TraceDetailsHeader.styles.scss';
|
||||
|
||||
interface FilterMetadata {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
traceId: string;
|
||||
}
|
||||
|
||||
interface TraceDetailsHeaderProps {
|
||||
filterMetadata: FilterMetadata;
|
||||
onFilteredSpansChange: (spanIds: string[], isFilterActive: boolean) => void;
|
||||
}
|
||||
|
||||
function TraceDetailsHeader({
|
||||
filterMetadata,
|
||||
onFilteredSpansChange,
|
||||
}: TraceDetailsHeaderProps): JSX.Element {
|
||||
function TraceDetailsHeader(): JSX.Element {
|
||||
const { id: traceID } = useParams<TraceDetailV2URLProps>();
|
||||
|
||||
const handlePreviousBtnClick = useCallback((): void => {
|
||||
@@ -47,14 +31,6 @@ function TraceDetailsHeader({
|
||||
<Typography.Text className="trace-id">Trace ID</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className="trace-id-value">{traceID}</Typography.Text>
|
||||
<div className="trace-details-header__filter">
|
||||
<Filters
|
||||
startTime={filterMetadata.startTime}
|
||||
endTime={filterMetadata.endTime}
|
||||
traceID={filterMetadata.traceId}
|
||||
onFilteredSpansChange={onFilteredSpansChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,14 +15,7 @@ import { useScrollToSpan } from './hooks/useScrollToSpan';
|
||||
import { EventRect, FlamegraphCanvasProps, SpanRect } from './types';
|
||||
|
||||
function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
const {
|
||||
layout,
|
||||
traceMetadata,
|
||||
firstSpanAtFetchLevel,
|
||||
onSpanClick,
|
||||
filteredSpanIds,
|
||||
isFilterActive,
|
||||
} = props;
|
||||
const { layout, traceMetadata, firstSpanAtFetchLevel, onSpanClick } = props;
|
||||
|
||||
const isDarkMode = useIsDarkMode(); //TODO: see if can be removed or use a new hook
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
@@ -134,8 +127,6 @@ function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
spanRectsRef,
|
||||
eventRectsRef,
|
||||
hoveredEventKey,
|
||||
filteredSpanIds,
|
||||
isFilterActive,
|
||||
});
|
||||
|
||||
useScrollToSpan({
|
||||
|
||||
@@ -9,15 +9,16 @@ import Error from '../TraceWaterfall/TraceWaterfallStates/Error/Error';
|
||||
import FlamegraphCanvas from './FlamegraphCanvas';
|
||||
import { useVisualLayoutWorker } from './hooks/useVisualLayoutWorker';
|
||||
|
||||
interface TraceFlamegraphProps {
|
||||
filteredSpanIds: string[];
|
||||
isFilterActive: boolean;
|
||||
//TODO: analyse if this is needed or not and move to separate file if needed else delete this enum.
|
||||
enum TraceFlamegraphState {
|
||||
LOADING = 'LOADING',
|
||||
SUCCESS = 'SUCCESS',
|
||||
NO_DATA = 'NO_DATA',
|
||||
ERROR = 'ERROR',
|
||||
FETCHING_WITH_OLD_DATA = 'FETCHING_WITH_OLD_DATA',
|
||||
}
|
||||
|
||||
function TraceFlamegraph({
|
||||
filteredSpanIds,
|
||||
isFilterActive,
|
||||
}: TraceFlamegraphProps): JSX.Element {
|
||||
function TraceFlamegraph(): JSX.Element {
|
||||
const { id: traceId } = useParams<TraceDetailFlamegraphURLProps>();
|
||||
const urlQuery = useUrlQuery();
|
||||
const history = useHistory();
|
||||
@@ -57,10 +58,45 @@ function TraceFlamegraph({
|
||||
spans,
|
||||
);
|
||||
|
||||
const content = useMemo(() => {
|
||||
const flamegraphState = useMemo(() => {
|
||||
// Loading: fetching data or worker computing layout
|
||||
if (isFetching || isComputing) {
|
||||
if (layout.totalVisualRows > 0) {
|
||||
return TraceFlamegraphState.FETCHING_WITH_OLD_DATA;
|
||||
}
|
||||
return TraceFlamegraphState.LOADING;
|
||||
}
|
||||
// Error: network or worker failure
|
||||
if (fetchError || workerError) {
|
||||
return TraceFlamegraphState.ERROR;
|
||||
}
|
||||
if (data?.payload?.spans && data.payload.spans.length === 0) {
|
||||
return TraceFlamegraphState.NO_DATA;
|
||||
}
|
||||
return TraceFlamegraphState.SUCCESS;
|
||||
}, [fetchError, workerError, isFetching, isComputing, data, layout]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
switch (flamegraphState) {
|
||||
case TraceFlamegraphState.LOADING:
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', padding: '8px 12px' }}>
|
||||
<Skeleton
|
||||
active
|
||||
paragraph={{
|
||||
rows: 8,
|
||||
width: ['100%', '95%', '85%', '70%', '50%', '35%', '20%', '10%'],
|
||||
}}
|
||||
title={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case TraceFlamegraphState.ERROR:
|
||||
return <Error error={(fetchError || workerError) as any} />;
|
||||
case TraceFlamegraphState.NO_DATA:
|
||||
return <div>No data found for trace {traceId}</div>;
|
||||
case TraceFlamegraphState.SUCCESS:
|
||||
case TraceFlamegraphState.FETCHING_WITH_OLD_DATA:
|
||||
return (
|
||||
<FlamegraphCanvas
|
||||
layout={layout}
|
||||
@@ -71,59 +107,19 @@ function TraceFlamegraph({
|
||||
startTime: data?.payload?.startTimestampMillis || 0,
|
||||
endTime: data?.payload?.endTimestampMillis || 0,
|
||||
}}
|
||||
filteredSpanIds={filteredSpanIds}
|
||||
isFilterActive={isFilterActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', padding: '8px 12px' }}>
|
||||
<Skeleton
|
||||
active
|
||||
paragraph={{
|
||||
rows: 8,
|
||||
width: ['100%', '95%', '85%', '70%', '50%', '35%', '20%', '10%'],
|
||||
}}
|
||||
title={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <div>Fetching the trace...</div>;
|
||||
}
|
||||
// Error: network or worker failure
|
||||
if (fetchError || workerError) {
|
||||
return <Error error={(fetchError || workerError) as any} />;
|
||||
}
|
||||
if (data?.payload?.spans && data.payload.spans.length === 0) {
|
||||
return <div>No data found for trace {traceId}</div>;
|
||||
}
|
||||
return (
|
||||
<FlamegraphCanvas
|
||||
layout={layout}
|
||||
firstSpanAtFetchLevel={firstSpanAtFetchLevel}
|
||||
setFirstSpanAtFetchLevel={setFirstSpanAtFetchLevel}
|
||||
onSpanClick={handleSpanClick}
|
||||
traceMetadata={{
|
||||
startTime: data?.payload?.startTimestampMillis || 0,
|
||||
endTime: data?.payload?.endTimestampMillis || 0,
|
||||
}}
|
||||
filteredSpanIds={filteredSpanIds}
|
||||
isFilterActive={isFilterActive}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
data?.payload?.endTimestampMillis,
|
||||
data?.payload?.startTimestampMillis,
|
||||
data?.payload?.spans,
|
||||
fetchError,
|
||||
filteredSpanIds,
|
||||
firstSpanAtFetchLevel,
|
||||
handleSpanClick,
|
||||
isComputing,
|
||||
isFilterActive,
|
||||
isFetching,
|
||||
layout,
|
||||
flamegraphState,
|
||||
spans,
|
||||
traceId,
|
||||
workerError,
|
||||
handleSpanClick,
|
||||
]);
|
||||
|
||||
return <>{content}</>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { RefObject, useCallback, useMemo, useRef } from 'react';
|
||||
import React, { RefObject, useCallback, useRef } from 'react';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
|
||||
@@ -28,8 +28,6 @@ interface UseFlamegraphDrawArgs {
|
||||
spanRectsRef?: React.MutableRefObject<SpanRect[]>;
|
||||
eventRectsRef?: React.MutableRefObject<EventRect[]>;
|
||||
hoveredEventKey?: string | null;
|
||||
filteredSpanIds?: string[];
|
||||
isFilterActive?: boolean;
|
||||
}
|
||||
|
||||
interface UseFlamegraphDrawResult {
|
||||
@@ -55,8 +53,6 @@ interface DrawLevelArgs {
|
||||
eventRectsArray: EventRect[];
|
||||
metrics: FlamegraphRowMetrics;
|
||||
hoveredEventKey?: string | null;
|
||||
filteredSpanIdsSet?: Set<string> | null;
|
||||
isFilterActive?: boolean;
|
||||
}
|
||||
|
||||
function drawLevel(args: DrawLevelArgs): void {
|
||||
@@ -75,8 +71,6 @@ function drawLevel(args: DrawLevelArgs): void {
|
||||
eventRectsArray,
|
||||
metrics,
|
||||
hoveredEventKey,
|
||||
filteredSpanIdsSet,
|
||||
isFilterActive: isFilterActiveInLevel,
|
||||
} = args;
|
||||
|
||||
const viewEndTs = viewStartTs + timeSpan;
|
||||
@@ -114,11 +108,6 @@ function drawLevel(args: DrawLevelArgs): void {
|
||||
|
||||
const color = getSpanColor({ span, isDarkMode });
|
||||
|
||||
const isDimmedByFilter =
|
||||
!!isFilterActiveInLevel &&
|
||||
!!filteredSpanIdsSet &&
|
||||
!filteredSpanIdsSet.has(span.spanId);
|
||||
|
||||
drawSpanBar({
|
||||
ctx,
|
||||
span,
|
||||
@@ -134,7 +123,6 @@ function drawLevel(args: DrawLevelArgs): void {
|
||||
selectedSpanId,
|
||||
hoveredSpanId,
|
||||
hoveredEventKey,
|
||||
isDimmedByFilter,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -219,8 +207,6 @@ export function useFlamegraphDraw(
|
||||
spanRectsRef: spanRectsRefProp,
|
||||
eventRectsRef: eventRectsRefProp,
|
||||
hoveredEventKey,
|
||||
filteredSpanIds,
|
||||
isFilterActive,
|
||||
} = args;
|
||||
|
||||
const spanRectsRefInternal = useRef<SpanRect[]>([]);
|
||||
@@ -228,14 +214,6 @@ export function useFlamegraphDraw(
|
||||
const eventRectsRefInternal = useRef<EventRect[]>([]);
|
||||
const eventRectsRef = eventRectsRefProp ?? eventRectsRefInternal;
|
||||
|
||||
const filteredSpanIdsSet = useMemo(
|
||||
() =>
|
||||
isFilterActive && filteredSpanIds && filteredSpanIds.length > 0
|
||||
? new Set(filteredSpanIds)
|
||||
: null,
|
||||
[filteredSpanIds, isFilterActive],
|
||||
);
|
||||
|
||||
const drawFlamegraph = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const container = containerRef.current;
|
||||
@@ -313,8 +291,6 @@ export function useFlamegraphDraw(
|
||||
eventRectsArray,
|
||||
metrics,
|
||||
hoveredEventKey: currentHoveredEventKey,
|
||||
filteredSpanIdsSet,
|
||||
isFilterActive,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -335,8 +311,6 @@ export function useFlamegraphDraw(
|
||||
hoveredSpanId,
|
||||
hoveredEventKey,
|
||||
isDarkMode,
|
||||
filteredSpanIdsSet,
|
||||
isFilterActive,
|
||||
]);
|
||||
|
||||
return { drawFlamegraph, spanRectsRef, eventRectsRef };
|
||||
|
||||
@@ -14,8 +14,6 @@ export interface FlamegraphCanvasProps {
|
||||
setFirstSpanAtFetchLevel: Dispatch<SetStateAction<string>>;
|
||||
onSpanClick: (spanId: string) => void;
|
||||
traceMetadata: ITraceMetadata;
|
||||
filteredSpanIds: string[];
|
||||
isFilterActive: boolean;
|
||||
}
|
||||
|
||||
export interface SpanRect {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
@@ -209,7 +208,6 @@ interface DrawSpanBarArgs {
|
||||
selectedSpanId?: string | null;
|
||||
hoveredSpanId?: string | null;
|
||||
hoveredEventKey?: string | null;
|
||||
isDimmedByFilter?: boolean;
|
||||
}
|
||||
|
||||
export function drawSpanBar(args: DrawSpanBarArgs): void {
|
||||
@@ -228,21 +226,12 @@ export function drawSpanBar(args: DrawSpanBarArgs): void {
|
||||
selectedSpanId,
|
||||
hoveredSpanId,
|
||||
hoveredEventKey,
|
||||
isDimmedByFilter,
|
||||
} = args;
|
||||
|
||||
const spanY = y + metrics.SPAN_BAR_Y_OFFSET;
|
||||
const isSelected = selectedSpanId === span.spanId;
|
||||
const isHovered = hoveredSpanId === span.spanId;
|
||||
const isSelectedOrHovered = isSelected || isHovered;
|
||||
const shouldDim = isDimmedByFilter && !isSelectedOrHovered;
|
||||
|
||||
// Dim non-matching spans when filter is active (matches waterfall's .dimmed-span { opacity: 0.4 }).
|
||||
// Alpha is applied to bar + events only; label is drawn after restoring alpha to 1
|
||||
// so text stays readable against the faded bar.
|
||||
if (shouldDim) {
|
||||
ctx.globalAlpha = 0.4;
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, spanY, width, metrics.SPAN_BAR_HEIGHT, 2);
|
||||
@@ -314,11 +303,6 @@ export function drawSpanBar(args: DrawSpanBarArgs): void {
|
||||
});
|
||||
});
|
||||
|
||||
// Restore alpha before drawing label so text is legible on dimmed bars
|
||||
if (shouldDim) {
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
drawSpanLabel({
|
||||
ctx,
|
||||
span,
|
||||
|
||||
@@ -30,8 +30,6 @@ interface ITraceWaterfallProps {
|
||||
setInterestedSpanId: Dispatch<SetStateAction<IInterestedSpan>>;
|
||||
selectedSpan: Span | undefined;
|
||||
setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>;
|
||||
filteredSpanIds: string[];
|
||||
isFilterActive: boolean;
|
||||
}
|
||||
|
||||
function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
|
||||
@@ -45,8 +43,6 @@ function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
|
||||
setInterestedSpanId,
|
||||
setSelectedSpan,
|
||||
selectedSpan,
|
||||
filteredSpanIds,
|
||||
isFilterActive,
|
||||
} = props;
|
||||
// get the current state of trace waterfall based on the API lifecycle
|
||||
const traceWaterfallState = useMemo(() => {
|
||||
@@ -110,8 +106,6 @@ function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
|
||||
setInterestedSpanId={setInterestedSpanId}
|
||||
selectedSpan={selectedSpan}
|
||||
setSelectedSpan={setSelectedSpan}
|
||||
filteredSpanIds={filteredSpanIds}
|
||||
isFilterActive={isFilterActive}
|
||||
isFetching={
|
||||
traceWaterfallState ===
|
||||
TraceWaterfallStates.FETCHING_WITH_OLD_DATA_PRESENT
|
||||
@@ -123,9 +117,7 @@ function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
|
||||
}
|
||||
}, [
|
||||
errorFetchingTraceData,
|
||||
filteredSpanIds,
|
||||
interestedSpanId,
|
||||
isFilterActive,
|
||||
selectedSpan,
|
||||
setInterestedSpanId,
|
||||
setSelectedSpan,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px 0px 20px;
|
||||
gap: 12px;
|
||||
|
||||
.query-builder-search-v2 {
|
||||
|
||||
@@ -40,6 +40,7 @@ import { EventTooltipContent } from '../../../SpanHoverCard/EventTooltipContent'
|
||||
import SpanHoverCard from '../../../SpanHoverCard/SpanHoverCard';
|
||||
import AddSpanToFunnelModal from '../../AddSpanToFunnelModal/AddSpanToFunnelModal';
|
||||
import { IInterestedSpan } from '../../TraceWaterfall';
|
||||
import Filters from './Filters/Filters';
|
||||
|
||||
import './Success.styles.scss';
|
||||
|
||||
@@ -61,8 +62,6 @@ interface ISuccessProps {
|
||||
setInterestedSpanId: Dispatch<SetStateAction<IInterestedSpan>>;
|
||||
selectedSpan: Span | undefined;
|
||||
setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>;
|
||||
filteredSpanIds: string[];
|
||||
isFilterActive: boolean;
|
||||
isFetching?: boolean;
|
||||
}
|
||||
|
||||
@@ -339,11 +338,11 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
setInterestedSpanId,
|
||||
setSelectedSpan,
|
||||
selectedSpan,
|
||||
filteredSpanIds,
|
||||
isFilterActive,
|
||||
isFetching,
|
||||
} = props;
|
||||
|
||||
const [filteredSpanIds, setFilteredSpanIds] = useState<string[]>([]);
|
||||
const [isFilterActive, setIsFilterActive] = useState<boolean>(false);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const virtualizerRef = useRef<Virtualizer<HTMLDivElement, Element>>();
|
||||
@@ -378,6 +377,14 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
applyHoverClass(null);
|
||||
}, [applyHoverClass]);
|
||||
|
||||
const handleFilteredSpansChange = useCallback(
|
||||
(spanIds: string[], isActive: boolean) => {
|
||||
setFilteredSpanIds(spanIds);
|
||||
setIsFilterActive(isActive);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCollapseUncollapse = useCallback(
|
||||
(spanId: string, collapse: boolean) => {
|
||||
setInterestedSpanId({ spanId, isUncollapsed: !collapse });
|
||||
@@ -571,6 +578,12 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Filters
|
||||
startTime={traceMetadata.startTime / 1e3}
|
||||
endTime={traceMetadata.endTime / 1e3}
|
||||
traceID={traceMetadata.traceId}
|
||||
onFilteredSpansChange={handleFilteredSpansChange}
|
||||
/>
|
||||
{isFetching && <div className="waterfall-loading-bar" />}
|
||||
<div className="waterfall-split-panel" ref={scrollContainerRef}>
|
||||
{/* Sticky header row */}
|
||||
|
||||
@@ -185,8 +185,6 @@ function TestComponent(): JSX.Element {
|
||||
setInterestedSpanId={jest.fn()}
|
||||
selectedSpan={selectedSpan}
|
||||
setSelectedSpan={setSelectedSpan}
|
||||
filteredSpanIds={[]}
|
||||
isFilterActive={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -218,8 +216,6 @@ describe('Span Click User Flows', () => {
|
||||
setInterestedSpanId={jest.fn()}
|
||||
selectedSpan={undefined}
|
||||
setSelectedSpan={jest.fn()}
|
||||
filteredSpanIds={[]}
|
||||
isFilterActive={false}
|
||||
/>,
|
||||
undefined,
|
||||
{ initialRoute: '/trace' },
|
||||
@@ -397,8 +393,6 @@ describe('Span Click User Flows', () => {
|
||||
setInterestedSpanId={jest.fn()}
|
||||
selectedSpan={undefined}
|
||||
setSelectedSpan={jest.fn()}
|
||||
filteredSpanIds={[]}
|
||||
isFilterActive={false}
|
||||
/>,
|
||||
undefined,
|
||||
{ initialRoute: '/trace' },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Collapse } from 'antd';
|
||||
import { useDetailsPanel } from 'components/DetailsPanel';
|
||||
@@ -30,8 +30,6 @@ function TraceDetailsV3(): JSX.Element {
|
||||
);
|
||||
const [uncollapsedNodes, setUncollapsedNodes] = useState<string[]>([]);
|
||||
const [selectedSpan, setSelectedSpan] = useState<Span>();
|
||||
const [filteredSpanIds, setFilteredSpanIds] = useState<string[]>([]);
|
||||
const [isFilterActive, setIsFilterActive] = useState(false);
|
||||
|
||||
const selectedSpanId = urlQuery.get('spanId') || undefined;
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
@@ -41,29 +39,15 @@ function TraceDetailsV3(): JSX.Element {
|
||||
safeNavigate({ search: urlQuery.toString() });
|
||||
}, [urlQuery, safeNavigate]);
|
||||
|
||||
const handleFilteredSpansChange = useCallback(
|
||||
(spanIds: string[], isActive: boolean): void => {
|
||||
setFilteredSpanIds(spanIds);
|
||||
setIsFilterActive(isActive);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const panelState = useDetailsPanel({
|
||||
entityId: selectedSpanId,
|
||||
onClose: handleSpanDetailsClose,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const spanId = urlQuery.get('spanId') || '';
|
||||
// Only update interestedSpanId when a new span is selected,
|
||||
// not when it's cleared (panel close) — avoids unnecessary API refetch
|
||||
if (!spanId) {
|
||||
return;
|
||||
}
|
||||
setInterestedSpanId({
|
||||
spanId,
|
||||
isUncollapsed: true,
|
||||
spanId: urlQuery.get('spanId') || '',
|
||||
isUncollapsed: urlQuery.get('spanId') !== '',
|
||||
});
|
||||
}, [urlQuery]);
|
||||
|
||||
@@ -118,19 +102,6 @@ function TraceDetailsV3(): JSX.Element {
|
||||
[],
|
||||
);
|
||||
|
||||
const filterMetadata = useMemo(
|
||||
() => ({
|
||||
startTime: (traceData?.payload?.startTimestampMillis || 0) / 1e3,
|
||||
endTime: (traceData?.payload?.endTimestampMillis || 0) / 1e3,
|
||||
traceId: traceId || '',
|
||||
}),
|
||||
[
|
||||
traceData?.payload?.startTimestampMillis,
|
||||
traceData?.payload?.endTimestampMillis,
|
||||
traceId,
|
||||
],
|
||||
);
|
||||
|
||||
const isDocked = spanDetailVariant === SpanDetailVariant.DOCKED;
|
||||
const isWaterfallDocked = panelState.isOpen && isDocked;
|
||||
|
||||
@@ -150,18 +121,13 @@ function TraceDetailsV3(): JSX.Element {
|
||||
uncollapsedNodes={uncollapsedNodes}
|
||||
selectedSpan={selectedSpan}
|
||||
setSelectedSpan={setSelectedSpan}
|
||||
filteredSpanIds={filteredSpanIds}
|
||||
isFilterActive={isFilterActive}
|
||||
/>
|
||||
</ResizableBox>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="trace-details-v3">
|
||||
<TraceDetailsHeader
|
||||
filterMetadata={filterMetadata}
|
||||
onFilteredSpansChange={handleFilteredSpansChange}
|
||||
/>
|
||||
<TraceDetailsHeader />
|
||||
|
||||
<div className="trace-details-v3__content">
|
||||
<Collapse
|
||||
@@ -186,10 +152,7 @@ function TraceDetailsV3(): JSX.Element {
|
||||
),
|
||||
children: (
|
||||
<ResizableBox defaultHeight={300} minHeight={100} maxHeight={400}>
|
||||
<TraceFlamegraph
|
||||
filteredSpanIds={filteredSpanIds}
|
||||
isFilterActive={isFilterActive}
|
||||
/>
|
||||
<TraceFlamegraph />
|
||||
</ResizableBox>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
|
||||
RequestContentType: "application/json",
|
||||
Response: new(citypes.GettableAccountWithConnectionArtifact),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
@@ -138,6 +138,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
|
||||
Summary: "List services metadata",
|
||||
Description: "This endpoint lists the services metadata for the specified cloud provider",
|
||||
Request: nil,
|
||||
RequestQuery: new(citypes.ListServicesMetadataParams),
|
||||
RequestContentType: "",
|
||||
Response: new(citypes.GettableServicesMetadata),
|
||||
ResponseContentType: "application/json",
|
||||
@@ -158,6 +159,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
|
||||
Summary: "Get service",
|
||||
Description: "This endpoint gets a service for the specified cloud provider",
|
||||
Request: nil,
|
||||
RequestQuery: new(citypes.GetServiceParams),
|
||||
RequestContentType: "",
|
||||
Response: new(citypes.Service),
|
||||
ResponseContentType: "application/json",
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
@@ -57,6 +58,7 @@ type provider struct {
|
||||
factoryHandler factory.Handler
|
||||
cloudIntegrationHandler cloudintegration.Handler
|
||||
ruleStateHistoryHandler rulestatehistory.Handler
|
||||
traceDetailHandler tracedetail.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -83,6 +85,7 @@ func NewFactory(
|
||||
factoryHandler factory.Handler,
|
||||
cloudIntegrationHandler cloudintegration.Handler,
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
|
||||
return newProvider(
|
||||
@@ -112,6 +115,7 @@ func NewFactory(
|
||||
factoryHandler,
|
||||
cloudIntegrationHandler,
|
||||
ruleStateHistoryHandler,
|
||||
traceDetailHandler,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -143,6 +147,7 @@ func newProvider(
|
||||
factoryHandler factory.Handler,
|
||||
cloudIntegrationHandler cloudintegration.Handler,
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
) (apiserver.APIServer, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
|
||||
router := mux.NewRouter().UseEncodedPath()
|
||||
@@ -172,6 +177,7 @@ func newProvider(
|
||||
factoryHandler: factoryHandler,
|
||||
cloudIntegrationHandler: cloudIntegrationHandler,
|
||||
ruleStateHistoryHandler: ruleStateHistoryHandler,
|
||||
traceDetailHandler: traceDetailHandler,
|
||||
}
|
||||
|
||||
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
|
||||
@@ -272,6 +278,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addTraceDetailRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
33
pkg/apiserver/signozapiserver/tracedetail.go
Normal file
33
pkg/apiserver/signozapiserver/tracedetail.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addTraceDetailRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v3/traces/{traceID}/waterfall", handler.New(
|
||||
provider.authZ.ViewAccess(provider.traceDetailHandler.GetWaterfall),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetWaterfall",
|
||||
Tags: []string{"tracedetail"},
|
||||
Summary: "Get waterfall view for a trace",
|
||||
Description: "Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination",
|
||||
Request: new(tracedetailtypes.WaterfallRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(tracedetailtypes.WaterfallResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
61
pkg/modules/tracedetail/impltracedetail/handler.go
Normal file
61
pkg/modules/tracedetail/impltracedetail/handler.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package impltracedetail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
module tracedetail.Module
|
||||
}
|
||||
|
||||
func NewHandler(module tracedetail.Module) tracedetail.Handler {
|
||||
return &handler{module: module}
|
||||
}
|
||||
|
||||
func (h *handler) GetWaterfall(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
traceID := mux.Vars(r)["traceID"]
|
||||
if traceID == "" {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "traceId is required"))
|
||||
return
|
||||
}
|
||||
|
||||
var req tracedetailtypes.WaterfallRequest
|
||||
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.module.GetWaterfall(ctx, orgID, traceID, &req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, result)
|
||||
}
|
||||
272
pkg/modules/tracedetail/impltracedetail/module.go
Normal file
272
pkg/modules/tracedetail/impltracedetail/module.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package impltracedetail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
tracedetailv2 "github.com/SigNoz/signoz/pkg/query-service/app/traces/tracedetail"
|
||||
)
|
||||
|
||||
const (
|
||||
traceDB = "signoz_traces"
|
||||
traceTable = "distributed_signoz_index_v3"
|
||||
traceSummaryTable = "distributed_trace_summary"
|
||||
cacheTTL = 5 * time.Minute
|
||||
fluxInterval = 2 * time.Minute
|
||||
)
|
||||
|
||||
type module struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
cache cache.Cache
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewModule(telemetryStore telemetrystore.TelemetryStore, cache cache.Cache, providerSettings factory.ProviderSettings) tracedetail.Module {
|
||||
return &module{
|
||||
telemetryStore: telemetryStore,
|
||||
cache: cache,
|
||||
logger: providerSettings.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *module) GetWaterfall(ctx context.Context, orgID valuer.UUID, traceID string, req *tracedetailtypes.WaterfallRequest) (*tracedetailtypes.WaterfallResponse, error) {
|
||||
response := new(tracedetailtypes.WaterfallResponse)
|
||||
var startTime, endTime, durationNano, totalErrorSpans, totalSpans uint64
|
||||
var spanIDToSpanNodeMap = map[string]*tracedetailtypes.WaterfallSpan{}
|
||||
var traceRoots []*tracedetailtypes.WaterfallSpan
|
||||
var serviceNameToTotalDurationMap = map[string]uint64{}
|
||||
var serviceNameIntervalMap = map[string][]tracedetailv2.Interval{}
|
||||
var hasMissingSpans bool
|
||||
|
||||
// Try cache first
|
||||
cachedTraceData, err := m.getFromCache(ctx, orgID, traceID)
|
||||
if err == nil {
|
||||
startTime = cachedTraceData.StartTime
|
||||
endTime = cachedTraceData.EndTime
|
||||
durationNano = cachedTraceData.DurationNano
|
||||
spanIDToSpanNodeMap = cachedTraceData.SpanIDToSpanNodeMap
|
||||
serviceNameToTotalDurationMap = cachedTraceData.ServiceNameToTotalDurationMap
|
||||
traceRoots = cachedTraceData.TraceRoots
|
||||
totalSpans = cachedTraceData.TotalSpans
|
||||
totalErrorSpans = cachedTraceData.TotalErrorSpans
|
||||
hasMissingSpans = cachedTraceData.HasMissingSpans
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
m.logger.InfoContext(ctx, "cache miss for v3 waterfall", slog.String("trace_id", traceID))
|
||||
|
||||
// Query trace summary for time boundaries
|
||||
var summary tracedetailtypes.TraceSummary
|
||||
summaryQuery := fmt.Sprintf(
|
||||
"SELECT trace_id, min(start) AS start, max(end) AS end, sum(num_spans) AS num_spans FROM %s.%s WHERE trace_id=$1 GROUP BY trace_id",
|
||||
traceDB, traceSummaryTable,
|
||||
)
|
||||
err := m.telemetryStore.ClickhouseDB().QueryRow(ctx, summaryQuery, traceID).Scan(
|
||||
&summary.TraceID, &summary.Start, &summary.End, &summary.NumSpans,
|
||||
)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return response, nil
|
||||
}
|
||||
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "error querying trace summary: %v", err)
|
||||
}
|
||||
|
||||
// Query span details
|
||||
detailsQuery := fmt.Sprintf(
|
||||
"SELECT DISTINCT ON (span_id) "+
|
||||
"timestamp, duration_nano, span_id, trace_id, has_error, kind, "+
|
||||
"resource_string_service$$name, name, links as references, "+
|
||||
"attributes_string, attributes_number, attributes_bool, resources_string, "+
|
||||
"events, status_message, status_code_string, kind_string, parent_span_id, "+
|
||||
"flags, is_remote, trace_state, status_code, "+
|
||||
"db_name, db_operation, http_method, http_url, http_host, "+
|
||||
"external_http_method, external_http_url, response_status_code "+
|
||||
"FROM %s.%s WHERE trace_id=$1 AND ts_bucket_start>=$2 AND ts_bucket_start<=$3 "+
|
||||
"ORDER BY timestamp ASC, name ASC",
|
||||
traceDB, traceTable,
|
||||
)
|
||||
|
||||
var spanItems []tracedetailtypes.SpanModel
|
||||
err = m.telemetryStore.ClickhouseDB().Select(
|
||||
ctx, &spanItems, detailsQuery,
|
||||
traceID,
|
||||
strconv.FormatInt(summary.Start.Unix()-1800, 10),
|
||||
strconv.FormatInt(summary.End.Unix(), 10),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "error querying trace spans: %v", err)
|
||||
}
|
||||
|
||||
if len(spanItems) == 0 {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
totalSpans = uint64(len(spanItems))
|
||||
spanIDToSpanNodeMap = make(map[string]*tracedetailtypes.WaterfallSpan, len(spanItems))
|
||||
|
||||
// Build span nodes
|
||||
for _, item := range spanItems {
|
||||
span := item.ToSpan()
|
||||
startTimeUnixNano := span.TimeUnixMilli
|
||||
|
||||
// Metadata calculation
|
||||
if startTime == 0 || startTimeUnixNano < startTime {
|
||||
startTime = startTimeUnixNano
|
||||
}
|
||||
if endTime == 0 || (startTimeUnixNano+span.DurationNano) > endTime {
|
||||
endTime = startTimeUnixNano + span.DurationNano
|
||||
}
|
||||
if durationNano == 0 || span.DurationNano > durationNano {
|
||||
durationNano = span.DurationNano
|
||||
}
|
||||
if span.HasError {
|
||||
totalErrorSpans++
|
||||
}
|
||||
|
||||
// Collect intervals for service execution time calculation
|
||||
serviceNameIntervalMap[span.ServiceName] = append(
|
||||
serviceNameIntervalMap[span.ServiceName],
|
||||
tracedetailv2.Interval{StartTime: startTimeUnixNano, Duration: span.DurationNano, Service: span.ServiceName},
|
||||
)
|
||||
|
||||
spanIDToSpanNodeMap[span.SpanID] = span
|
||||
}
|
||||
|
||||
// Build tree: parent-child relationships and missing spans
|
||||
for _, spanNode := range spanIDToSpanNodeMap {
|
||||
if spanNode.ParentSpanID != "" {
|
||||
if parentNode, exists := spanIDToSpanNodeMap[spanNode.ParentSpanID]; exists {
|
||||
parentNode.Children = append(parentNode.Children, spanNode)
|
||||
} else {
|
||||
// Insert missing span
|
||||
missingSpan := &tracedetailtypes.WaterfallSpan{
|
||||
SpanID: spanNode.ParentSpanID,
|
||||
TraceID: spanNode.TraceID,
|
||||
Name: "Missing Span",
|
||||
TimeUnixMilli: spanNode.TimeUnixMilli,
|
||||
DurationNano: spanNode.DurationNano,
|
||||
Events: make([]tracedetailtypes.Event, 0),
|
||||
Children: make([]*tracedetailtypes.WaterfallSpan, 0),
|
||||
Attributes: make(map[string]any),
|
||||
Resource: make(map[string]string),
|
||||
}
|
||||
missingSpan.Children = append(missingSpan.Children, spanNode)
|
||||
spanIDToSpanNodeMap[missingSpan.SpanID] = missingSpan
|
||||
traceRoots = append(traceRoots, missingSpan)
|
||||
hasMissingSpans = true
|
||||
}
|
||||
} else if !containsSpan(traceRoots, spanNode) {
|
||||
traceRoots = append(traceRoots, spanNode)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort children of each span for consistent ordering across requests.
|
||||
for _, root := range traceRoots {
|
||||
SortSpanChildren(root)
|
||||
}
|
||||
|
||||
// Sort trace roots
|
||||
sort.Slice(traceRoots, func(i, j int) bool {
|
||||
if traceRoots[i].TimeUnixMilli == traceRoots[j].TimeUnixMilli {
|
||||
return traceRoots[i].Name < traceRoots[j].Name
|
||||
}
|
||||
return traceRoots[i].TimeUnixMilli < traceRoots[j].TimeUnixMilli
|
||||
})
|
||||
|
||||
serviceNameToTotalDurationMap = tracedetailv2.CalculateServiceTime(serviceNameIntervalMap)
|
||||
|
||||
// Cache the processed data
|
||||
traceCache := &tracedetailtypes.WaterfallCache{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
DurationNano: durationNano,
|
||||
TotalSpans: totalSpans,
|
||||
TotalErrorSpans: totalErrorSpans,
|
||||
SpanIDToSpanNodeMap: spanIDToSpanNodeMap,
|
||||
ServiceNameToTotalDurationMap: serviceNameToTotalDurationMap,
|
||||
TraceRoots: traceRoots,
|
||||
HasMissingSpans: hasMissingSpans,
|
||||
}
|
||||
cacheKey := strings.Join([]string{"v3_waterfall", traceID}, "-")
|
||||
if cacheErr := m.cache.Set(ctx, orgID, cacheKey, traceCache, cacheTTL); cacheErr != nil {
|
||||
m.logger.DebugContext(ctx, "failed to store v3 waterfall cache", slog.String("trace_id", traceID), errors.Attr(cacheErr))
|
||||
}
|
||||
}
|
||||
|
||||
// Span selection: all spans or windowed
|
||||
limit := min(req.Limit, MaxLimitToSelectAllSpans)
|
||||
selectAllSpans := totalSpans <= uint64(limit)
|
||||
|
||||
var (
|
||||
selectedSpans []*tracedetailtypes.WaterfallSpan
|
||||
uncollapsedSpans []string
|
||||
rootServiceName, rootServiceEntryPoint string
|
||||
)
|
||||
if selectAllSpans {
|
||||
selectedSpans, rootServiceName, rootServiceEntryPoint = GetAllSpans(traceRoots)
|
||||
} else {
|
||||
selectedSpans, uncollapsedSpans, rootServiceName, rootServiceEntryPoint = GetSelectedSpans(
|
||||
req.UncollapsedSpans, req.SelectedSpanID, traceRoots, spanIDToSpanNodeMap,
|
||||
)
|
||||
}
|
||||
|
||||
// Convert timestamps to milliseconds for service duration map
|
||||
for serviceName, totalDuration := range serviceNameToTotalDurationMap {
|
||||
serviceNameToTotalDurationMap[serviceName] = totalDuration / 1000000
|
||||
}
|
||||
|
||||
response.Spans = selectedSpans
|
||||
response.UncollapsedSpans = uncollapsedSpans
|
||||
response.StartTimestampMillis = startTime / 1000000
|
||||
response.EndTimestampMillis = endTime / 1000000
|
||||
response.TotalSpansCount = totalSpans
|
||||
response.TotalErrorSpansCount = totalErrorSpans
|
||||
response.RootServiceName = rootServiceName
|
||||
response.RootServiceEntryPoint = rootServiceEntryPoint
|
||||
response.ServiceNameToTotalDurationMap = serviceNameToTotalDurationMap
|
||||
response.HasMissingSpans = hasMissingSpans
|
||||
response.HasMore = !selectAllSpans
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (m *module) getFromCache(ctx context.Context, orgID valuer.UUID, traceID string) (*tracedetailtypes.WaterfallCache, error) {
|
||||
cachedData := new(tracedetailtypes.WaterfallCache)
|
||||
cacheKey := strings.Join([]string{"v3_waterfall", traceID}, "-")
|
||||
err := m.cache.Get(ctx, orgID, cacheKey, cachedData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Skip cache if trace end time falls within flux interval
|
||||
if time.Since(time.UnixMilli(int64(cachedData.EndTime))) < fluxInterval {
|
||||
m.logger.InfoContext(ctx, "trace end time within flux interval, skipping v3 waterfall cache", slog.String("trace_id", traceID))
|
||||
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "trace end time within flux interval, traceID: %s", traceID)
|
||||
}
|
||||
|
||||
m.logger.InfoContext(ctx, "cache hit for v3 waterfall", slog.String("trace_id", traceID))
|
||||
return cachedData, nil
|
||||
}
|
||||
|
||||
func containsSpan(spans []*tracedetailtypes.WaterfallSpan, target *tracedetailtypes.WaterfallSpan) bool {
|
||||
for _, s := range spans {
|
||||
if s.SpanID == target.SpanID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
191
pkg/modules/tracedetail/impltracedetail/waterfall.go
Normal file
191
pkg/modules/tracedetail/impltracedetail/waterfall.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package impltracedetail
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
)
|
||||
|
||||
var (
|
||||
spanLimitPerRequest float64 = 500
|
||||
maxDepthForSelectedChildren int = 5
|
||||
MaxLimitToSelectAllSpans uint = 10_000
|
||||
)
|
||||
|
||||
type traverseOpts struct {
|
||||
uncollapsedSpans map[string]struct{}
|
||||
selectedSpanID string
|
||||
selectAll bool
|
||||
}
|
||||
|
||||
func traverseTrace(
|
||||
span *tracedetailtypes.WaterfallSpan,
|
||||
opts traverseOpts,
|
||||
level uint64,
|
||||
isPartOfPreOrder bool,
|
||||
autoExpandDepth int,
|
||||
) ([]*tracedetailtypes.WaterfallSpan, []string) {
|
||||
|
||||
preOrderTraversal := []*tracedetailtypes.WaterfallSpan{}
|
||||
autoExpandedSpans := []string{}
|
||||
|
||||
span.SubTreeNodeCount = 0
|
||||
nodeWithoutChildren := span.CopyWithoutChildren(level)
|
||||
|
||||
if isPartOfPreOrder {
|
||||
preOrderTraversal = append(preOrderTraversal, nodeWithoutChildren)
|
||||
}
|
||||
|
||||
remainingAutoExpandDepth := 0
|
||||
_, isSelectedSpanUncollapsed := opts.uncollapsedSpans[opts.selectedSpanID]
|
||||
if span.SpanID == opts.selectedSpanID && isSelectedSpanUncollapsed {
|
||||
remainingAutoExpandDepth = maxDepthForSelectedChildren
|
||||
} else if autoExpandDepth > 0 {
|
||||
remainingAutoExpandDepth = autoExpandDepth - 1
|
||||
}
|
||||
|
||||
_, isAlreadyUncollapsed := opts.uncollapsedSpans[span.SpanID]
|
||||
for _, child := range span.Children {
|
||||
isChildWithinMaxDepth := remainingAutoExpandDepth > 0
|
||||
childIsPartOfPreOrder := opts.selectAll || (isPartOfPreOrder && (isAlreadyUncollapsed || isChildWithinMaxDepth))
|
||||
|
||||
if isPartOfPreOrder && isChildWithinMaxDepth && !isAlreadyUncollapsed {
|
||||
if !slices.Contains(autoExpandedSpans, span.SpanID) {
|
||||
autoExpandedSpans = append(autoExpandedSpans, span.SpanID)
|
||||
}
|
||||
}
|
||||
|
||||
childTraversal, childAutoExpanded := traverseTrace(child, opts, level+1, childIsPartOfPreOrder, remainingAutoExpandDepth)
|
||||
preOrderTraversal = append(preOrderTraversal, childTraversal...)
|
||||
autoExpandedSpans = append(autoExpandedSpans, childAutoExpanded...)
|
||||
nodeWithoutChildren.SubTreeNodeCount += child.SubTreeNodeCount + 1
|
||||
span.SubTreeNodeCount += child.SubTreeNodeCount + 1
|
||||
}
|
||||
|
||||
nodeWithoutChildren.SubTreeNodeCount += 1
|
||||
return preOrderTraversal, autoExpandedSpans
|
||||
}
|
||||
|
||||
func GetSelectedSpans(uncollapsedSpans []string, selectedSpanID string, traceRoots []*tracedetailtypes.WaterfallSpan, spanIDToSpanNodeMap map[string]*tracedetailtypes.WaterfallSpan) ([]*tracedetailtypes.WaterfallSpan, []string, string, string) {
|
||||
var preOrderTraversal = make([]*tracedetailtypes.WaterfallSpan, 0)
|
||||
var rootServiceName, rootServiceEntryPoint string
|
||||
|
||||
uncollapsedSpanMap := make(map[string]struct{})
|
||||
for _, spanID := range uncollapsedSpans {
|
||||
uncollapsedSpanMap[spanID] = struct{}{}
|
||||
}
|
||||
|
||||
selectedSpanIndex := -1
|
||||
for _, rootSpanID := range traceRoots {
|
||||
if rootNode, exists := spanIDToSpanNodeMap[rootSpanID.SpanID]; exists {
|
||||
present, spansFromRootToNode := getPathFromRootToSelectedSpanID(rootNode, selectedSpanID)
|
||||
if present {
|
||||
for _, spanID := range spansFromRootToNode {
|
||||
if selectedSpanID == spanID {
|
||||
continue
|
||||
}
|
||||
uncollapsedSpanMap[spanID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
opts := traverseOpts{
|
||||
uncollapsedSpans: uncollapsedSpanMap,
|
||||
selectedSpanID: selectedSpanID,
|
||||
}
|
||||
traversal, autoExpanded := traverseTrace(rootNode, opts, 0, true, 0)
|
||||
for _, spanID := range autoExpanded {
|
||||
uncollapsedSpanMap[spanID] = struct{}{}
|
||||
}
|
||||
idx := findIndexForSelectedSpan(traversal, selectedSpanID)
|
||||
|
||||
if idx != -1 {
|
||||
selectedSpanIndex = idx + len(preOrderTraversal)
|
||||
}
|
||||
|
||||
preOrderTraversal = append(preOrderTraversal, traversal...)
|
||||
|
||||
if rootServiceName == "" {
|
||||
rootServiceName = rootNode.ServiceName
|
||||
}
|
||||
if rootServiceEntryPoint == "" {
|
||||
rootServiceEntryPoint = rootNode.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if selectedSpanIndex == -1 && selectedSpanID != "" {
|
||||
selectedSpanIndex = 0
|
||||
}
|
||||
|
||||
// Window: 40% before, 60% after selected span
|
||||
startIndex := selectedSpanIndex - int(spanLimitPerRequest*0.4)
|
||||
endIndex := selectedSpanIndex + int(spanLimitPerRequest*0.6)
|
||||
|
||||
if startIndex < 0 {
|
||||
endIndex = endIndex - startIndex
|
||||
startIndex = 0
|
||||
}
|
||||
if endIndex > len(preOrderTraversal) {
|
||||
startIndex = startIndex - (endIndex - len(preOrderTraversal))
|
||||
endIndex = len(preOrderTraversal)
|
||||
}
|
||||
if startIndex < 0 {
|
||||
startIndex = 0
|
||||
}
|
||||
|
||||
return preOrderTraversal[startIndex:endIndex], slices.Collect(maps.Keys(uncollapsedSpanMap)), rootServiceName, rootServiceEntryPoint
|
||||
}
|
||||
|
||||
func GetAllSpans(traceRoots []*tracedetailtypes.WaterfallSpan) (spans []*tracedetailtypes.WaterfallSpan, rootServiceName, rootEntryPoint string) {
|
||||
if len(traceRoots) > 0 {
|
||||
rootServiceName = traceRoots[0].ServiceName
|
||||
rootEntryPoint = traceRoots[0].Name
|
||||
}
|
||||
for _, root := range traceRoots {
|
||||
childSpans, _ := traverseTrace(root, traverseOpts{selectAll: true}, 0, true, 0)
|
||||
spans = append(spans, childSpans...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getPathFromRootToSelectedSpanID(node *tracedetailtypes.WaterfallSpan, selectedSpanID string) (bool, []string) {
|
||||
path := []string{node.SpanID}
|
||||
if node.SpanID == selectedSpanID {
|
||||
return true, path
|
||||
}
|
||||
|
||||
for _, child := range node.Children {
|
||||
found, childPath := getPathFromRootToSelectedSpanID(child, selectedSpanID)
|
||||
if found {
|
||||
path = append(path, childPath...)
|
||||
return true, path
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func findIndexForSelectedSpan(spans []*tracedetailtypes.WaterfallSpan, selectedSpanID string) int {
|
||||
for i, span := range spans {
|
||||
if span.SpanID == selectedSpanID {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// SortSpanChildren recursively sorts children of each span by TimeUnixNano then Name.
|
||||
// Must be called once after the span tree is fully built so that traverseTrace
|
||||
// sees a consistent ordering without needing to re-sort on every call.
|
||||
func SortSpanChildren(span *tracedetailtypes.WaterfallSpan) {
|
||||
sort.Slice(span.Children, func(i, j int) bool {
|
||||
if span.Children[i].TimeUnixMilli == span.Children[j].TimeUnixMilli {
|
||||
return span.Children[i].Name < span.Children[j].Name
|
||||
}
|
||||
return span.Children[i].TimeUnixMilli < span.Children[j].TimeUnixMilli
|
||||
})
|
||||
for _, child := range span.Children {
|
||||
SortSpanChildren(child)
|
||||
}
|
||||
}
|
||||
559
pkg/modules/tracedetail/impltracedetail/waterfall_test.go
Normal file
559
pkg/modules/tracedetail/impltracedetail/waterfall_test.go
Normal file
@@ -0,0 +1,559 @@
|
||||
// Package impltracedetail tests — waterfall
|
||||
//
|
||||
// # Background
|
||||
//
|
||||
// The waterfall view renders a trace as a scrollable list of spans in
|
||||
// pre-order (parent before children, siblings left-to-right). Because a trace
|
||||
// can have thousands of spans, only a window of ~500 is returned per request.
|
||||
// The window is centred on the selected span.
|
||||
//
|
||||
// # Key concepts
|
||||
//
|
||||
// uncollapsedSpans
|
||||
//
|
||||
// The set of span IDs the user has manually expanded in the UI.
|
||||
// Only the direct children of an uncollapsed span are included in the
|
||||
// output; grandchildren stay hidden until their parent is also uncollapsed.
|
||||
// When multiple spans are uncollapsed their children are all visible at once.
|
||||
//
|
||||
// selectedSpanID
|
||||
//
|
||||
// The span currently focused — set when the user clicks a span in the
|
||||
// waterfall or selects one from the flamegraph. The output window is always
|
||||
// centred on this span. The path from the trace root down to the selected
|
||||
// span is automatically uncollapsed so ancestors are visible even if they are
|
||||
// not in uncollapsedSpans.
|
||||
//
|
||||
//
|
||||
// traceRoots
|
||||
//
|
||||
// Root spans of the trace — spans with no parent in the current dataset.
|
||||
// Normally one, but multiple roots are common when upstream services are
|
||||
// not instrumented or their spans were not sampled/exported.
|
||||
|
||||
package impltracedetail
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func mkSpan(id, service string, children ...*tracedetailtypes.WaterfallSpan) *tracedetailtypes.WaterfallSpan {
|
||||
return &tracedetailtypes.WaterfallSpan{
|
||||
SpanID: id,
|
||||
ServiceName: service,
|
||||
Name: id + "-op",
|
||||
Children: children,
|
||||
}
|
||||
}
|
||||
|
||||
func spanIDs(spans []*tracedetailtypes.WaterfallSpan) []string {
|
||||
ids := make([]string, len(spans))
|
||||
for i, s := range spans {
|
||||
ids[i] = s.SpanID
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func buildSpanMap(roots ...*tracedetailtypes.WaterfallSpan) map[string]*tracedetailtypes.WaterfallSpan {
|
||||
m := map[string]*tracedetailtypes.WaterfallSpan{}
|
||||
var walk func(*tracedetailtypes.WaterfallSpan)
|
||||
walk = func(s *tracedetailtypes.WaterfallSpan) {
|
||||
m[s.SpanID] = s
|
||||
for _, c := range s.Children {
|
||||
walk(c)
|
||||
}
|
||||
}
|
||||
for _, r := range roots {
|
||||
SortSpanChildren(r)
|
||||
walk(r)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// makeChain builds a linear trace: span0 → span1 → … → span(n-1).
|
||||
func makeChain(n int) (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan, []string) {
|
||||
spans := make([]*tracedetailtypes.WaterfallSpan, n)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
if i == n-1 {
|
||||
spans[i] = mkSpan(fmt.Sprintf("span%d", i), "svc")
|
||||
} else {
|
||||
spans[i] = mkSpan(fmt.Sprintf("span%d", i), "svc", spans[i+1])
|
||||
}
|
||||
}
|
||||
uncollapsed := make([]string, n)
|
||||
for i := range spans {
|
||||
uncollapsed[i] = fmt.Sprintf("span%d", i)
|
||||
}
|
||||
return spans[0], buildSpanMap(spans[0]), uncollapsed
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// GetSelectedSpans — span ordering and visibility
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
buildRoots func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan)
|
||||
uncollapsedSpans []string
|
||||
selectedSpanID string
|
||||
wantSpanIDs []string
|
||||
}{
|
||||
{
|
||||
// Pre-order traversal is preserved: parent before children, siblings left-to-right.
|
||||
name: "pre_order_traversal",
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("child1", "svc", mkSpan("grandchild", "svc")),
|
||||
mkSpan("child2", "svc"),
|
||||
)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"root", "child1"},
|
||||
selectedSpanID: "root",
|
||||
wantSpanIDs: []string{"root", "child1", "grandchild", "child2"},
|
||||
},
|
||||
{
|
||||
// Multiple spans uncollapsed simultaneously: children of all uncollapsed spans are visible at once.
|
||||
//
|
||||
// root
|
||||
// ├─ childA (uncollapsed) → grandchildA ✓
|
||||
// └─ childB (uncollapsed) → grandchildB ✓
|
||||
name: "multiple_uncollapsed",
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("childA", "svc", mkSpan("grandchildA", "svc")),
|
||||
mkSpan("childB", "svc", mkSpan("grandchildB", "svc")),
|
||||
)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"root", "childA", "childB"},
|
||||
selectedSpanID: "root",
|
||||
wantSpanIDs: []string{"root", "childA", "grandchildA", "childB", "grandchildB"},
|
||||
},
|
||||
{
|
||||
// Collapsing a span with other uncollapsed spans.
|
||||
//
|
||||
// root
|
||||
// ├─ childA (previously expanded — in uncollapsedSpans)
|
||||
// │ ├─ grandchild1 ✓
|
||||
// │ │ └─ greatGrandchild ✗ (grandchild1 not in uncollapsedSpans)
|
||||
// │ └─ grandchild2 ✓
|
||||
// └─ childB ← selected (not expanded)
|
||||
name: "manual_uncollapse",
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("childA", "svc",
|
||||
mkSpan("grandchild1", "svc", mkSpan("greatGrandchild", "svc")),
|
||||
mkSpan("grandchild2", "svc"),
|
||||
),
|
||||
mkSpan("childB", "svc"),
|
||||
)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"childA"},
|
||||
selectedSpanID: "childB",
|
||||
wantSpanIDs: []string{"root", "childA", "grandchild1", "grandchild2", "childB"},
|
||||
},
|
||||
{
|
||||
// A collapsed span hides all children.
|
||||
name: "collapsed_span_hides_children",
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("child1", "svc"),
|
||||
mkSpan("child2", "svc"),
|
||||
)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "root",
|
||||
wantSpanIDs: []string{"root"},
|
||||
},
|
||||
{
|
||||
// Selecting a span auto-uncollpases the path from root to that span so it is visible.
|
||||
//
|
||||
// root → parent → selected
|
||||
name: "path_to_selected_is_uncollapsed",
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "selected",
|
||||
wantSpanIDs: []string{"root", "parent", "selected"},
|
||||
},
|
||||
{
|
||||
// Siblings of ancestors are rendered as collapsed nodes but their subtrees must NOT be expanded.
|
||||
//
|
||||
// root
|
||||
// ├─ unrelated → unrelated-child (✗)
|
||||
// └─ parent → selected
|
||||
name: "siblings_not_expanded",
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("unrelated", "svc", mkSpan("unrelated-child", "svc")),
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "selected",
|
||||
// children of root sort alphabetically: parent < unrelated; unrelated-child stays hidden
|
||||
wantSpanIDs: []string{"root", "parent", "selected", "unrelated"},
|
||||
},
|
||||
{
|
||||
// An unknown selectedSpanID must not panic; returns a window from index 0.
|
||||
name: "unknown_selected_span",
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc", mkSpan("child", "svc"))
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "nonexistent",
|
||||
wantSpanIDs: []string{"root"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
roots, spanMap := tc.buildRoots()
|
||||
spans, _, _, _ := GetSelectedSpans(tc.uncollapsedSpans, tc.selectedSpanID, roots, spanMap)
|
||||
assert.Equal(t, tc.wantSpanIDs, spanIDs(spans))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple roots: both trees are flattened into a single pre-order list with
|
||||
// root1's subtree before root2's. Service/entry-point come from the first root.
|
||||
//
|
||||
// root1 svc-a ← selected
|
||||
// └─ child1
|
||||
// root2 svc-b
|
||||
// └─ child2
|
||||
//
|
||||
// Expected output order: root1 → child1 → root2 → child2.
|
||||
func TestGetSelectedSpans_MultipleRoots(t *testing.T) {
|
||||
root1 := mkSpan("root1", "svc-a", mkSpan("child1", "svc-a"))
|
||||
root2 := mkSpan("root2", "svc-b", mkSpan("child2", "svc-b"))
|
||||
spanMap := buildSpanMap(root1, root2)
|
||||
|
||||
spans, _, svcName, entryPoint := GetSelectedSpans([]string{"root1", "root2"}, "root1", []*tracedetailtypes.WaterfallSpan{root1, root2}, spanMap)
|
||||
|
||||
assert.Equal(t, []string{"root1", "child1", "root2", "child2"}, spanIDs(spans), "root1 subtree must precede root2 subtree")
|
||||
assert.Equal(t, "svc-a", svcName, "metadata comes from first root")
|
||||
assert.Equal(t, "root1-op", entryPoint, "metadata comes from first root")
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// GetSelectedSpans — uncollapsed span tracking
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestGetSelectedSpans_UncollapsedTracking(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
buildRoot func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan)
|
||||
uncollapsedSpans []string
|
||||
selectedSpanID string
|
||||
wantSpanIDs []string
|
||||
checkUncollapsed func(t *testing.T, uncollapsed []string)
|
||||
}{
|
||||
{
|
||||
// The path-to-selected spans are returned in updatedUncollapsedSpans.
|
||||
name: "path_returned_in_uncollapsed",
|
||||
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
return root, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "selected",
|
||||
wantSpanIDs: []string{"root", "parent", "selected"},
|
||||
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
|
||||
assert.ElementsMatch(t, []string{"root", "parent"}, uncollapsed)
|
||||
},
|
||||
},
|
||||
{
|
||||
// Siblings of ancestors are not tracked as uncollapsed.
|
||||
name: "siblings_not_in_uncollapsed",
|
||||
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("unrelated", "svc", mkSpan("unrelated-child", "svc")),
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
return root, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "selected",
|
||||
wantSpanIDs: []string{"root", "parent", "selected", "unrelated"},
|
||||
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
|
||||
assert.ElementsMatch(t, []string{"root", "parent"}, uncollapsed)
|
||||
},
|
||||
},
|
||||
{
|
||||
// Auto-expanded span IDs from ALL branches are returned in updatedUncollapsedSpans.
|
||||
// Only internal nodes (spans with children) are tracked — leaf spans are never added.
|
||||
// root is in uncollapsedSpans, so its children are auto-expanded.
|
||||
//
|
||||
// root (selected, expanded)
|
||||
// ├─ childA (internal ✓)
|
||||
// │ └─ grandchildA (internal ✓)
|
||||
// │ └─ leafA (leaf ✗)
|
||||
// └─ childB (internal ✓)
|
||||
// └─ grandchildB (internal ✓)
|
||||
// └─ leafB (leaf ✗)
|
||||
name: "auto_expanded_spans_returned",
|
||||
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("childA", "svc",
|
||||
mkSpan("grandchildA", "svc",
|
||||
mkSpan("leafA", "svc"),
|
||||
),
|
||||
),
|
||||
mkSpan("childB", "svc",
|
||||
mkSpan("grandchildB", "svc",
|
||||
mkSpan("leafB", "svc"),
|
||||
),
|
||||
),
|
||||
)
|
||||
return root, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"root"},
|
||||
selectedSpanID: "root",
|
||||
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
|
||||
assert.Contains(t, uncollapsed, "root")
|
||||
assert.Contains(t, uncollapsed, "childA", "internal node depth 1, branch A")
|
||||
assert.Contains(t, uncollapsed, "childB", "internal node depth 1, branch B")
|
||||
assert.Contains(t, uncollapsed, "grandchildA", "internal node depth 2, branch A")
|
||||
assert.Contains(t, uncollapsed, "grandchildB", "internal node depth 2, branch B")
|
||||
assert.NotContains(t, uncollapsed, "leafA", "leaf spans are never added to uncollapsedSpans")
|
||||
assert.NotContains(t, uncollapsed, "leafB", "leaf spans are never added to uncollapsedSpans")
|
||||
},
|
||||
},
|
||||
{
|
||||
// If the selected span is already in uncollapsedSpans,
|
||||
// it should appear exactly once in the result.
|
||||
name: "duplicate_in_uncollapsed",
|
||||
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("selected", "svc", mkSpan("child", "svc")),
|
||||
)
|
||||
return root, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"selected"}, // already present
|
||||
selectedSpanID: "selected",
|
||||
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
|
||||
count := 0
|
||||
for _, id := range uncollapsed {
|
||||
if id == "selected" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, count, "should appear once")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root, spanMap := tc.buildRoot()
|
||||
spans, uncollapsed, _, _ := GetSelectedSpans(tc.uncollapsedSpans, tc.selectedSpanID, []*tracedetailtypes.WaterfallSpan{root}, spanMap)
|
||||
if tc.wantSpanIDs != nil {
|
||||
assert.Equal(t, tc.wantSpanIDs, spanIDs(spans))
|
||||
}
|
||||
if tc.checkUncollapsed != nil {
|
||||
tc.checkUncollapsed(t, uncollapsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// GetSelectedSpans — span metadata
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Test to check if Level, HasChildren, and SubTreeNodeCount are populated correctly.
|
||||
//
|
||||
// root level=0, hasChildren=true, subTree=4
|
||||
// child1 level=1, hasChildren=true, subTree=2
|
||||
// grandchild level=2, hasChildren=false, subTree=1
|
||||
// child2 level=1, hasChildren=false, subTree=1
|
||||
func TestGetSelectedSpans_SpanMetadata(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("child1", "svc", mkSpan("grandchild", "svc")),
|
||||
mkSpan("child2", "svc"),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
spans, _, _, _ := GetSelectedSpans([]string{"root", "child1"}, "root", []*tracedetailtypes.WaterfallSpan{root}, spanMap)
|
||||
|
||||
byID := map[string]*tracedetailtypes.WaterfallSpan{}
|
||||
for _, s := range spans {
|
||||
byID[s.SpanID] = s
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
spanID string
|
||||
wantLevel uint64
|
||||
wantHasChildren bool
|
||||
wantSubTree uint64
|
||||
}{
|
||||
{"root", 0, true, 4},
|
||||
{"child1", 1, true, 2},
|
||||
{"child2", 1, false, 1},
|
||||
{"grandchild", 2, false, 1},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.spanID, func(t *testing.T) {
|
||||
s := byID[tc.spanID]
|
||||
assert.Equal(t, tc.wantLevel, s.Level)
|
||||
assert.Equal(t, tc.wantHasChildren, s.HasChildren)
|
||||
assert.Equal(t, tc.wantSubTree, s.SubTreeNodeCount)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// GetSelectedSpans — windowing
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestGetSelectedSpans_Window(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
selectedSpanID string
|
||||
wantLen int
|
||||
wantFirst string
|
||||
wantLast string
|
||||
wantSelectedPos int
|
||||
}{
|
||||
{
|
||||
// The selected span is centred: 200 spans before it, 300 after (0.4 / 0.6 split).
|
||||
name: "centred_on_selected",
|
||||
selectedSpanID: "span300",
|
||||
wantLen: 500,
|
||||
wantFirst: "span100",
|
||||
wantLast: "span599",
|
||||
wantSelectedPos: 200,
|
||||
},
|
||||
{
|
||||
// When the selected span is near the start, the window shifts right so no
|
||||
// negative index is used — the result is still 500 spans.
|
||||
name: "shifts_at_start",
|
||||
selectedSpanID: "span10",
|
||||
wantLen: 500,
|
||||
wantFirst: "span0",
|
||||
wantSelectedPos: 10,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root, spanMap, uncollapsed := makeChain(600)
|
||||
spans, _, _, _ := GetSelectedSpans(uncollapsed, tc.selectedSpanID, []*tracedetailtypes.WaterfallSpan{root}, spanMap)
|
||||
|
||||
assert.Equal(t, tc.wantLen, len(spans), "window size")
|
||||
assert.Equal(t, tc.wantFirst, spans[0].SpanID, "first span in window")
|
||||
if tc.wantLast != "" {
|
||||
assert.Equal(t, tc.wantLast, spans[len(spans)-1].SpanID, "last span in window")
|
||||
}
|
||||
assert.Equal(t, tc.selectedSpanID, spans[tc.wantSelectedPos].SpanID, "selected span position")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// GetSelectedSpans — depth limit
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Depth is measured from the selected span, not the trace root.
|
||||
// Ancestors appear via the path-to-root logic, not the depth limit.
|
||||
// Each depth level has two children to confirm the limit is enforced on all
|
||||
// branches, not just the first.
|
||||
//
|
||||
// root
|
||||
// └─ A ancestor ✓ (path-to-root)
|
||||
// └─ selected
|
||||
// ├─ d1a depth 1 ✓
|
||||
// │ ├─ d2a depth 2 ✓
|
||||
// │ │ ├─ d3a depth 3 ✓
|
||||
// │ │ │ ├─ d4a depth 4 ✓
|
||||
// │ │ │ │ ├─ d5a depth 5 ✓
|
||||
// │ │ │ │ │ └─ d6a depth 6 ✗
|
||||
// │ │ │ │ └─ d5b depth 5 ✓
|
||||
// │ │ │ └─ d4b depth 4 ✓
|
||||
// │ │ └─ d3b depth 3 ✓
|
||||
// │ └─ d2b depth 2 ✓
|
||||
// └─ d1b depth 1 ✓
|
||||
func TestGetSelectedSpans_DepthCountedFromSelectedSpan(t *testing.T) {
|
||||
selected := mkSpan("selected", "svc",
|
||||
mkSpan("d1a", "svc",
|
||||
mkSpan("d2a", "svc",
|
||||
mkSpan("d3a", "svc",
|
||||
mkSpan("d4a", "svc",
|
||||
mkSpan("d5a", "svc",
|
||||
mkSpan("d6a", "svc"), // depth 6 — excluded
|
||||
),
|
||||
mkSpan("d5b", "svc"), // depth 5 — included
|
||||
),
|
||||
mkSpan("d4b", "svc"), // depth 4 — included
|
||||
),
|
||||
mkSpan("d3b", "svc"), // depth 3 — included
|
||||
),
|
||||
mkSpan("d2b", "svc"), // depth 2 — included
|
||||
),
|
||||
mkSpan("d1b", "svc"), // depth 1 — included
|
||||
)
|
||||
root := mkSpan("root", "svc", mkSpan("A", "svc", selected))
|
||||
|
||||
spanMap := buildSpanMap(root)
|
||||
spans, _, _, _ := GetSelectedSpans([]string{"selected"}, "selected", []*tracedetailtypes.WaterfallSpan{root}, spanMap)
|
||||
ids := spanIDs(spans)
|
||||
|
||||
assert.Contains(t, ids, "root", "ancestor shown via path-to-root")
|
||||
assert.Contains(t, ids, "A", "ancestor shown via path-to-root")
|
||||
for _, id := range []string{"d1a", "d1b", "d2a", "d2b", "d3a", "d3b", "d4a", "d4b", "d5a", "d5b"} {
|
||||
assert.Contains(t, ids, id, "depth ≤ 5 — must be included")
|
||||
}
|
||||
assert.NotContains(t, ids, "d6a", "depth 6 > limit — excluded")
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// GetAllSpans
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestGetAllSpans(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("childA", "svc",
|
||||
mkSpan("grandchildA", "svc",
|
||||
mkSpan("leafA", "svc2"),
|
||||
),
|
||||
),
|
||||
mkSpan("childB", "svc3",
|
||||
mkSpan("grandchildB", "svc",
|
||||
mkSpan("leafB", "svc2"),
|
||||
),
|
||||
),
|
||||
)
|
||||
spans, rootServiceName, rootEntryPoint := GetAllSpans([]*tracedetailtypes.WaterfallSpan{root})
|
||||
assert.ElementsMatch(t, spanIDs(spans), []string{"root", "childA", "grandchildA", "leafA", "childB", "grandchildB", "leafB"})
|
||||
assert.Equal(t, "svc", rootServiceName)
|
||||
assert.Equal(t, "root-op", rootEntryPoint)
|
||||
}
|
||||
@@ -34,6 +34,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/services/implservices"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
@@ -62,6 +64,7 @@ type Handlers struct {
|
||||
RegistryHandler factory.Handler
|
||||
CloudIntegrationHandler cloudintegration.Handler
|
||||
RuleStateHistory rulestatehistory.Handler
|
||||
TraceDetail tracedetail.Handler
|
||||
}
|
||||
|
||||
func NewHandlers(
|
||||
@@ -99,5 +102,6 @@ func NewHandlers(
|
||||
RegistryHandler: registryHandler,
|
||||
CloudIntegrationHandler: implcloudintegration.NewHandler(),
|
||||
RuleStateHistory: implrulestatehistory.NewHandler(modules.RuleStateHistory),
|
||||
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/session/implsession"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
@@ -72,6 +74,7 @@ type Modules struct {
|
||||
Promote promote.Module
|
||||
ServiceAccount serviceaccount.Module
|
||||
RuleStateHistory rulestatehistory.Module
|
||||
TraceDetail tracedetail.Module
|
||||
}
|
||||
|
||||
func NewModules(
|
||||
@@ -119,5 +122,6 @@ func NewModules(
|
||||
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
|
||||
ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, cache, analytics, providerSettings, config.ServiceAccount),
|
||||
RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger)),
|
||||
TraceDetail: impltracedetail.NewModule(telemetryStore, cache, providerSettings),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
@@ -69,6 +70,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ factory.Handler }{},
|
||||
struct{ cloudintegration.Handler }{},
|
||||
struct{ rulestatehistory.Handler }{},
|
||||
struct{ tracedetail.Handler }{},
|
||||
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -288,6 +288,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.RegistryHandler,
|
||||
handlers.CloudIntegrationHandler,
|
||||
handlers.RuleStateHistory,
|
||||
handlers.TraceDetail,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@ type GettableServicesMetadata struct {
|
||||
Services []*ServiceMetadata `json:"services" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
type ListServicesMetadataParams struct {
|
||||
CloudIntegrationID valuer.UUID `query:"cloud_integration_id" required:"false"`
|
||||
}
|
||||
|
||||
// Service represents a cloud integration service with its definition,
|
||||
// cloud integration service is non nil only when the service entry exists in DB with ANY config (enabled or disabled).
|
||||
type Service struct {
|
||||
@@ -69,6 +73,10 @@ type Service struct {
|
||||
CloudIntegrationService *CloudIntegrationService `json:"cloudIntegrationService" required:"true" nullable:"true"`
|
||||
}
|
||||
|
||||
type GetServiceParams struct {
|
||||
CloudIntegrationID valuer.UUID `query:"cloud_integration_id" required:"false"`
|
||||
}
|
||||
|
||||
type UpdatableService struct {
|
||||
Config *ServiceConfig `json:"config" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ type WaterfallRequest struct {
|
||||
type WaterfallResponse struct {
|
||||
StartTimestampMillis uint64 `json:"startTimestampMillis"`
|
||||
EndTimestampMillis uint64 `json:"endTimestampMillis"`
|
||||
DurationNano uint64 `json:"durationNano"`
|
||||
RootServiceName string `json:"rootServiceName"`
|
||||
RootServiceEntryPoint string `json:"rootServiceEntryPoint"`
|
||||
TotalSpansCount uint64 `json:"totalSpansCount"`
|
||||
@@ -28,6 +27,7 @@ type WaterfallResponse struct {
|
||||
Spans []*WaterfallSpan `json:"spans"`
|
||||
HasMissingSpans bool `json:"hasMissingSpans"`
|
||||
UncollapsedSpans []string `json:"uncollapsedSpans"`
|
||||
HasMore bool `json:"hasMore"`
|
||||
}
|
||||
|
||||
// Event represents a span event.
|
||||
@@ -58,7 +58,6 @@ type WaterfallSpan struct {
|
||||
IsRemote string `json:"is_remote"`
|
||||
Kind int32 `json:"kind"`
|
||||
KindString string `json:"kind_string"`
|
||||
Links string `json:"links"`
|
||||
Name string `json:"name"`
|
||||
ParentSpanID string `json:"parent_span_id"`
|
||||
Resource map[string]string `json:"resource"`
|
||||
@@ -67,7 +66,7 @@ type WaterfallSpan struct {
|
||||
StatusCode int32 `json:"status_code"`
|
||||
StatusCodeString string `json:"status_code_string"`
|
||||
StatusMessage string `json:"status_message"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
TimeUnixMilli uint64 `json:"timestamp"`
|
||||
TraceID string `json:"trace_id"`
|
||||
TraceState string `json:"trace_state"`
|
||||
|
||||
@@ -77,10 +76,7 @@ type WaterfallSpan struct {
|
||||
HasChildren bool `json:"has_children"`
|
||||
Level uint64 `json:"level"`
|
||||
|
||||
// timeUnixNano is an internal field used for tree building and sorting.
|
||||
// It is not serialized in the JSON response.
|
||||
TimeUnixNano uint64 `json:"-"`
|
||||
// serviceName is an internal field used for service time calculation.
|
||||
// used only for service time calculation
|
||||
ServiceName string `json:"-"`
|
||||
}
|
||||
|
||||
@@ -149,7 +145,7 @@ func (item *SpanModel) ToSpan() *WaterfallSpan {
|
||||
for _, eventStr := range item.Events {
|
||||
var event Event
|
||||
if err := json.Unmarshal([]byte(eventStr), &event); err != nil {
|
||||
continue
|
||||
continue // skipping malformed events
|
||||
}
|
||||
events = append(events, event)
|
||||
}
|
||||
@@ -170,7 +166,6 @@ func (item *SpanModel) ToSpan() *WaterfallSpan {
|
||||
IsRemote: item.IsRemote,
|
||||
Kind: int32(item.Kind),
|
||||
KindString: item.SpanKind,
|
||||
Links: item.References,
|
||||
Name: item.Name,
|
||||
ParentSpanID: item.ParentSpanID,
|
||||
Resource: resources,
|
||||
@@ -179,11 +174,10 @@ func (item *SpanModel) ToSpan() *WaterfallSpan {
|
||||
StatusCode: item.StatusCode,
|
||||
StatusCodeString: item.StatusCodeString,
|
||||
StatusMessage: item.StatusMessage,
|
||||
Timestamp: item.TimeUnixNano.Format(time.RFC3339Nano),
|
||||
TraceID: item.TraceID,
|
||||
TraceState: item.TraceState,
|
||||
Children: make([]*WaterfallSpan, 0),
|
||||
TimeUnixNano: uint64(item.TimeUnixNano.UnixNano()),
|
||||
TimeUnixMilli: uint64(item.TimeUnixNano.UnixNano() / 1000_000),
|
||||
ServiceName: item.ServiceName,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user