mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-02 10:20:24 +01:00
Compare commits
7 Commits
refactor/c
...
ns/waterfa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e93814b9e | ||
|
|
5add200a47 | ||
|
|
fb74637a97 | ||
|
|
4e9b3f3b0f | ||
|
|
ed5c30012d | ||
|
|
fdaa52f2b0 | ||
|
|
e0a3392d02 |
@@ -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
|
||||
}
|
||||
56
pkg/modules/tracedetail/impltracedetail/handler.go
Normal file
56
pkg/modules/tracedetail/impltracedetail/handler.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package impltracedetail
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"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) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
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(r.Context(), 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.Span{}
|
||||
var traceRoots []*tracedetailtypes.Span
|
||||
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.Info("cache miss for v3 waterfall", "traceID", 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.Span, len(spanItems))
|
||||
|
||||
// Build span nodes
|
||||
for _, item := range spanItems {
|
||||
span := item.ToSpan()
|
||||
startTimeUnixNano := span.TimeUnixNano
|
||||
|
||||
// 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.Span{
|
||||
SpanID: spanNode.ParentSpanID,
|
||||
TraceID: spanNode.TraceID,
|
||||
Name: "Missing Span",
|
||||
TimeUnixNano: spanNode.TimeUnixNano,
|
||||
DurationNano: spanNode.DurationNano,
|
||||
Events: make([]tracedetailtypes.Event, 0),
|
||||
Children: make([]*tracedetailtypes.Span, 0),
|
||||
Attributes: make(map[string]any),
|
||||
Resources: 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].TimeUnixNano == traceRoots[j].TimeUnixNano {
|
||||
return traceRoots[i].Name < traceRoots[j].Name
|
||||
}
|
||||
return traceRoots[i].TimeUnixNano < traceRoots[j].TimeUnixNano
|
||||
})
|
||||
|
||||
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", "traceID", traceID, "error", cacheErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Span selection: all spans or windowed
|
||||
limit := min(req.Limit, MaxLimitToSelectAllSpans)
|
||||
selectAllSpans := totalSpans <= uint64(limit)
|
||||
|
||||
var (
|
||||
selectedSpans []*tracedetailtypes.Span
|
||||
uncollapsedSpans []string
|
||||
rootServiceName, rootServiceEntryPoint string
|
||||
)
|
||||
if selectAllSpans {
|
||||
selectedSpans, rootServiceName, rootServiceEntryPoint = GetAllSpans(traceRoots)
|
||||
} else {
|
||||
selectedSpans, uncollapsedSpans, rootServiceName, rootServiceEntryPoint = GetSelectedSpans(
|
||||
req.UncollapsedSpans, req.SelectedSpanID, traceRoots, spanIDToSpanNodeMap, req.IsSelectedSpanIDUnCollapsed,
|
||||
)
|
||||
}
|
||||
|
||||
// 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.DurationNano = durationNano
|
||||
response.TotalSpansCount = totalSpans
|
||||
response.TotalErrorSpansCount = totalErrorSpans
|
||||
response.RootServiceName = rootServiceName
|
||||
response.RootServiceEntryPoint = rootServiceEntryPoint
|
||||
response.ServiceNameToTotalDurationMap = serviceNameToTotalDurationMap
|
||||
response.HasMissingSpans = hasMissingSpans
|
||||
|
||||
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", "traceID", 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", "traceID", traceID)
|
||||
return cachedData, nil
|
||||
}
|
||||
|
||||
func containsSpan(spans []*tracedetailtypes.Span, target *tracedetailtypes.Span) bool {
|
||||
for _, s := range spans {
|
||||
if s.SpanID == target.SpanID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
193
pkg/modules/tracedetail/impltracedetail/waterfall.go
Normal file
193
pkg/modules/tracedetail/impltracedetail/waterfall.go
Normal file
@@ -0,0 +1,193 @@
|
||||
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
|
||||
isSelectedSpanUncollapsed bool
|
||||
selectAll bool
|
||||
}
|
||||
|
||||
func traverseTrace(
|
||||
span *tracedetailtypes.Span,
|
||||
opts traverseOpts,
|
||||
level uint64,
|
||||
isPartOfPreOrder bool,
|
||||
hasSibling bool,
|
||||
autoExpandDepth int,
|
||||
) ([]*tracedetailtypes.Span, []string) {
|
||||
|
||||
preOrderTraversal := []*tracedetailtypes.Span{}
|
||||
autoExpandedSpans := []string{}
|
||||
|
||||
span.SubTreeNodeCount = 0
|
||||
nodeWithoutChildren := span.CopyWithoutChildren(level, hasSibling)
|
||||
|
||||
if isPartOfPreOrder {
|
||||
preOrderTraversal = append(preOrderTraversal, nodeWithoutChildren)
|
||||
}
|
||||
|
||||
remainingAutoExpandDepth := 0
|
||||
if span.SpanID == opts.selectedSpanID && opts.isSelectedSpanUncollapsed {
|
||||
remainingAutoExpandDepth = maxDepthForSelectedChildren
|
||||
} else if autoExpandDepth > 0 {
|
||||
remainingAutoExpandDepth = autoExpandDepth - 1
|
||||
}
|
||||
|
||||
_, isAlreadyUncollapsed := opts.uncollapsedSpans[span.SpanID]
|
||||
for index, 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, index != (len(span.Children)-1), 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.Span, spanIDToSpanNodeMap map[string]*tracedetailtypes.Span, isSelectedSpanIDUnCollapsed bool) ([]*tracedetailtypes.Span, []string, string, string) {
|
||||
var preOrderTraversal = make([]*tracedetailtypes.Span, 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 && !isSelectedSpanIDUnCollapsed {
|
||||
continue
|
||||
}
|
||||
uncollapsedSpanMap[spanID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
opts := traverseOpts{
|
||||
uncollapsedSpans: uncollapsedSpanMap,
|
||||
selectedSpanID: selectedSpanID,
|
||||
isSelectedSpanUncollapsed: isSelectedSpanIDUnCollapsed,
|
||||
}
|
||||
traversal, autoExpanded := traverseTrace(rootNode, opts, 0, true, false, 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.Span) (spans []*tracedetailtypes.Span, 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, false, 0)
|
||||
spans = append(spans, childSpans...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getPathFromRootToSelectedSpanID(node *tracedetailtypes.Span, 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.Span, 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.Span) {
|
||||
sort.Slice(span.Children, func(i, j int) bool {
|
||||
if span.Children[i].TimeUnixNano == span.Children[j].TimeUnixNano {
|
||||
return span.Children[i].Name < span.Children[j].Name
|
||||
}
|
||||
return span.Children[i].TimeUnixNano < span.Children[j].TimeUnixNano
|
||||
})
|
||||
for _, child := range span.Children {
|
||||
SortSpanChildren(child)
|
||||
}
|
||||
}
|
||||
580
pkg/modules/tracedetail/impltracedetail/waterfall_test.go
Normal file
580
pkg/modules/tracedetail/impltracedetail/waterfall_test.go
Normal file
@@ -0,0 +1,580 @@
|
||||
// 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.
|
||||
//
|
||||
// isSelectedSpanIDUnCollapsed
|
||||
//
|
||||
// Controls whether the selected span's own children are shown:
|
||||
// true — user expanded the span (click-to-open in waterfall or flamegraph);
|
||||
// direct children of the selected span are included.
|
||||
// false — user selected without expanding;
|
||||
// the span is visible but its children remain hidden.
|
||||
//
|
||||
// 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.Span) *tracedetailtypes.Span {
|
||||
return &tracedetailtypes.Span{
|
||||
SpanID: id,
|
||||
ServiceName: service,
|
||||
Name: id + "-op",
|
||||
Children: children,
|
||||
}
|
||||
}
|
||||
|
||||
func spanIDs(spans []*tracedetailtypes.Span) []string {
|
||||
ids := make([]string, len(spans))
|
||||
for i, s := range spans {
|
||||
ids[i] = s.SpanID
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func buildSpanMap(roots ...*tracedetailtypes.Span) map[string]*tracedetailtypes.Span {
|
||||
m := map[string]*tracedetailtypes.Span{}
|
||||
var walk func(*tracedetailtypes.Span)
|
||||
walk = func(s *tracedetailtypes.Span) {
|
||||
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.Span, map[string]*tracedetailtypes.Span, []string) {
|
||||
spans := make([]*tracedetailtypes.Span, 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.Span, map[string]*tracedetailtypes.Span)
|
||||
uncollapsedSpans []string
|
||||
selectedSpanID string
|
||||
isSelectedSpanIDUnCollapsed bool
|
||||
wantSpanIDs []string
|
||||
}{
|
||||
{
|
||||
// Pre-order traversal is preserved: parent before children, siblings left-to-right.
|
||||
name: "pre_order_traversal",
|
||||
buildRoots: func() ([]*tracedetailtypes.Span, map[string]*tracedetailtypes.Span) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("child1", "svc", mkSpan("grandchild", "svc")),
|
||||
mkSpan("child2", "svc"),
|
||||
)
|
||||
return []*tracedetailtypes.Span{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"root", "child1"},
|
||||
selectedSpanID: "root",
|
||||
isSelectedSpanIDUnCollapsed: false,
|
||||
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.Span, map[string]*tracedetailtypes.Span) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("childA", "svc", mkSpan("grandchildA", "svc")),
|
||||
mkSpan("childB", "svc", mkSpan("grandchildB", "svc")),
|
||||
)
|
||||
return []*tracedetailtypes.Span{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"root", "childA", "childB"},
|
||||
selectedSpanID: "root",
|
||||
isSelectedSpanIDUnCollapsed: false,
|
||||
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.Span, map[string]*tracedetailtypes.Span) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("childA", "svc",
|
||||
mkSpan("grandchild1", "svc", mkSpan("greatGrandchild", "svc")),
|
||||
mkSpan("grandchild2", "svc"),
|
||||
),
|
||||
mkSpan("childB", "svc"),
|
||||
)
|
||||
return []*tracedetailtypes.Span{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"childA"},
|
||||
selectedSpanID: "childB",
|
||||
isSelectedSpanIDUnCollapsed: false,
|
||||
wantSpanIDs: []string{"root", "childA", "grandchild1", "grandchild2", "childB"},
|
||||
},
|
||||
{
|
||||
// A collapsed span hides all children.
|
||||
name: "collapsed_span_hides_children",
|
||||
buildRoots: func() ([]*tracedetailtypes.Span, map[string]*tracedetailtypes.Span) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("child1", "svc"),
|
||||
mkSpan("child2", "svc"),
|
||||
)
|
||||
return []*tracedetailtypes.Span{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "root",
|
||||
isSelectedSpanIDUnCollapsed: false,
|
||||
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.Span, map[string]*tracedetailtypes.Span) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
return []*tracedetailtypes.Span{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "selected",
|
||||
isSelectedSpanIDUnCollapsed: false,
|
||||
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.Span, map[string]*tracedetailtypes.Span) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("unrelated", "svc", mkSpan("unrelated-child", "svc")),
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
return []*tracedetailtypes.Span{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "selected",
|
||||
isSelectedSpanIDUnCollapsed: false,
|
||||
// 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.Span, map[string]*tracedetailtypes.Span) {
|
||||
root := mkSpan("root", "svc", mkSpan("child", "svc"))
|
||||
return []*tracedetailtypes.Span{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "nonexistent",
|
||||
isSelectedSpanIDUnCollapsed: false,
|
||||
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, tc.isSelectedSpanIDUnCollapsed)
|
||||
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.Span{root1, root2}, spanMap, false)
|
||||
|
||||
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.Span, map[string]*tracedetailtypes.Span)
|
||||
uncollapsedSpans []string
|
||||
selectedSpanID string
|
||||
isSelectedSpanIDUnCollapsed bool
|
||||
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.Span, map[string]*tracedetailtypes.Span) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
return root, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "selected",
|
||||
isSelectedSpanIDUnCollapsed: false,
|
||||
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.Span, map[string]*tracedetailtypes.Span) {
|
||||
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",
|
||||
isSelectedSpanIDUnCollapsed: false,
|
||||
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 (selected, expanded)
|
||||
// ├─ childA (internal ✓)
|
||||
// │ └─ grandchildA (internal ✓)
|
||||
// │ └─ leafA (leaf ✗)
|
||||
// └─ childB (internal ✓)
|
||||
// └─ grandchildB (internal ✓)
|
||||
// └─ leafB (leaf ✗)
|
||||
name: "auto_expanded_spans_returned",
|
||||
buildRoot: func() (*tracedetailtypes.Span, map[string]*tracedetailtypes.Span) {
|
||||
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{},
|
||||
selectedSpanID: "root",
|
||||
isSelectedSpanIDUnCollapsed: true,
|
||||
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 AND isSelectedSpanIDUnCollapsed=true,
|
||||
// it should appear exactly once in the result.
|
||||
name: "duplicate_in_uncollapsed",
|
||||
buildRoot: func() (*tracedetailtypes.Span, map[string]*tracedetailtypes.Span) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("selected", "svc", mkSpan("child", "svc")),
|
||||
)
|
||||
return root, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"selected"}, // already present
|
||||
selectedSpanID: "selected",
|
||||
isSelectedSpanIDUnCollapsed: true,
|
||||
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.Span{root}, spanMap, tc.isSelectedSpanIDUnCollapsed)
|
||||
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, HasSiblings, and SubTreeNodeCount are populated correctly.
|
||||
//
|
||||
// root level=0, hasChildren=true, hasSiblings=false, subTree=4
|
||||
// child1 level=1, hasChildren=true, hasSiblings=true, subTree=2
|
||||
// grandchild level=2, hasChildren=false, hasSiblings=false, subTree=1
|
||||
// child2 level=1, hasChildren=false, hasSiblings=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.Span{root}, spanMap, false)
|
||||
|
||||
byID := map[string]*tracedetailtypes.Span{}
|
||||
for _, s := range spans {
|
||||
byID[s.SpanID] = s
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
spanID string
|
||||
wantLevel uint64
|
||||
wantHasChildren bool
|
||||
wantHasSiblings bool
|
||||
wantSubTree uint64
|
||||
}{
|
||||
{"root", 0, true, false, 4},
|
||||
{"child1", 1, true, true, 2},
|
||||
{"child2", 1, false, false, 1},
|
||||
{"grandchild", 2, false, 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.wantHasSiblings, s.HasSiblings)
|
||||
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.Span{root}, spanMap, false)
|
||||
|
||||
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", []*tracedetailtypes.Span{root}, spanMap, true)
|
||||
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.Span{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)
|
||||
}
|
||||
19
pkg/modules/tracedetail/tracedetail.go
Normal file
19
pkg/modules/tracedetail/tracedetail.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package tracedetail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// Handler exposes HTTP handlers for trace detail APIs.
|
||||
type Handler interface {
|
||||
GetWaterfall(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
// Module defines the business logic for trace detail operations.
|
||||
type Module interface {
|
||||
GetWaterfall(ctx context.Context, orgID valuer.UUID, traceID string, req *tracedetailtypes.WaterfallRequest) (*tracedetailtypes.WaterfallResponse, error)
|
||||
}
|
||||
@@ -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, emailing, providerSettings),
|
||||
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
|
||||
|
||||
@@ -281,6 +281,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.RegistryHandler,
|
||||
handlers.CloudIntegrationHandler,
|
||||
handlers.RuleStateHistory,
|
||||
handlers.TraceDetail,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
249
pkg/types/tracedetailtypes/waterfall.go
Normal file
249
pkg/types/tracedetailtypes/waterfall.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package tracedetailtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/cachetypes"
|
||||
)
|
||||
|
||||
// WaterfallRequest is the request body for the v3 waterfall API.
|
||||
type WaterfallRequest struct {
|
||||
SelectedSpanID string `json:"selectedSpanId"`
|
||||
IsSelectedSpanIDUnCollapsed bool `json:"isSelectedSpanIDUnCollapsed"`
|
||||
UncollapsedSpans []string `json:"uncollapsedSpans"`
|
||||
Limit uint `json:"limit"`
|
||||
}
|
||||
|
||||
// WaterfallResponse is the response for the v3 waterfall API.
|
||||
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"`
|
||||
TotalErrorSpansCount uint64 `json:"totalErrorSpansCount"`
|
||||
ServiceNameToTotalDurationMap map[string]uint64 `json:"serviceNameToTotalDurationMap"`
|
||||
Spans []*Span `json:"spans"`
|
||||
HasMissingSpans bool `json:"hasMissingSpans"`
|
||||
UncollapsedSpans []string `json:"uncollapsedSpans"`
|
||||
}
|
||||
|
||||
// Event represents a span event.
|
||||
type Event struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
TimeUnixNano uint64 `json:"timeUnixNano,omitempty"`
|
||||
AttributeMap map[string]any `json:"attributeMap,omitempty"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
}
|
||||
|
||||
// Span represents the span in waterfall response,
|
||||
// this uses snake_case keys for response as a special case since these
|
||||
// keys can be directly used to query spans and client need to know the actual fields.
|
||||
// This pattern should not be copied elsewhere.
|
||||
type Span struct {
|
||||
Attributes map[string]any `json:"attributes"`
|
||||
DBName string `json:"db_name"`
|
||||
DBOperation string `json:"db_operation"`
|
||||
DurationNano uint64 `json:"duration_nano"`
|
||||
Events []Event `json:"events"`
|
||||
ExternalHTTPMethod string `json:"external_http_method"`
|
||||
ExternalHTTPURL string `json:"external_http_url"`
|
||||
Flags uint32 `json:"flags"`
|
||||
HasError bool `json:"has_error"`
|
||||
HTTPHost string `json:"http_host"`
|
||||
HTTPMethod string `json:"http_method"`
|
||||
HTTPURL string `json:"http_url"`
|
||||
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"`
|
||||
Resources map[string]string `json:"resources"`
|
||||
ResponseStatusCode string `json:"response_status_code"`
|
||||
SpanID string `json:"span_id"`
|
||||
StatusCode int16 `json:"status_code"`
|
||||
StatusCodeString string `json:"status_code_string"`
|
||||
StatusMessage string `json:"status_message"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
TraceID string `json:"trace_id"`
|
||||
TraceState string `json:"trace_state"`
|
||||
// Tree structure fields
|
||||
Children []*Span `json:"children"`
|
||||
SubTreeNodeCount uint64 `json:"sub_tree_node_count"`
|
||||
HasChildren bool `json:"has_children"`
|
||||
HasSiblings bool `json:"has_siblings"`
|
||||
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.
|
||||
ServiceName string `json:"-"`
|
||||
}
|
||||
|
||||
// CopyWithoutChildren creates a shallow copy and reset computed tree fields.
|
||||
func (s *Span) CopyWithoutChildren(level uint64, hasSiblings bool) *Span {
|
||||
cp := *s
|
||||
cp.Level = level
|
||||
cp.HasChildren = len(s.Children) > 0
|
||||
cp.HasSiblings = hasSiblings
|
||||
cp.Children = make([]*Span, 0)
|
||||
cp.SubTreeNodeCount = 0
|
||||
return &cp
|
||||
}
|
||||
|
||||
// SpanModel is the ClickHouse scan struct for the v3 waterfall query.
|
||||
type SpanModel struct {
|
||||
TimeUnixNano time.Time `ch:"timestamp"`
|
||||
DurationNano uint64 `ch:"duration_nano"`
|
||||
SpanID string `ch:"span_id"`
|
||||
TraceID string `ch:"trace_id"`
|
||||
HasError bool `ch:"has_error"`
|
||||
Kind int8 `ch:"kind"`
|
||||
ServiceName string `ch:"resource_string_service$$name"`
|
||||
Name string `ch:"name"`
|
||||
References string `ch:"references"`
|
||||
AttributesString map[string]string `ch:"attributes_string"`
|
||||
AttributesNumber map[string]float64 `ch:"attributes_number"`
|
||||
AttributesBool map[string]bool `ch:"attributes_bool"`
|
||||
ResourcesString map[string]string `ch:"resources_string"`
|
||||
Events []string `ch:"events"`
|
||||
StatusMessage string `ch:"status_message"`
|
||||
StatusCodeString string `ch:"status_code_string"`
|
||||
SpanKind string `ch:"kind_string"`
|
||||
ParentSpanID string `ch:"parent_span_id"`
|
||||
Flags uint32 `ch:"flags"`
|
||||
IsRemote string `ch:"is_remote"`
|
||||
TraceState string `ch:"trace_state"`
|
||||
StatusCode int16 `ch:"status_code"`
|
||||
DBName string `ch:"db_name"`
|
||||
DBOperation string `ch:"db_operation"`
|
||||
HTTPMethod string `ch:"http_method"`
|
||||
HTTPURL string `ch:"http_url"`
|
||||
HTTPHost string `ch:"http_host"`
|
||||
ExternalHTTPMethod string `ch:"external_http_method"`
|
||||
ExternalHTTPURL string `ch:"external_http_url"`
|
||||
ResponseStatusCode string `ch:"response_status_code"`
|
||||
}
|
||||
|
||||
// ToSpan converts a SpanModel (ClickHouse scan result) into a Span for the waterfall response.
|
||||
func (item *SpanModel) ToSpan() *Span {
|
||||
// Merge attributes_string, attributes_number, attributes_bool preserving native types
|
||||
attributes := make(map[string]any, len(item.AttributesString)+len(item.AttributesNumber)+len(item.AttributesBool))
|
||||
for k, v := range item.AttributesString {
|
||||
attributes[k] = v
|
||||
}
|
||||
for k, v := range item.AttributesNumber {
|
||||
attributes[k] = v
|
||||
}
|
||||
for k, v := range item.AttributesBool {
|
||||
attributes[k] = v
|
||||
}
|
||||
|
||||
resources := make(map[string]string)
|
||||
maps.Copy(resources, item.ResourcesString)
|
||||
|
||||
events := make([]Event, 0, len(item.Events))
|
||||
for _, eventStr := range item.Events {
|
||||
var event Event
|
||||
if err := json.Unmarshal([]byte(eventStr), &event); err != nil {
|
||||
continue
|
||||
}
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
return &Span{
|
||||
Attributes: attributes,
|
||||
DBName: item.DBName,
|
||||
DBOperation: item.DBOperation,
|
||||
DurationNano: item.DurationNano,
|
||||
Events: events,
|
||||
ExternalHTTPMethod: item.ExternalHTTPMethod,
|
||||
ExternalHTTPURL: item.ExternalHTTPURL,
|
||||
Flags: item.Flags,
|
||||
HasError: item.HasError,
|
||||
HTTPHost: item.HTTPHost,
|
||||
HTTPMethod: item.HTTPMethod,
|
||||
HTTPURL: item.HTTPURL,
|
||||
IsRemote: item.IsRemote,
|
||||
Kind: int32(item.Kind),
|
||||
KindString: item.SpanKind,
|
||||
Links: item.References,
|
||||
Name: item.Name,
|
||||
ParentSpanID: item.ParentSpanID,
|
||||
Resources: resources,
|
||||
ResponseStatusCode: item.ResponseStatusCode,
|
||||
SpanID: item.SpanID,
|
||||
StatusCode: item.StatusCode,
|
||||
StatusCodeString: item.StatusCodeString,
|
||||
StatusMessage: item.StatusMessage,
|
||||
Timestamp: item.TimeUnixNano.Format(time.RFC3339Nano),
|
||||
TraceID: item.TraceID,
|
||||
TraceState: item.TraceState,
|
||||
Children: make([]*Span, 0),
|
||||
TimeUnixNano: uint64(item.TimeUnixNano.UnixNano()),
|
||||
ServiceName: item.ServiceName,
|
||||
}
|
||||
}
|
||||
|
||||
// TraceSummary is the ClickHouse scan struct for the trace_summary query.
|
||||
type TraceSummary struct {
|
||||
TraceID string `ch:"trace_id"`
|
||||
Start time.Time `ch:"start"`
|
||||
End time.Time `ch:"end"`
|
||||
NumSpans uint64 `ch:"num_spans"`
|
||||
}
|
||||
|
||||
// OtelSpanRef is used for parsing the references/links JSON from ClickHouse.
|
||||
type OtelSpanRef struct {
|
||||
TraceId string `json:"traceId,omitempty"`
|
||||
SpanId string `json:"spanId,omitempty"`
|
||||
RefType string `json:"refType,omitempty"`
|
||||
}
|
||||
|
||||
// WaterfallCache holds pre-processed trace data for caching.
|
||||
type WaterfallCache struct {
|
||||
StartTime uint64 `json:"startTime"`
|
||||
EndTime uint64 `json:"endTime"`
|
||||
DurationNano uint64 `json:"durationNano"`
|
||||
TotalSpans uint64 `json:"totalSpans"`
|
||||
TotalErrorSpans uint64 `json:"totalErrorSpans"`
|
||||
ServiceNameToTotalDurationMap map[string]uint64 `json:"serviceNameToTotalDurationMap"`
|
||||
SpanIDToSpanNodeMap map[string]*Span `json:"spanIdToSpanNodeMap"`
|
||||
TraceRoots []*Span `json:"traceRoots"`
|
||||
HasMissingSpans bool `json:"hasMissingSpans"`
|
||||
}
|
||||
|
||||
func (c *WaterfallCache) Clone() cachetypes.Cacheable {
|
||||
copyOfServiceNameToTotalDurationMap := make(map[string]uint64)
|
||||
maps.Copy(copyOfServiceNameToTotalDurationMap, c.ServiceNameToTotalDurationMap)
|
||||
|
||||
copyOfSpanIDToSpanNodeMap := make(map[string]*Span)
|
||||
maps.Copy(copyOfSpanIDToSpanNodeMap, c.SpanIDToSpanNodeMap)
|
||||
|
||||
copyOfTraceRoots := make([]*Span, len(c.TraceRoots))
|
||||
copy(copyOfTraceRoots, c.TraceRoots)
|
||||
return &WaterfallCache{
|
||||
StartTime: c.StartTime,
|
||||
EndTime: c.EndTime,
|
||||
DurationNano: c.DurationNano,
|
||||
TotalSpans: c.TotalSpans,
|
||||
TotalErrorSpans: c.TotalErrorSpans,
|
||||
ServiceNameToTotalDurationMap: copyOfServiceNameToTotalDurationMap,
|
||||
SpanIDToSpanNodeMap: copyOfSpanIDToSpanNodeMap,
|
||||
TraceRoots: copyOfTraceRoots,
|
||||
HasMissingSpans: c.HasMissingSpans,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WaterfallCache) MarshalBinary() (data []byte, err error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
func (c *WaterfallCache) UnmarshalBinary(data []byte) error {
|
||||
return json.Unmarshal(data, c)
|
||||
}
|
||||
Reference in New Issue
Block a user