mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
384 lines
10 KiB
Go
384 lines
10 KiB
Go
package traceql
|
|
|
|
import (
|
|
"math"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/grafana/tempo/pkg/tempopb"
|
|
"github.com/grafana/tempo/pkg/util"
|
|
)
|
|
|
|
type MetadataCombiner interface {
|
|
AddMetadata(*tempopb.TraceSearchMetadata) bool
|
|
IsCompleteFor(ts uint32) bool
|
|
|
|
Metadata() []*tempopb.TraceSearchMetadata
|
|
MetadataAfter(ts uint32) []*tempopb.TraceSearchMetadata
|
|
|
|
addSpanset(*Spanset)
|
|
}
|
|
|
|
const TimestampNever = uint32(math.MaxUint32)
|
|
|
|
func NewMetadataCombiner(limit int, keepMostRecent bool) MetadataCombiner {
|
|
if keepMostRecent {
|
|
return newMostRecentCombiner(limit)
|
|
}
|
|
|
|
return newAnyCombiner(limit)
|
|
}
|
|
|
|
type anyCombiner struct {
|
|
trs map[string]*tempopb.TraceSearchMetadata
|
|
limit int
|
|
}
|
|
|
|
func newAnyCombiner(limit int) *anyCombiner {
|
|
return &anyCombiner{
|
|
trs: make(map[string]*tempopb.TraceSearchMetadata, limit),
|
|
limit: limit,
|
|
}
|
|
}
|
|
|
|
// addSpanset adds a new spanset to the combiner. It only performs the asTraceSearchMetadata
|
|
// conversion if the spanset will be added
|
|
func (c *anyCombiner) addSpanset(ss *Spanset) {
|
|
// if it's already in the list, then we should add it
|
|
if _, ok := c.trs[util.TraceIDToHexString(ss.TraceID)]; ok {
|
|
c.AddMetadata(asTraceSearchMetadata(ss))
|
|
return
|
|
}
|
|
|
|
// if we don't have too many
|
|
if c.IsCompleteFor(0) {
|
|
return
|
|
}
|
|
|
|
c.AddMetadata(asTraceSearchMetadata(ss))
|
|
}
|
|
|
|
// AddMetadata adds the new metadata to the map. if it already exists
|
|
// use CombineSearchResults to combine the two
|
|
func (c *anyCombiner) AddMetadata(meta *tempopb.TraceSearchMetadata) bool {
|
|
if existing, ok := c.trs[meta.TraceID]; ok {
|
|
combineSearchResults(existing, meta)
|
|
return true
|
|
}
|
|
|
|
// if we don't have too many
|
|
if c.IsCompleteFor(0) {
|
|
return false
|
|
}
|
|
|
|
c.trs[meta.TraceID] = meta
|
|
return true
|
|
}
|
|
|
|
func (c *anyCombiner) Count() int {
|
|
return len(c.trs)
|
|
}
|
|
|
|
func (c *anyCombiner) Exists(id string) bool {
|
|
_, ok := c.trs[id]
|
|
return ok
|
|
}
|
|
|
|
func (c *anyCombiner) IsCompleteFor(_ uint32) bool {
|
|
return c.Count() >= c.limit && c.limit > 0
|
|
}
|
|
|
|
func (c *anyCombiner) Metadata() []*tempopb.TraceSearchMetadata {
|
|
m := make([]*tempopb.TraceSearchMetadata, 0, len(c.trs))
|
|
for _, tr := range c.trs {
|
|
m = append(m, tr)
|
|
}
|
|
sort.Slice(m, func(i, j int) bool {
|
|
return m[i].StartTimeUnixNano > m[j].StartTimeUnixNano
|
|
})
|
|
return m
|
|
}
|
|
|
|
// MetadataAfter returns all traces that started after the given time. anyCombiner has no concept of time so it just returns all traces
|
|
func (c *anyCombiner) MetadataAfter(_ uint32) []*tempopb.TraceSearchMetadata {
|
|
return c.Metadata()
|
|
}
|
|
|
|
type mostRecentCombiner struct {
|
|
trs map[string]*tempopb.TraceSearchMetadata
|
|
trsSorted []*tempopb.TraceSearchMetadata
|
|
keepMostRecent int
|
|
}
|
|
|
|
func newMostRecentCombiner(limit int) *mostRecentCombiner {
|
|
return &mostRecentCombiner{
|
|
trs: make(map[string]*tempopb.TraceSearchMetadata, limit),
|
|
trsSorted: make([]*tempopb.TraceSearchMetadata, 0, limit),
|
|
keepMostRecent: limit,
|
|
}
|
|
}
|
|
|
|
// addSpanset adds a new spanset to the combiner. It only performs the asTraceSearchMetadata
|
|
// conversion if the spanset will be added
|
|
func (c *mostRecentCombiner) addSpanset(ss *Spanset) {
|
|
// if we're not configured to keep most recent then just add it
|
|
if c.keepMostRecent == 0 || c.Count() < c.keepMostRecent {
|
|
c.AddMetadata(asTraceSearchMetadata(ss))
|
|
return
|
|
}
|
|
|
|
// else let's see if it's worth converting this to a metadata and adding it
|
|
// if it's already in the list, then we should add it
|
|
if _, ok := c.trs[util.TraceIDToHexString(ss.TraceID)]; ok {
|
|
c.AddMetadata(asTraceSearchMetadata(ss))
|
|
return
|
|
}
|
|
|
|
// if it's within range
|
|
if c.OldestTimestampNanos() <= ss.StartTimeUnixNanos {
|
|
c.AddMetadata(asTraceSearchMetadata(ss))
|
|
return
|
|
}
|
|
|
|
// this spanset is too old to bother converting and adding it
|
|
}
|
|
|
|
// AddMetadata adds the new metadata to the map. if it already exists
|
|
// use CombineSearchResults to combine the two
|
|
func (c *mostRecentCombiner) AddMetadata(meta *tempopb.TraceSearchMetadata) bool {
|
|
if existing, ok := c.trs[meta.TraceID]; ok {
|
|
combineSearchResults(existing, meta)
|
|
return true
|
|
}
|
|
|
|
if c.Count() == c.keepMostRecent && c.keepMostRecent > 0 {
|
|
// if this is older than the oldest element, bail
|
|
if c.OldestTimestampNanos() > meta.StartTimeUnixNano {
|
|
return false
|
|
}
|
|
|
|
// otherwise remove the oldest element and we'll add the new one below
|
|
oldest := c.trsSorted[c.Count()-1]
|
|
delete(c.trs, oldest.TraceID)
|
|
c.trsSorted = c.trsSorted[:len(c.trsSorted)-1]
|
|
}
|
|
|
|
// insert new in the right spot
|
|
c.trs[meta.TraceID] = meta
|
|
idx, _ := slices.BinarySearchFunc(c.trsSorted, meta, func(a, b *tempopb.TraceSearchMetadata) int {
|
|
if a.StartTimeUnixNano > b.StartTimeUnixNano {
|
|
return -1
|
|
}
|
|
return 1
|
|
})
|
|
c.trsSorted = slices.Insert(c.trsSorted, idx, meta)
|
|
return true
|
|
}
|
|
|
|
func (c *mostRecentCombiner) Count() int {
|
|
return len(c.trs)
|
|
}
|
|
|
|
func (c *mostRecentCombiner) Exists(id string) bool {
|
|
_, ok := c.trs[id]
|
|
return ok
|
|
}
|
|
|
|
// IsCompleteFor returns true if the combiner has reached the limit and all traces are after the given time
|
|
func (c *mostRecentCombiner) IsCompleteFor(ts uint32) bool {
|
|
if ts == TimestampNever {
|
|
return false
|
|
}
|
|
|
|
if c.Count() < c.keepMostRecent {
|
|
return false
|
|
}
|
|
|
|
return c.OldestTimestampNanos() > uint64(ts)*uint64(time.Second)
|
|
}
|
|
|
|
func (c *mostRecentCombiner) Metadata() []*tempopb.TraceSearchMetadata {
|
|
return c.trsSorted
|
|
}
|
|
|
|
// MetadataAfter returns all traces that started after the given time
|
|
func (c *mostRecentCombiner) MetadataAfter(afterSeconds uint32) []*tempopb.TraceSearchMetadata {
|
|
afterNanos := uint64(afterSeconds) * uint64(time.Second)
|
|
afterTraces := make([]*tempopb.TraceSearchMetadata, 0, len(c.trsSorted))
|
|
|
|
for _, tr := range c.trsSorted {
|
|
if tr.StartTimeUnixNano > afterNanos {
|
|
afterTraces = append(afterTraces, tr)
|
|
}
|
|
}
|
|
|
|
return afterTraces
|
|
}
|
|
|
|
func (c *mostRecentCombiner) OldestTimestampNanos() uint64 {
|
|
if len(c.trsSorted) == 0 {
|
|
return 0
|
|
}
|
|
|
|
return c.trsSorted[len(c.trsSorted)-1].StartTimeUnixNano
|
|
}
|
|
|
|
// combineSearchResults overlays the incoming search result with the existing result. This is required
|
|
// for the following reason: a trace may be present in multiple blocks, or in partial segments
|
|
// in live traces. The results should reflect elements of all segments.
|
|
func combineSearchResults(existing *tempopb.TraceSearchMetadata, incoming *tempopb.TraceSearchMetadata) {
|
|
if existing.TraceID == "" {
|
|
existing.TraceID = incoming.TraceID
|
|
}
|
|
|
|
if existing.RootServiceName == "" {
|
|
existing.RootServiceName = incoming.RootServiceName
|
|
}
|
|
|
|
if existing.RootTraceName == "" {
|
|
existing.RootTraceName = incoming.RootTraceName
|
|
}
|
|
|
|
// Earliest start time.
|
|
if existing.StartTimeUnixNano > incoming.StartTimeUnixNano || existing.StartTimeUnixNano == 0 {
|
|
existing.StartTimeUnixNano = incoming.StartTimeUnixNano
|
|
}
|
|
|
|
// Longest duration
|
|
if existing.DurationMs < incoming.DurationMs || existing.DurationMs == 0 {
|
|
existing.DurationMs = incoming.DurationMs
|
|
}
|
|
|
|
// Combine service stats
|
|
// It's possible to find multiple trace fragments that satisfy a TraceQL result,
|
|
// therefore we use max() to merge the ServiceStats.
|
|
for service, incomingStats := range incoming.ServiceStats {
|
|
existingStats, ok := existing.ServiceStats[service]
|
|
if !ok {
|
|
existingStats = &tempopb.ServiceStats{}
|
|
if existing.ServiceStats == nil {
|
|
existing.ServiceStats = make(map[string]*tempopb.ServiceStats)
|
|
}
|
|
existing.ServiceStats[service] = existingStats
|
|
}
|
|
existingStats.SpanCount = max(existingStats.SpanCount, incomingStats.SpanCount)
|
|
existingStats.ErrorCount = max(existingStats.ErrorCount, incomingStats.ErrorCount)
|
|
}
|
|
|
|
// make a map of existing Spansets
|
|
existingSS := make(map[string]*tempopb.SpanSet)
|
|
for _, ss := range existing.SpanSets {
|
|
existingSS[spansetID(ss)] = ss
|
|
}
|
|
|
|
// add any new spansets
|
|
for _, ss := range incoming.SpanSets {
|
|
id := spansetID(ss)
|
|
// if not found just add directly
|
|
if _, ok := existingSS[id]; !ok {
|
|
existing.SpanSets = append(existing.SpanSets, ss)
|
|
continue
|
|
}
|
|
|
|
// otherwise combine with existing
|
|
combineSpansets(existingSS[id], ss)
|
|
}
|
|
|
|
// choose an arbitrary spanset to be the "main" one. this field is deprecated
|
|
if len(existing.SpanSets) > 0 {
|
|
existing.SpanSet = existing.SpanSets[0]
|
|
}
|
|
}
|
|
|
|
// combineSpansets "combines" spansets. This isn't actually possible so it just
|
|
// choose the spanset that has the highest "Matched" number as it is hopefully
|
|
// more representative of the spanset
|
|
func combineSpansets(existing *tempopb.SpanSet, incoming *tempopb.SpanSet) {
|
|
if existing.Matched >= incoming.Matched {
|
|
return
|
|
}
|
|
|
|
existing.Matched = incoming.Matched
|
|
existing.Attributes = incoming.Attributes
|
|
existing.Spans = incoming.Spans
|
|
}
|
|
|
|
func spansetID(ss *tempopb.SpanSet) string {
|
|
id := ""
|
|
|
|
for _, s := range ss.Attributes {
|
|
// any attributes that start with "by" are considered to be part of the spanset identity
|
|
if strings.HasPrefix(s.Key, "by") {
|
|
id += s.Key + s.Value.String()
|
|
}
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
type QueryRangeCombiner struct {
|
|
req *tempopb.QueryRangeRequest
|
|
eval *MetricsFrontendEvaluator
|
|
metrics *tempopb.SearchMetrics
|
|
|
|
maxSeries int
|
|
maxSeriesReached bool
|
|
}
|
|
|
|
func QueryRangeCombinerFor(req *tempopb.QueryRangeRequest, mode AggregateMode, maxSeriesLimit int) (*QueryRangeCombiner, error) {
|
|
eval, err := NewEngine().CompileMetricsQueryRangeNonRaw(req, mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &QueryRangeCombiner{
|
|
req: req,
|
|
eval: eval,
|
|
maxSeries: maxSeriesLimit,
|
|
metrics: &tempopb.SearchMetrics{},
|
|
}, nil
|
|
}
|
|
|
|
func (q *QueryRangeCombiner) Combine(resp *tempopb.QueryRangeResponse) {
|
|
if resp == nil || q.maxSeriesReached {
|
|
return
|
|
}
|
|
|
|
// Here is where the job results are reentered into the pipeline
|
|
q.eval.ObserveSeries(resp.Series)
|
|
seriesCount := q.eval.Length()
|
|
|
|
if (q.maxSeries > 0 && seriesCount >= q.maxSeries) || resp.Status == tempopb.PartialStatus_PARTIAL {
|
|
q.maxSeriesReached = true
|
|
}
|
|
|
|
// TODO: this is here only for the querier
|
|
// we want to use the metrics combiner from the frontend
|
|
if resp.Metrics != nil {
|
|
q.metrics.TotalJobs += resp.Metrics.TotalJobs
|
|
q.metrics.TotalBlocks += resp.Metrics.TotalBlocks
|
|
q.metrics.TotalBlockBytes += resp.Metrics.TotalBlockBytes
|
|
q.metrics.InspectedBytes += resp.Metrics.InspectedBytes
|
|
q.metrics.InspectedTraces += resp.Metrics.InspectedTraces
|
|
q.metrics.InspectedSpans += resp.Metrics.InspectedSpans
|
|
q.metrics.CompletedJobs += resp.Metrics.CompletedJobs
|
|
}
|
|
}
|
|
|
|
func (q *QueryRangeCombiner) Response() *tempopb.QueryRangeResponse {
|
|
response := &tempopb.QueryRangeResponse{
|
|
Series: q.eval.Results().ToProto(q.req),
|
|
Metrics: q.metrics,
|
|
}
|
|
if q.maxSeriesReached {
|
|
response.Status = tempopb.PartialStatus_PARTIAL
|
|
}
|
|
return response
|
|
}
|
|
|
|
func (q *QueryRangeCombiner) MaxSeriesReached() bool {
|
|
return q.maxSeriesReached
|
|
}
|