Compare commits

...

9 Commits

Author SHA1 Message Date
vikrantgupta25
5b3839dca9 feat: minor fixes 2024-10-23 16:33:15 +05:30
vikrantgupta25
767d7fc513 feat: make top reached changes for client 2024-10-23 15:48:10 +05:30
vikrantgupta25
58f3ab377b feat: handle scroll into view 2024-10-23 01:23:49 +05:30
vikrantgupta25
8da1817061 feat: a lot of frontend and a little query service changes 2024-10-23 01:11:41 +05:30
vikrantgupta25
d1205953c7 feat: pushing some more changes to build the traces on frontend 2024-10-22 18:16:38 +05:30
vikrantgupta25
a846caccd9 feat: frontend taking shape 2024-10-22 17:12:56 +05:30
vikrantgupta25
9ac5e48ac2 feat: added base setup for frontend code 2024-10-22 15:55:57 +05:30
vikrantgupta25
fc9219b0ab feat: setup the http handlers and ch reader for v2 trace details 2024-10-22 09:30:59 +05:30
vikrantgupta25
f4cbe854b1 feat: make the base query service changes for search traces v2 2024-10-22 09:10:14 +05:30
17 changed files with 752 additions and 1 deletions

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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(

View 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;

View 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;

View 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;

View 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 {
}
}
}
}
}
}

View File

@@ -0,0 +1 @@
export const LEFT_COL_WIDTH = 320;

View 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;

View 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[];
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"`

View File

@@ -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"`