Compare commits
9 Commits
main
...
v0.57.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b3839dca9 | ||
|
|
767d7fc513 | ||
|
|
58f3ab377b | ||
|
|
8da1817061 | ||
|
|
d1205953c7 | ||
|
|
a846caccd9 | ||
|
|
9ac5e48ac2 | ||
|
|
fc9219b0ab | ||
|
|
f4cbe854b1 |
@@ -159,6 +159,7 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
router.HandleFunc("/api/v1/register", am.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(ah.searchTraces)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/traces/{traceId}", am.ViewAccess(ah.searchTracesV2)).Methods(http.MethodPost)
|
||||
|
||||
// PAT APIs
|
||||
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)
|
||||
|
||||
@@ -31,3 +31,19 @@ func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) {
|
||||
ah.WriteJSON(w, r, result)
|
||||
|
||||
}
|
||||
|
||||
func (ah *APIHandler) searchTracesV2(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
searchTracesParams, err := baseapp.ParseSearchTracesV2Params(r)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading params")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := ah.opts.DataConnector.SearchTracesV2(r.Context(), searchTracesParams)
|
||||
if ah.HandleError(w, err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
ah.WriteJSON(w, r, result)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ export const TraceFilter = Loadable(
|
||||
);
|
||||
|
||||
export const TraceDetail = Loadable(
|
||||
() => import(/* webpackChunkName: "TraceDetail Page" */ 'pages/TraceDetail'),
|
||||
() =>
|
||||
import(/* webpackChunkName: "TraceDetail Page" */ 'pages/TraceDetailsV2'),
|
||||
);
|
||||
|
||||
export const UsageExplorerPage = Loadable(
|
||||
|
||||
33
frontend/src/api/trace/getTraceDetails.ts
Normal file
33
frontend/src/api/trace/getTraceDetails.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ApiV2Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
GetTraceDetailsProps,
|
||||
GetTraceDetailsSuccessResponse,
|
||||
} from 'types/api/trace/getTraceDetails';
|
||||
|
||||
const getTraceDetails = async (
|
||||
props: GetTraceDetailsProps,
|
||||
): Promise<SuccessResponse<GetTraceDetailsSuccessResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await ApiV2Instance.post<GetTraceDetailsSuccessResponse>(
|
||||
`/traces/${props.traceID}`,
|
||||
{
|
||||
spanId: props.spanID,
|
||||
uncollapsedNodes: props.uncollapsedNodes,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getTraceDetails;
|
||||
9
frontend/src/container/TimelineV2/TimelineV2.tsx
Normal file
9
frontend/src/container/TimelineV2/TimelineV2.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import './TimelineV2.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
|
||||
function TimelineV2(): JSX.Element {
|
||||
return <Typography.Text>Timeline V2</Typography.Text>;
|
||||
}
|
||||
|
||||
export default TimelineV2;
|
||||
194
frontend/src/container/TraceDetailV2/TraceDetailV2.tsx
Normal file
194
frontend/src/container/TraceDetailV2/TraceDetailV2.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import './TraceDetailsV2.styles.scss';
|
||||
|
||||
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
|
||||
import { Typography } from 'antd';
|
||||
import TimelineV2 from 'container/TimelineV2/TimelineV2';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { ListRange, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
import {
|
||||
GetTraceDetailsSuccessResponse,
|
||||
SpanItem,
|
||||
} from 'types/api/trace/getTraceDetails';
|
||||
|
||||
import { LEFT_COL_WIDTH } from './constants';
|
||||
|
||||
interface ITraceDetailV2Props {
|
||||
uncollapsedNodes: string[];
|
||||
spanID: string;
|
||||
setSpanID: React.Dispatch<React.SetStateAction<string>>;
|
||||
setUncollapsedNodes: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
traceDetailsResponse?: GetTraceDetailsSuccessResponse;
|
||||
}
|
||||
|
||||
function getSpanItemRenderer(
|
||||
index: number,
|
||||
data: SpanItem,
|
||||
uncollapsedNodes: string[],
|
||||
setUncollapsedNodes: React.Dispatch<React.SetStateAction<string[]>>,
|
||||
setSpanID: React.Dispatch<React.SetStateAction<string>>,
|
||||
startTimeTraceTimeline: number,
|
||||
endTimeTraceTimeline: number,
|
||||
): JSX.Element {
|
||||
// this is the total duration of the trace
|
||||
const baseSpread = endTimeTraceTimeline - startTimeTraceTimeline;
|
||||
const currentSpanShare = (data.durationNano / (baseSpread * 1000000)) * 100;
|
||||
const currentSpanLeftOffsert =
|
||||
((data.timestamp * 1000 - startTimeTraceTimeline) / baseSpread) * 100;
|
||||
|
||||
function handleOnCollapseExpand(collapse: boolean): void {
|
||||
if (collapse) {
|
||||
setUncollapsedNodes((prev) => prev.filter((id) => id !== data.spanID));
|
||||
} else {
|
||||
setUncollapsedNodes((prev) => [...prev, data.spanID]);
|
||||
}
|
||||
setSpanID(data.spanID);
|
||||
}
|
||||
return (
|
||||
<div key={index} className="span-container">
|
||||
<section
|
||||
className="span-container-details-section"
|
||||
style={{ width: `${LEFT_COL_WIDTH}px`, paddingLeft: `${data.level * 5}px` }}
|
||||
>
|
||||
<div className="span-header-row">
|
||||
{data.childrenCount > 0 && (
|
||||
<div className="span-count-collapse">
|
||||
<Typography.Text>{data.childrenCount}</Typography.Text>
|
||||
{uncollapsedNodes.includes(data.spanID) ? (
|
||||
<CaretDownFilled
|
||||
size={14}
|
||||
className="collapse-uncollapse-icon"
|
||||
onClick={(): void => handleOnCollapseExpand(true)}
|
||||
/>
|
||||
) : (
|
||||
<CaretRightFilled
|
||||
size={14}
|
||||
className="collapse-uncollapse-icon"
|
||||
onClick={(): void => handleOnCollapseExpand(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="span-description">
|
||||
<Typography.Text className="span-name">{data.name}</Typography.Text>
|
||||
<Typography.Text className="span-service-name">
|
||||
{data.serviceName}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</section>
|
||||
<section className="span-container-duration-section">
|
||||
<div
|
||||
style={{
|
||||
width: `${currentSpanShare}%`,
|
||||
left: `${currentSpanLeftOffsert}%`,
|
||||
border: '1px solid white',
|
||||
position: 'relative',
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TraceDetailV2(props: ITraceDetailV2Props): JSX.Element {
|
||||
const {
|
||||
traceDetailsResponse,
|
||||
setUncollapsedNodes,
|
||||
setSpanID,
|
||||
spanID,
|
||||
uncollapsedNodes,
|
||||
} = props;
|
||||
const isInitialLoad = useRef(true);
|
||||
const handleEndReached = (index: number): void => {
|
||||
if (traceDetailsResponse?.spans?.[index]?.spanID)
|
||||
setSpanID(traceDetailsResponse.spans[index].spanID);
|
||||
};
|
||||
const handleRangeChanged = (range: ListRange): void => {
|
||||
const { startIndex } = range;
|
||||
|
||||
// Only trigger the function after the initial load
|
||||
if (isInitialLoad.current && startIndex > 0) {
|
||||
isInitialLoad.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!isInitialLoad.current &&
|
||||
startIndex === 0 &&
|
||||
traceDetailsResponse?.spans?.[0]?.spanID
|
||||
) {
|
||||
setSpanID(traceDetailsResponse.spans[0].spanID);
|
||||
}
|
||||
};
|
||||
|
||||
const ref = useRef<VirtuosoHandle>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (traceDetailsResponse?.spans && traceDetailsResponse.spans.length > 0) {
|
||||
const currentInterestedSpanIndex =
|
||||
traceDetailsResponse?.spans?.findIndex((val) => val.spanID === spanID) || 0;
|
||||
|
||||
setTimeout(() => {
|
||||
ref.current?.scrollToIndex({
|
||||
index: currentInterestedSpanIndex,
|
||||
behavior: 'auto',
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
}, [spanID, traceDetailsResponse?.spans]);
|
||||
|
||||
return (
|
||||
<div className="trace-details-v2-container">
|
||||
<section className="trace-details-v2-flame-graph">
|
||||
<div
|
||||
className="trace-details-metadata"
|
||||
style={{ width: `${LEFT_COL_WIDTH}px` }}
|
||||
>
|
||||
<Typography.Text className="spans-count">Total Spans</Typography.Text>
|
||||
<Typography.Text>{traceDetailsResponse?.totalSpans || 0}</Typography.Text>
|
||||
</div>
|
||||
<div className="trace-details-flame-graph">
|
||||
<Typography.Text>Flame graph comes here...</Typography.Text>
|
||||
</div>
|
||||
</section>
|
||||
<section className="timeline-graph">
|
||||
<Typography.Text
|
||||
className="global-start-time-marker"
|
||||
style={{ width: `${LEFT_COL_WIDTH}px` }}
|
||||
>
|
||||
{dayjs(traceDetailsResponse?.startTimestampMillis).format(
|
||||
'hh:mm:ss a MM/DD',
|
||||
)}
|
||||
</Typography.Text>
|
||||
<TimelineV2 />
|
||||
</section>
|
||||
<section className="trace-details-v2-waterfall-model">
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
rangeChanged={handleRangeChanged}
|
||||
data={traceDetailsResponse?.spans || []}
|
||||
endReached={handleEndReached}
|
||||
itemContent={(index, data): React.ReactNode =>
|
||||
getSpanItemRenderer(
|
||||
index,
|
||||
data,
|
||||
uncollapsedNodes,
|
||||
setUncollapsedNodes,
|
||||
setSpanID,
|
||||
traceDetailsResponse?.startTimestampMillis || 0,
|
||||
traceDetailsResponse?.endTimestampMillis || 0,
|
||||
)
|
||||
}
|
||||
className="trace-details-v2-span-area"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TraceDetailV2.defaultProps = {
|
||||
traceDetailsResponse: {},
|
||||
};
|
||||
|
||||
export default TraceDetailV2;
|
||||
132
frontend/src/container/TraceDetailV2/TraceDetailsV2.styles.scss
Normal file
132
frontend/src/container/TraceDetailV2/TraceDetailsV2.styles.scss
Normal file
@@ -0,0 +1,132 @@
|
||||
.trace-details-v2-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 10px;
|
||||
|
||||
.trace-details-v2-flame-graph {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
|
||||
.trace-details-metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
|
||||
.spans-count {
|
||||
color: var(--bg-vanilla-200);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 12px 18px 6px 14px;
|
||||
}
|
||||
}
|
||||
.trace-details-flame-graph {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-graph {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
|
||||
.global-start-time-marker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bg-vanilla-200);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 12px 18px 6px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-details-v2-waterfall-model {
|
||||
.trace-details-v2-span-area {
|
||||
height: 80vh !important;
|
||||
|
||||
.span-container {
|
||||
display: flex;
|
||||
gap: 25px;
|
||||
padding-top: 1rem;
|
||||
|
||||
.span-container-details-section {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
align-items: flex-start;
|
||||
cursor: pointer;
|
||||
|
||||
.span-header-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
.span-count-collapse {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border: 1px solid var(--bg-slate-200);
|
||||
padding: 1px 8px;
|
||||
|
||||
.collapse-uncollapse-icon {
|
||||
color: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.span-description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
.span-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bg-vanilla-200);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
}
|
||||
|
||||
.span-service-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgb(172, 172, 172);
|
||||
font-family: Inter;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.span-container-duration-section {
|
||||
width: 100%;
|
||||
.span-bar {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
frontend/src/container/TraceDetailV2/constants.ts
Normal file
1
frontend/src/container/TraceDetailV2/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const LEFT_COL_WIDTH = 320;
|
||||
45
frontend/src/pages/TraceDetailsV2/index.tsx
Normal file
45
frontend/src/pages/TraceDetailsV2/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import getTraceDetails from 'api/trace/getTraceDetails';
|
||||
import TraceDetailV2 from 'container/TraceDetailV2/TraceDetailV2';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { TraceDetailsProps } from 'types/api/trace/getTraceDetails';
|
||||
|
||||
function TraceDetailsV2(): JSX.Element {
|
||||
const { id: traceID } = useParams<TraceDetailsProps>();
|
||||
const urlQuery = useUrlQuery();
|
||||
const [spanID, setSpanID] = useState<string>(urlQuery.get('spanId') || '');
|
||||
const [uncollapsedNodes, setUncollapsedNodes] = useState<string[]>([]);
|
||||
|
||||
const { data: spansData } = useQuery({
|
||||
queryFn: () =>
|
||||
getTraceDetails({
|
||||
traceID,
|
||||
spanID,
|
||||
uncollapsedNodes,
|
||||
}),
|
||||
queryKey: [spanID, traceID, ...uncollapsedNodes],
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (uncollapsedNodes.length === 0 && spansData?.payload?.uncollapsedNodes) {
|
||||
setUncollapsedNodes(spansData?.payload?.uncollapsedNodes);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [spansData]);
|
||||
|
||||
return (
|
||||
<TraceDetailV2
|
||||
traceDetailsResponse={defaultTo(spansData?.payload, undefined)}
|
||||
uncollapsedNodes={uncollapsedNodes}
|
||||
setUncollapsedNodes={setUncollapsedNodes}
|
||||
spanID={spanID}
|
||||
setSpanID={setSpanID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default TraceDetailsV2;
|
||||
36
frontend/src/types/api/trace/getTraceDetails.ts
Normal file
36
frontend/src/types/api/trace/getTraceDetails.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export interface TraceDetailsProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface GetTraceDetailsProps {
|
||||
traceID: string;
|
||||
spanID: string;
|
||||
uncollapsedNodes: string[];
|
||||
}
|
||||
|
||||
interface OtelSpanRef {
|
||||
traceId: string;
|
||||
spanId: string;
|
||||
refType: string;
|
||||
}
|
||||
|
||||
export interface SpanItem {
|
||||
timestamp: number;
|
||||
traceID: string;
|
||||
spanID: string;
|
||||
parentSpanID: string;
|
||||
name: string;
|
||||
serviceName: string;
|
||||
durationNano: number;
|
||||
references: OtelSpanRef[];
|
||||
level: number;
|
||||
childrenCount: number;
|
||||
}
|
||||
|
||||
export interface GetTraceDetailsSuccessResponse {
|
||||
totalSpans: number;
|
||||
startTimestampMillis: number;
|
||||
endTimestampMillis: number;
|
||||
uncollapsedNodes: string[];
|
||||
spans: SpanItem[];
|
||||
}
|
||||
@@ -1666,6 +1666,219 @@ func (r *ClickHouseReader) GetUsage(ctx context.Context, queryParams *model.GetU
|
||||
return &usageItems, nil
|
||||
}
|
||||
|
||||
func includes(targetSlice []string, targetElement string) bool {
|
||||
for _, value := range targetSlice {
|
||||
if value == targetElement {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getPreOrderTraversal(rootSpan *model.SpanNode, uncollapsedNodes []string, level int64) []model.SpanNode {
|
||||
preOrderTraversal := []model.SpanNode{{
|
||||
Timestamp: rootSpan.Timestamp,
|
||||
TraceID: rootSpan.TraceID,
|
||||
SpanID: rootSpan.SpanID,
|
||||
ParentSpanID: rootSpan.ParentSpanID,
|
||||
Name: rootSpan.Name,
|
||||
ServiceName: rootSpan.ServiceName,
|
||||
DurationNano: rootSpan.DurationNano,
|
||||
Level: level,
|
||||
ChildrenCount: int64(len(rootSpan.Children)),
|
||||
}}
|
||||
|
||||
if includes(uncollapsedNodes, rootSpan.SpanID) {
|
||||
for _, children := range rootSpan.Children {
|
||||
childTraversal := getPreOrderTraversal(children, uncollapsedNodes, level+1)
|
||||
preOrderTraversal = append(preOrderTraversal, childTraversal...)
|
||||
}
|
||||
}
|
||||
|
||||
return preOrderTraversal
|
||||
}
|
||||
|
||||
func getPathFromRoot(rootSpan *model.SpanNode, targetSpanId string) ([]string, bool) {
|
||||
path := []string{}
|
||||
|
||||
// if the current span is the target span then push it to the path and return
|
||||
if rootSpan.SpanID == targetSpanId {
|
||||
path = append(path, targetSpanId)
|
||||
return path, true
|
||||
}
|
||||
|
||||
// recursively check the children for the path
|
||||
for _, children := range rootSpan.Children {
|
||||
childPath, found := getPathFromRoot(children, targetSpanId)
|
||||
// if path is found in the children node then push this to the current path
|
||||
if found {
|
||||
path = append(path, childPath...)
|
||||
}
|
||||
}
|
||||
|
||||
// if the target span is not in this subtree then return false
|
||||
if len(path) == 0 {
|
||||
return path, false
|
||||
}
|
||||
|
||||
// else if found in some child tree then push the current span id to the path and return
|
||||
path = append(path, rootSpan.SpanID)
|
||||
return path, true
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) SearchTracesV2(ctx context.Context, params *model.SearchTracesV2Params) (*model.SearchTracesV2Result, error) {
|
||||
// todo[@vikrantgupta25]: if this is specifically required or not ? calculate the number of spans here and if less than let's say 10k then return the entire response to the client without any manipulations
|
||||
|
||||
var startTime, endTime, durationNano uint64
|
||||
// get all the spans from the clickhouse based on traceID
|
||||
var spans []model.SearchSpanDBV2ResponseItem
|
||||
query := fmt.Sprintf(`SELECT timestamp, traceID, spanID, parentSpanID, serviceName, name, durationNano, references FROM %s.%s WHERE traceID=$1 ORDER BY timestamp;`, r.TraceDB, r.indexTable)
|
||||
err := r.db.Select(ctx, &spans, query, params.TraceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in processing sql query: %w", err)
|
||||
}
|
||||
|
||||
// create a spanID to spanNode map for tree construction
|
||||
spanIDNodeMap := map[string]*model.SpanNode{}
|
||||
for _, span := range spans {
|
||||
spanNode := model.SpanNode{
|
||||
Timestamp: uint64(span.Timestamp.Unix()),
|
||||
TraceID: span.TraceID,
|
||||
SpanID: span.SpanID,
|
||||
ParentSpanID: span.ParentSpanID,
|
||||
ServiceName: span.ServiceName,
|
||||
Name: span.Name,
|
||||
DurationNano: span.DurationNano,
|
||||
}
|
||||
var references []model.OtelSpanRef
|
||||
err = json.Unmarshal([]byte(span.References), &references)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in processing span references %w", err)
|
||||
}
|
||||
spanNode.References = references
|
||||
|
||||
spanIDNodeMap[span.SpanID] = &spanNode
|
||||
|
||||
if startTime == 0 || startTime > uint64(span.Timestamp.UnixNano()/1000000) {
|
||||
startTime = uint64(span.Timestamp.UnixNano() / 1000000)
|
||||
}
|
||||
|
||||
if endTime == 0 || endTime < uint64(span.Timestamp.UnixNano()/1000000) {
|
||||
endTime = uint64(span.Timestamp.UnixNano() / 1000000)
|
||||
}
|
||||
if durationNano == 0 || uint64(spanNode.DurationNano) > durationNano {
|
||||
durationNano = uint64(spanNode.DurationNano)
|
||||
}
|
||||
}
|
||||
|
||||
var traceRoots []*model.SpanNode
|
||||
// create the tree from the spans array using the above spanID => spanNode map structure
|
||||
for _, span := range spans {
|
||||
spanNode := spanIDNodeMap[span.SpanID]
|
||||
if span.ParentSpanID == "" {
|
||||
traceRoots = append(traceRoots, spanNode)
|
||||
continue
|
||||
}
|
||||
ok, seen := spanIDNodeMap[span.ParentSpanID]
|
||||
// if the parentSpanID is present for the current span
|
||||
if seen {
|
||||
// mark the span as processed true to schedule for removal in the next step
|
||||
spanNode.IsProcessed = true
|
||||
ok.Children = append(ok.Children, spanNode)
|
||||
} else {
|
||||
// insert a missing span for the parent with base attributes
|
||||
missingSpan := model.SpanNode{
|
||||
TraceID: span.TraceID,
|
||||
SpanID: span.ParentSpanID,
|
||||
Name: "Missing Span",
|
||||
}
|
||||
spanNode.IsProcessed = true
|
||||
// insert the current span as the child of the missing span
|
||||
missingSpan.Children = append(missingSpan.Children, spanNode)
|
||||
spanIDNodeMap[span.ParentSpanID] = &missingSpan
|
||||
}
|
||||
}
|
||||
|
||||
// for _, span := range spanIDNodeMap {
|
||||
// if !span.IsProcessed {
|
||||
// traceRoots = append(traceRoots, span)
|
||||
// }
|
||||
// }
|
||||
|
||||
// todo[@vikrantgupta25]: check what to do in this case ?
|
||||
if len(traceRoots) > 1 {
|
||||
return nil, fmt.Errorf("more than one root spans found in a single trace")
|
||||
}
|
||||
|
||||
// get the path from the root of the tree to current spanId and mark them as uncollapsed nodes only if the uncollapsedNodes length is zero
|
||||
var pathFromRootToCurrentSpanID []string
|
||||
var found bool
|
||||
uniqueNodes := make(map[string]bool)
|
||||
// only get the path from root to the current node if the length of uncollapsedNodes is zero i.e. base case in which the frontend
|
||||
// will rely on the uncollapsed nodes being sent by the query-service
|
||||
if len(params.UncollapsedNodes) == 0 {
|
||||
pathFromRootToCurrentSpanID, found = getPathFromRoot(traceRoots[0], params.SpanID)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("current span id is not found in the trace tree")
|
||||
}
|
||||
}
|
||||
|
||||
for _, node := range pathFromRootToCurrentSpanID {
|
||||
uniqueNodes[node] = true
|
||||
}
|
||||
for _, node := range params.UncollapsedNodes {
|
||||
uniqueNodes[node] = true
|
||||
}
|
||||
mergedUncollapsedNodes := make([]string, 0, len(uniqueNodes))
|
||||
for node := range uniqueNodes {
|
||||
mergedUncollapsedNodes = append(mergedUncollapsedNodes, node)
|
||||
}
|
||||
|
||||
// get the traversal for the trace tree
|
||||
preOrderTraversal := getPreOrderTraversal(traceRoots[0], mergedUncollapsedNodes, 0)
|
||||
|
||||
// now based on the spanID of interest send the window containing the spanID
|
||||
spanIndex := -1
|
||||
for i, span := range preOrderTraversal {
|
||||
if span.SpanID == params.SpanID {
|
||||
spanIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if spanIndex == -1 {
|
||||
return nil, fmt.Errorf("spanID not found in preOrderTraversal")
|
||||
}
|
||||
// windowing based on 200 spans before the current one and 300 elements after
|
||||
start := spanIndex - 200
|
||||
end := spanIndex + 300
|
||||
// if there aren't 200 spans before the current one then move the window towards the right
|
||||
if start < 0 {
|
||||
end = end - start
|
||||
start = 0
|
||||
}
|
||||
// if there aren't 300 + x (from the above if) on the right side then move the window left
|
||||
if end > len(preOrderTraversal) {
|
||||
start = start - (end - len(preOrderTraversal))
|
||||
end = len(preOrderTraversal)
|
||||
}
|
||||
// if can't adjust 500 then return all!
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
selectedSpans := preOrderTraversal[start:end]
|
||||
|
||||
responseItem := model.SearchTracesV2Result{
|
||||
Spans: selectedSpans,
|
||||
UncollapsedNodes: mergedUncollapsedNodes,
|
||||
StartTimestampMillis: startTime - (durationNano / 1000000),
|
||||
EndTimestampMillis: endTime + (durationNano / 1000000),
|
||||
TotalSpans: int64(len(spans)),
|
||||
}
|
||||
|
||||
return &responseItem, nil
|
||||
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) SearchTraces(ctx context.Context, params *model.SearchTracesParams,
|
||||
smartTraceAlgorithm func(payload []model.SearchSpanResponseItem, targetSpanId string,
|
||||
levelUp int, levelDown int, spanLimit int) ([]model.SearchSpansResult, error)) (*[]model.SearchSpansResult, error) {
|
||||
|
||||
@@ -440,6 +440,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
|
||||
router.HandleFunc("/api/v1/service/top_operations", am.ViewAccess(aH.getTopOperations)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/service/top_level_operations", am.ViewAccess(aH.getServicesTopLevelOps)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(aH.SearchTraces)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/traces/{traceId}", am.ViewAccess(aH.SearchTracesV2)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/usage", am.ViewAccess(aH.getUsage)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/dependency_graph", am.ViewAccess(aH.dependencyGraph)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/settings/ttl", am.AdminAccess(aH.setTTL)).Methods(http.MethodPost)
|
||||
@@ -1684,6 +1685,22 @@ func (aH *APIHandler) getServicesList(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
|
||||
func (aH *APIHandler) SearchTracesV2(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
params, err := ParseSearchTracesV2Params(r)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading params")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := aH.reader.SearchTracesV2(r.Context(), params)
|
||||
if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
aH.WriteJSON(w, r, result)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) SearchTraces(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
params, err := ParseSearchTracesParams(r)
|
||||
|
||||
@@ -238,6 +238,16 @@ func parseGetServicesRequest(r *http.Request) (*model.GetServicesParams, error)
|
||||
postData.Period = int(postData.End.Unix() - postData.Start.Unix())
|
||||
return postData, nil
|
||||
}
|
||||
func ParseSearchTracesV2Params(r *http.Request) (*model.SearchTracesV2Params, error) {
|
||||
vars := mux.Vars(r)
|
||||
params := &model.SearchTracesV2Params{}
|
||||
err := json.NewDecoder(r.Body).Decode(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params.TraceID = vars["traceId"]
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func ParseSearchTracesParams(r *http.Request) (*model.SearchTracesParams, error) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
@@ -47,6 +47,7 @@ type Reader interface {
|
||||
|
||||
// Search Interfaces
|
||||
SearchTraces(ctx context.Context, params *model.SearchTracesParams, smartTraceAlgorithm func(payload []model.SearchSpanResponseItem, targetSpanId string, levelUp int, levelDown int, spanLimit int) ([]model.SearchSpansResult, error)) (*[]model.SearchSpansResult, error)
|
||||
SearchTracesV2(ctx context.Context, params *model.SearchTracesV2Params) (*model.SearchTracesV2Result, error)
|
||||
|
||||
// Setter Interfaces
|
||||
SetTTL(ctx context.Context, ttlParams *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError)
|
||||
|
||||
@@ -315,6 +315,12 @@ type SearchTracesParams struct {
|
||||
MaxSpansInTrace int `json:"maxSpansInTrace"`
|
||||
}
|
||||
|
||||
type SearchTracesV2Params struct {
|
||||
TraceID string `json:"traceId"`
|
||||
SpanID string `json:"spanId"`
|
||||
UncollapsedNodes []string `json:"uncollapsedNodes"`
|
||||
}
|
||||
|
||||
type SpanFilterParams struct {
|
||||
TraceID []string `json:"traceID"`
|
||||
Status []string `json:"status"`
|
||||
|
||||
@@ -210,6 +210,42 @@ type ServiceOverviewItem struct {
|
||||
ErrorRate float64 `json:"errorRate" ch:"errorRate"`
|
||||
}
|
||||
|
||||
// todo[@vikrantgupta25]: update the spans to correct types
|
||||
type SearchTracesV2Result struct {
|
||||
StartTimestampMillis uint64 `json:"startTimestampMillis"`
|
||||
EndTimestampMillis uint64 `json:"endTimestampMillis"`
|
||||
Spans []SpanNode `json:"spans"`
|
||||
TotalSpans int64 `json:"totalSpans"`
|
||||
UncollapsedNodes []string `json:"uncollapsedNodes"`
|
||||
}
|
||||
|
||||
type SearchSpanDBV2ResponseItem struct {
|
||||
Timestamp time.Time `ch:"timestamp"`
|
||||
TraceID string `ch:"traceID"`
|
||||
SpanID string `ch:"spanID"`
|
||||
ParentSpanID string `ch:"parentSpanID"`
|
||||
ServiceName string `ch:"serviceName"`
|
||||
Name string `ch:"name"`
|
||||
DurationNano uint64 `ch:"durationNano"`
|
||||
References string `ch:"references"`
|
||||
}
|
||||
|
||||
// todo[@vikrantgupta25]: check if the IsProcessed flag can be removed here!
|
||||
type SpanNode struct {
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
TraceID string `json:"traceID"`
|
||||
SpanID string `json:"spanID"`
|
||||
ParentSpanID string `json:"parentSpanID"`
|
||||
ServiceName string `json:"serviceName"`
|
||||
Name string `json:"name"`
|
||||
DurationNano uint64 `json:"durationNano"`
|
||||
References []OtelSpanRef `json:"references"`
|
||||
Children []*SpanNode `json:"spanNode"`
|
||||
IsProcessed bool `json:"isProcessed"`
|
||||
Level int64 `json:"level"`
|
||||
ChildrenCount int64 `json:"childrenCount"`
|
||||
}
|
||||
|
||||
type SearchSpansResult struct {
|
||||
StartTimestampMillis uint64 `json:"startTimestampMillis"`
|
||||
EndTimestampMillis uint64 `json:"endTimestampMillis"`
|
||||
|
||||
Reference in New Issue
Block a user